-
Notifications
You must be signed in to change notification settings - Fork 6
Home
Laravel.js is a front-end framework for javascript applications that provides a dependency injection container and familiar framework design that encourages you to use object oriented principals in your frontend application.
The container is the heart of the application and has many useful methods for interacting with services and bindings.
/**
* Bootstrap the application
*/
import ErrorHandler from "./MyErrorHandler"
import Application from "./Application"
import Providers from "./ServiceProviders"
const app = new Application
app.errorHandler(Handler)
Providers.forEach((ServiceProvider)=>{
app.register(ServiceProvider)
})
app.boot()
app.start()
const PipeState = { state: 0 }
import {PipeA, PipeB, PipeC} from "./Mocks"
app.bind('App', App)
app.bind('PipeA', PipeA)
app.bind('PipeB', PipeB)
app.bind('PipeC', PipeC)
const result = (new Pipeline(App))
.send(PipeState)
.through([PipeA, PipeB, PipeC])
.via('handle')
.then((obj) => {
obj.state = (obj.state * 2)
return obj
})
//result.state === 10
The global middleware pipeline can pass an instance of "something" (whatever type of object you want it to be) to each subsuquent class until it's final state can be acted upon.
import LaravelMicro from "laravel-micro.js"
import Authenticate from "./Middleware/Authenticate"
export default class Application extends LaravelMicro {
/** Application Constructor */
constructor() {
super()
}
/** Boot the Application Service Providers */
boot() {
this.bootProviders()
}
/** Start the Primary Application Service */
start() {
this.make('VueRoot').$mount('#app')
}
/**
* Run the Request through the Middleware Kernel
* (called before every route)
* @return void
*/
run(request) {
//Make the Application Kernel instance.
const kernel = this.make('Kernel')
//Set Global Middleware on the Kernel.
kernel.setMiddleware([
Authenticate,
RandomLoadingText
])
//Get the response from the middleware kernel.
kernel.handle(request, (finalRequest) => {
const next = finalRequest.next
if (this.isCallable(next)) next()
})
}
}
import Routes from "./Routes"
import Root from "./Root"
this.app.bind('Router', () => new VueRouter(Routes))
//Add Root Vue Instance
this.app.bind('VueRoot', (Router, Events) => {
//Capture a new request instance and run it through the middleware pipeline.
Router.beforeEach((to, from, next) => {
this.app.run(this.app.make('Request').capture(to, from, next))
})
Root.router = Router
return new Vue(Root)
})
export default class Authenticate {
/**
* @param app {Application}
* @param next
* @return void
*/
constructor(App){
this.app = App
}
/**
* Handle Middleware
* @param request {Request}
* @param nextPipe
* @return void
*/
handle(request, next) {
//Get parameters from the route request.
const to = request.get('to')
const from = request.get('from')
const routerNext = request.get('next')
//Call the next middleware pipe.
next(request)
}
}
import Pipeline from "laravel-micro.js"
export default class Kernel{
/**
* App Kernel Constructor
* @return void
*/
constructor(App) {
this.app = App
this._middleware = []
this._pipeline = new Pipeline(App)
}
/**
* Set Middleware Stack
* @param middleware {Array}
* @return void
*/
setMiddleware(middleware){
this._middleware = middleware
}
/**
* Register Middleware Stack
* @param request {*}
* @param then {function}
* @return {*}
*/
handle(request, then = (response) => response){
try{
return this._pipeline
.send(request)
.through(this._middleware)
.via('handle')
.then(then)
}catch (e) {
this.app.handleError(e)
}
}
}
Service Providers declare their provided bindings in the "provides" method.
Providers which set this.deferred=true
will not have their boot method called until the first binding is resolved.
/**
* All Providers are deferred by default. The "boot" method will not
* be called until the service is resolved the first time.
* To force the provider to boot when the bootProviders method is
* called, change "deferred" to "false" in the constructor.
*/
import {ServiceProvider} from "laravel-micro.js"
export default class AppServiceProvider extends ServiceProvider {
/**
* Provider Constructor.
* @param app {Application}
* @return void
*/
constructor(app) {
super(app)
this.deferred = true //true by default
}
/**
* Register any application services.
* @return void
*/
register() {
//shared instance once resolved by default
this.app.bind('MyService', MyImplementation)
}
/**
* Boot any application services.
* @return void
*/
boot() {
const instance = this.app.make('MyService')
instance.serviceMe()
}
/**
* Declare the aliases for the provided services.
* Used to boot the provider if the service is deferred.
* @return {Array}
*/
get provides() {
return ['MyService']
}
}
Register Service Providers with the container.
const app = new App
app.register(AppServiceProvider)
app.register(AuthServiceProvider)
app.register(VueServiceProvider)
app.register(HttpServiceProvider)
app.register(StoreServiceProvider)
// etc...
// or short and sweet...
import Providers from "./ServiceProviders"
Providers.forEach((provider) => app.register(provider))
/**
* Once all the providers are registered, call the "bootProviders" method:
*/
app.bootProviders()
Boot the Service Providers.
app.bootProviders()
app.isRegistered(string ProviderName)
Determines if a Service Provider is registered:
if(!app.isRegistered('NotificationServiceProvider')){
app.register(AlertServiceProvider)
}
The container is the heart of your application and has many useful methods for interacting with services and bindings.
Add an binding for an abstract alias that returns a concrete instance.
app.bind(alias {String}, abstract {*}, isSharable {boolean})
Bind a Class Constructor with Auto-Dependency Injection:
app.bind('Config', ConfigClass)
Bind a Object:
app.bind('Config', {
debug: false,
})
Bind a Class that provides a service and a callback that Injects the Dependency and provides a value:
/**
* The container supports binding common types of objects.
* Here's a few examples of what you can do:
*/
const container = new Container
container.bind('object', {prop: true})
container.bind('array', ['test'])
container.bind('boolean', true)
container.bind('number', 100)
container.bind('MyClass', MyClass)
container.bind('randomPick',()=>{
return 'yes!'
})
/**
* Bindings are shared instances by default.
* Specify "false" to force a binding to be unsharable.
* This forces classes and callbacks to be constructed fresh every time they are resolved.
*/
container.bind('MyCallback',()=>{
return Math.random()
}, false)
/** Specify the aliases of needed dependencies in constructors. */
class ClassA{
constructor(classB, classC){
this.classB = classB
this.classC = classC
}
}
class ClassB{
constructor(classC){
this.classC = classC
}
}
class ClassC{
constructor(App){
this.app = App
}
}
/** Then, import and bind un-instantiated. */
import {ClassA, ClassB} from './MyServices'
const container = new Container
container.bind('classA',ClassA)
container.bind('classB',ClassB)
/** Functions can also specify Dependencies. */
container.bind('myCallback',(classA, classB, classC) => {
return 'You Bet!' //Do something with the injections
})
app.make(abstract)
Build or Resolve an instance from the container.
const classA = container.make('classA')
const classB = container.make('classB')
const classC = container.make('classC')
const randomPick = container.make('myCallback')
app.destroy(abstract)
app.destroy('MyService')
Container: UnBinding: "MyService"...
Container: UnSharing "MyService"...
Container: Destroying shared instance of "MyService"...
Container: Cleaning up resolved references of "MyService"...
app.isBound(abstract)
Determines if a binding is available. Useful for binding temporary services or required services.
if(!app.isBound('App')) {
app.bind('App', () => App, true) //allow sharing
}
app.isResolved(alias)
Determines if a service abstract has an shared concrete instance that's already resolved.
if(app.isResolved('DatabaseService')){
app.destroy('DatabaseService')
}
app.rebound(alias)
Destroy and re-build a service abstract that has a shared concrete instance that's already resolved.
app.rebound(alias)
app.setInstance(alias, concrete)
Set an concrete instance of a binding.
app.setInstance('AuthToken', { token: XXX})
app.getInstance(alias)
Get a concrete instance of a binding.
const token = app.getInstance('AuthToken')
The container's sharing API allows you to share references to bindings as functions. You can share many references with many objects and revoke access to all instances of specific references at any time.
app.bind('TempService', () => {
console.log('ok')
return 'ok'
})
app.bind('tempInstance', () => {
const tempInstance = app.make('TempService')
setTimeout(() => app.unBind('tempInstance'), 10 * 1000)
console.log('You have 10 seconds to use me!')
return tempInstance
}, true)
app.share('tempInstance').withOthers(window)
window.tempInstance()
wait 10 seconds...
window.tempInstance()
Uncaught ReferenceError: tempInstance is not defined
app.make('tempInstance')
Container: No Binding found for "tempInstance".
let MortalA, MortalB = {}
app.bind('Potions', ['brewDragon', 'brewLizard', 'brewOger'])
app.bind('doMagic', (Potions) => (new Wizard(Potions)).brew())
app.bind('playMusic', () => (new MusicBox).play())
app.bind('toMereMortal', () => {
app.unShare('playMusic')
app.unShare('doMagic')
app.unShare('toMereMortal')
})
app.share('doMagic', 'playMusic', 'toMereMortal').with(MortalA, MortalB)
MortalA.doMagic()
MortalB.playMusic()
MortalA.doMagic()
MortalB.playMusic()
MortalA.toMereMortal()
MortalB.toMereMortal()
MortalA.doMagic() undefined
MortalA.playMusic() undefined
MortalA.toMereMortal() undefined
MortalB.doMagic() undefined
MortalB.playMusic() undefined
MortalB.toMereMortal() undefined
Debugging methods allow you to list the current state of the application container by logging the results to the console for table display.
app.providers
app.bindings
app.resolved
app.sharable
app.sharedWith
app.getName(Thing)
Returns a string of the Object "name" property or "constructor name" (class name).
To make code as reusable as possible, a mixin is provided that allows you to create class traits.
Define a Class Trait:
import Mixin from "../Utilities/Mixin"
export default (instance) => Mixin(instance, {
/**
* Log if debugging.
* @return void
*/
_log(){
if(this._debug){
console.log.apply(this, arguments)
}
}
})
Trait Usage:
import CanDebug from "../Traits/CanDebug"
export default class MyClass{
construct(){
this._debug = true
}
doSomething(){
this._debug(arguments)
}
}
/** MyClass Traits **/
CanDebug(MyClass)
Setting an error handler on the container allows you to catch all errors that bubble up from conatiner bindings.
You can set the container error handler to a class or callback, (disabled by default).
const App = new Application
app.errorHandler(MyErrorHandlerClass)
or use a callback:
app.errorHandler((Error) => {
console.error(Error)
})
The included error handler class is designed to interact with 'Exceptions" which can have a "handle" method. When the error is thrown it will get caught by the error handler and if there's a handle method that's callable it will call it can return the provided value.
export default class Handler{
/**
* Exception Handler Class
* @param App {Application}
*/
constructor(App) {
this.app = App
}
/**
* Handle Method
* @param Error {Error|Exception}
*/
handle(Error){
if(typeof Error.handle === 'function'){
try{
return Error.handle(this.app)
}catch (e) {
console.error(`Failed to handle "${Error.name}"...`, Error)
}
}
console.info(`"${Error.name}" Encountered...`, Error)
}
}
If an exception is encountered that has a "handle" method, the method will be called and the exception can react to itself and provide a new instance from the container which will be resolved for the called binding.
In the example below:
- An exception is thrown as the container attempts to make "badBinding"
- The exception is handled and it's handle method is called.
- The handle method dictates the app should return an instance of "backupObject" in it's place.
- Every subsequnt call to "app.make('badBinding')" will return the shared instance of "backupObject".
app.bind('backupObject', () => {
return { yourGood: true }
})
class Handler {
handle(Error){
return Error.handle ? Error.handle(this.app) : null
}
}
class MyException extends Exception{
constructor(...args) {
super(...args)
this.handle = (App) => {
console.log('MyException Encountered, Self-Handling by Providing "backupObject"')
return app.make('backupObject')
}
}
}
app.errorHandler(Handler)
app.bind('badBinding', () => {
throw new MyException('HAHA Immediate Fail.')
})
const result = app.make('badBinding')
Container: Sharing Instance of "App".
Container Binding: "backupObject"...
Container Binding: "badBinding"...
Container: Making "badBinding"...
Container: Resolving Binding for "badBinding"...
MyException Encountered, Self-Handling by Providing "backupObject"
Container: Making "backupObject"...
Container: Resolving Binding for "backupObject"...
Container: Instantaiated Concrete Instance successfully. {yourGood: true}
Container: "backupObject" is Sharable.
Container: Sharing Instance of "backupObject".
Container: "badBinding" is Sharable.
Container: Sharing Instance of "badBinding".
Result: {yourGood: true}
The framework includes examples for using custom errors referred to as "Exceptions". Exceptions can extend the browsers built-in Error interface. In the example included Exceptions can have a "handle" method that is passed the application instance to its "handle" method. This allows Error to handle themselves by reacting to the application state.
export default class Exception extends Error{
/**
* Generic Exception Class
* @param args {*}
*/
constructor(...args) {
super(...args)
/**
* Arguments passed to the exception.
* @property args {Array}
*/
this.args = args
/**
* The name of the Custom Exception.
* @property name {String}
*/
this.name = 'Exception'
/**
* Handle from the exception when it's thrown.
* @param App {Application}
* @return {*}
*/
this.handle = (App) => {
//react to the exception when it's thrown.
}
}
}