Skip to content
Dan Alvidrez edited this page Mar 2, 2019 · 31 revisions

LaravelMicro.js

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.

Application extends LaravelMicro (Container)

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()

Middleware Pipelines

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

Global Application Middleware

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.

Appplication Class Example


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()
		})
	}
}

Vue Router Example

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)
})

Middleware Pipe Class

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)
    }
}

Middleware Pipeline (App Kernel)

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

app.register(MyServiceProvider)

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)
}

Application extends Container

The container is the heart of your application and has many useful methods for interacting with services and bindings.


Binding Alias References: From Abstract to Concrete

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)

Dependency Injection


/** 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')

Sharing References to Concrete Instances as Callable Methods

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

Computed Container Properties

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).


Class Traits (Mixins)

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)

Service Providers

Error Handling

Setting an error handler on the container allows you to catch all errors that bubble up from conatiner bindings.

app.errorHandler(callback or ClassObject)

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)
})

Error Handler

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)
    }
}

Self-Handling Exceptions

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:

  1. An exception is thrown as the container attempts to make "badBinding"
  2. The exception is handled and it's handle method is called.
  3. The handle method dictates the app should return an instance of "backupObject" in it's place.
  4. 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}

Exception extends Error

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.
        }
    }
}

Clone this wiki locally