lucontainer
v2.0.0
Published
Service Container for Node js
Downloads
4
Maintainers
Readme
lucontainer
Lucontainer offers an (almost) identical api to Laravel Container.
- Installation
- Introduction
- Zero Configuration Resolution
- Binding
- Resolving
- Method Invocation & Injection
- Container Events
- Decorators
Installation
npm install lucontainer reflect-metadata
The Lucontainer type definitions are included in the lucontainer npm package.
Warning If You Use Typescript Lucontainer requires TypeScript >= 4.4 and the experimentalDecorators, emitDecoratorMetadata options in your tsconfig.json file.
{
"compilerOptions": {
"types": ["reflect-metadata"],
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
Lucontainer requires a modern JavaScript engine with support for:
Warning The
reflect-metadata
polyfill should be imported only once in your entire application because the Reflect object is meant to be a global singleton.
Introduction
The Lucontainer service container is a powerful tool for managing class dependencies and performing dependency injection. Dependency injection is a fancy phrase that essentially means this: class dependencies are "injected" into the class via the constructor or, in some cases, "setter" methods.
Let's look at a simple example:
import UserRepository from 'user-repository';
import { constructable } from 'lucontainer';
@constructable()
class UserController {
constructor(protected users: UserRepository) {}
public show(id: number): User {
return this.users.find(id);
}
}
In this example, the UserController
needs to retrieve users from a data source. So, we will inject a service that is able to retrieve users. In this context, our UserRepository
to retrieve user information from the database. However, since the repository is injected, we are able to easily swap it out with another implementation. We are also able to easily "mock", or create a dummy implementation of the UserRepository
when testing our application.
A deep understanding of the Lucontainer service container is essential to building a powerful, large application, as well as for contributing to the Lucontainer core itself.
Zero Configuration Resolution
If a class has no dependencies or only depends on other concrete classes (not interfaces), the container does not need to be instructed on how to resolve that class. For example:
import { Container, constructable } from './src';
@constructable()
class Service {
//
}
@constructable()
class UsingService {
constructor(protected service: Service) {}
}
const container = new Container();
console.log(container.get(UsingService));
Binding
Binding Basics
Simple Bindings
We can register a binding using the bind
method, passing the class or a name (you can't bind Typescript Interfaces) that we wish to register along with a closure that returns an instance of the class:
import Transistor from './services/transistor';
import PodcastParser from './services/podcast-parser';
container.bind(Transistor, function ({container}) {
return new Transistor(container.make(PodcastParser));
});
Note that we receive the container itself as an argument to the resolver. We can then use the container to resolve sub-dependencies of the object we are building.
Note There is no need to bind classes into the container if they do not depend on any interfaces. The container does not need to be instructed on how to build these objects, since it can automatically resolve these objects using reflection.
Binding A Singleton
The singleton
method binds a class or a name (you can't bind Typescript Interfaces) into the container that should only be resolved one time. Once a singleton binding is resolved, the same object instance will be returned on subsequent calls into the container:
import Transistor from './services/transistor';
import PodcastParser from './services/podcast-parser';
container.singleton(Transistor, function ({container}) {
return new Transistor(container.make(PodcastParser));
});
Binding Scoped Singletons
The scoped
method binds a class or a name (you can't bind Typescript Interfaces) into the container that should only be resolved one time within a given request / job lifecycle. While this method is similar to the singleton method, instances registered using the scoped method should be safely flushed whenever the application starts a new "lifecycle", such as your application receive a new request
or a worker processes a new job
:
import Transistor from './services/transistor';
import PodcastParser from './services/podcast-parser';
container.scoped(Transistor, function ({container}) {
return new Transistor(container.make(PodcastParser));
});
Binding Instances
You may also bind an existing object instance into the container using the instance
method. The given instance will always be returned on subsequent calls into the container:
import Transistor from './services/transistor';
import PodcastParser from './services/podcast-parser';
container.instance(Transistor, function ({container}) {
return new Transistor(container.make(PodcastParser));
});
Binding Names To Implementations
Note Interfaces Workaround
A very powerful feature of the service container is its ability to bind a name (you can't bind Typescript Interfaces) to a given implementation. For example, let's assume we have an EventPusher
interface and a RedisEventPusher
implementation. Once we have coded our RedisEventPusher
implementation of this interface, we can register it with the service container like so:
import { constructable } from './src';
interface EventPusher {}
@constructable()
class RedisEventPusher implements EventPusher {}
container.bind('EventPusher', RedisEventPusher);
container.bind(Symbol.for('EventPusher'), RedisEventPusher);
This statement tells the container that it should inject the RedisEventPusher
when a class needs an implementation of EventPusher
. Now we can type-hint the EventPusher
interface in the constructor of a class that is resolved by the container:
import { constructable, inject } from './src';
interface EventPusher {}
@constructable()
class Test {
constructor(@inject(Symbol.for('EventPusher')) public pusher: EventPusher) {}
}
@constructable()
class Test2 {
constructor(@inject('EventPusher') public pusher: EventPusher) {}
}
Note Named Binding cannot be Automatically Injected, you need to add @inject parameter decorator, in this way type of parameter will respect the original interface and the container can automatically inject the registered implementations.
Contextual Binding
Sometimes you may have two classes that utilize the same interface, but you wish to inject different implementations into each class. For example, two controllers may depend on different implementations of the Filesystem
contract:
import PhotoController from './service/photo-controller';
import UploadController from './service/upload-controller';
import VideoController from './service/video-controller';
container
.when(PhotoController)
.needs('FileSystem')
.give(function () {
return 'local';
});
container
.when([VideoController, UploadController])
.needs('FileSystem')
.give(function () {
return 'cloud';
});
Binding Primitives
Sometimes you may have a class that receives some injected classes, but also needs an injected primitive value such as an integer. You may easily use contextual binding to inject any value your class may need:
import UserController from './service/user-controller';
container.when(UserController).needs('parameterName').give(10);
Sometimes a class may depend on an array of tagged instances. Using the giveTagged
method, you may easily inject all of the container bindings with that tag:
import ReportAggregator from './service/report-aggregator';
container.when(ReportAggregator).needs('reports').giveTagged('reports');
Binding Typed Variadics
Occasionally, you may have a class that receives an array of typed objects using a variadic constructor argument:
import Filter from './models/filter';
import Logger from './services/logger';
@constructable()
class Firewall {
constructor(protected logger: Logger, protected ...filters: Filter[]) {}
}
Using contextual binding, you may resolve this dependency by providing the give
method with a closure that returns an array of resolved Filter
instances:
container
.when(Firewall)
.needs(Filter)
.give(function ({container}) {
return [container.make(NullFilter), container.make(ProfanityFilter), container.make(TooLongFilter)];
});
For convenience, you may also just provide an array of class names to be resolved by the container whenever Firewall
needs Filter
instances:
container.when(Firewall).needs(Filter).give([NullFilter, ProfanityFilter, TooLongFilter]);
Variadic Tag Dependencies
Sometimes a class may have a variadic dependency that is type-hinted as a given class (...reports: Report[])
. Using the needs
and giveTagged
methods, you may easily inject all of the container bindings with that tag for the given dependency:
container.when(ReportAggregator).needs(Report).giveTagged('reports');
Tagging
Occasionally, you may need to resolve all of a certain "category" of binding. For example, perhaps you are building a report analyzer that receives an array of many different Report
interface implementations. After registering the Report
implementations, you can assign them a tag using the tag
method:
container.bind(CpuReport, function () {
//
});
container.bind(MemoryReport, function () {
//
});
container.tag([CpuReport, MemoryReport], 'reports');
Once the services have been tagged, you may easily resolve them all via the container's tagged
method:
import ReportAnalyzer from './services/report-analyzer';
container.bind(ReportAnalyzer, function ({container}) {
return new ReportAnalyzer(container.tagged('reports'));
});
Extending Bindings
The extend
method allows the modification of resolved services. For example, when a service is resolved, you may run additional code to decorate or configure the service. The extend
method accepts two arguments, the service class you're extending and a closure that should return the modified service. The closure receives the service being resolved and the container instance:
container.extend(Service, function ({instance}) {
return new DecoratedService(instance);
});
Resolving
The make Method
You may use the make
method to resolve a class instance from the container. The make
method accepts a class or a name (you can't bind Typescript Interfaces) you wish to resolve:
import Transistor from './services/transistor';
const transistor = container.make(Transistor);
If some of your class' dependencies are not resolvable via the container, you may inject them by passing them as a key-value object into the makeWith
method. For example, we may manually pass the id
constructor argument required by the Transistor
service:
import Transistor from './services/transistor';
const transistor = container.makeWith(Transistor, { id: 1 });
If you would like to have the Lucontainer instance itself injected into a class that is being resolved by the container, you can register an instance and type-hint the Container
class on your class' constructor:
import Container from 'lucontainer';
const container = new Container();
container.instance(Container, container);
@constructable()
class Test {
constructor(protected container: Container) {}
}
Method Invocation & Injection
Sometimes you may wish to invoke a method on an object instance while allowing the container to automatically inject that method's dependencies. For example, given the following class:
import { constructable, methodable } from 'lucontainer';
import UserRepository from 'user-repository';
@constructable()
class UserReport {
@methodable()
public generate(repository: UserRepository) {
// ...
}
}
You may invoke the generate
method via the container like so:
import UserReport from 'user-report';
const report = container.call([new UserReport(), 'generate']);
The call
method accepts an array of [class | instance, method, static (true|false)].
The container's call method may even be used to invoke an annotated closure while automatically injecting its dependencies:
import { annotate } from 'lucontainer';
import UserRepository from 'user-repository';
function toCall(repository: UserRepository) {}
annotate(toCall, [], [UserRepository]);
const result = container.call(toCall);
Container Events
The service container fires an event each time it resolves an object. You may listen to this event using the resolving
, beforeResolving
,AfterResolving
method:
import Transistor from './services/transistor';
container.beforeResolving(
Transistor,
function ({instance, parameters, container}) {
// Called before container resolves objects of type "Transistor"...
}
);
container.resolving(Transistor, function ({instance, container}) {
// Called when container resolves objects of type "Transistor"...
});
container.afterResolving(Transistor, function ({instance, container}) {
// Called after container resolves objects of type "Transistor"...
});
container.beforeResolving(function ({instance, parameters, container}) {
// Called before container resolves object of any type...
});
container.resolving(function ({instance, container}) {
// Called when container resolves object of any type...
});
container.afterResolving(function ({instance, container}) {
// Called after container resolves object of any type...
});
As you can see, the object being resolved will be passed to the callback, allowing you to set any additional properties on the object before it is given to its consum
Decorators
Every Class or Function Resolved By Lucontainer need to be decorated
Typescript
Constructable
Decorate Class with Constructable
in order to be resolved by Lucontainer
import { constructable } from 'lucontainer';
@constructable()
class Test {}
Because Typescript Interfaces are not compiled to javascript, you can pass a list of Named implementation, in order to raise implementations events
import { Container, constructable } from 'lucontainer';
interface ResolvingContractStub {
//
}
@constructable(Symbol.for('ResolvingContractStub'))
class ResolvingImplementationStub implements ResolvingContractStub {
//
}
@constructable()
class ResolvingImplementationStubTwo implements ResolvingContractStub {
//
}
const container = new Container();
let callCounter = 0;
container.resolving(Symbol.for('ResolvingContractStub'), () => {
callCounter++;
});
container.bind(Symbol.for('ResolvingContractStub'), ResolvingImplementationStub);
container.make(ResolvingImplementationStub);
console.log(callCounter); // 1
container.bind(Symbol.for('ResolvingContractStub'), ResolvingImplementationStubTwo);
container.make(ResolvingImplementationStubTwo);
console.log(callCounter); // 1
Methodable
Decorate Class methods with Methodable
in order to be called by Lucontainer
import { constructable, methodable } from 'lucontainer';
@constructable()
class Test {
@methodable()
public inject(bar: number = 1) {}
@methodable()
public static inject(bar: number = 1) {}
}
Inject
Decorate Class Method Parameters in order to resolve Named Parameters within Lucontainer.
import { Container, constructable, methodable, inject } from 'lucontainer';
interface IContainerContractStub {
//
}
@constructable()
class ContainerImplementationStub implements IContainerContractStub {
//
}
@constructable()
class ContainerImplementationStubTwo implements IContainerContractStub {
//
}
@constructable()
class ContainerDependentStub {
public constructor(@inject('IContainerContractStub') public impl: IContainerContractStub) {}
@methodable()
public setImplementation(@inject('IContainerContractStub') impl: IContainerContractStub) {
this.impl = impl;
}
}
const container = new Container();
container.bind('IContainerContractStub', ContainerImplementationStub);
const obj = container.make(ContainerDependentStub);
console.log(obj.impl instanceof ContainerImplementationStub); // true
container.bind('IContainerContractStub', ContainerImplementationStubTwo);
container.call([obj, 'setImplementation']);
console.log(obj.impl instanceof ContainerImplementationStubTwo); // true
Javascript
Annotate
Decorate Class And Function with annotate
Function in order to be resolved by Lucontainer.
Note @decorator in Typescript can not be used to decorate function, you must use annotate.
Class or Function Constructor:
import { Container, annotate } from 'lucontainer';
// const {Container, annotate} = require('lucontainer');
class ContainerImplementationStub {
//
}
annotate(ContainerImplementationStub, [], []);
class ContainerImplementationStubTwo {
//
}
annotate(ContainerImplementationStubTwo, [], []);
function ContainerDependentStub(impl, number = 10) {
this.impl = impl;
this.number = 10;
}
ContainerDependentStub.prototype.setImplementation = function (impl) {
this.impl = impl;
};
// annotate constructor (Fn or Class, array of implementations, array of parameters Types)
annotate(ContainerDependentStub, [Symbol.for('ResolvingContractStub')], [Symbol.for('IContainerContractStub'), Number]);
// annotate method (Fn or Class or prototype, method name, array of parameters Types)
annotate(ContainerDependentStub.prototype, 'setImplementation', [Symbol.for('IContainerContractStub')]);
const container = new Container();
let callCounter = 0;
container.bind(Symbol.for('ResolvingContractStub'), ContainerDependentStub);
container.resolving(Symbol.for('ResolvingContractStub'), () => {
callCounter++;
});
container.bind(Symbol.for('IContainerContractStub'), ContainerImplementationStub);
const obj = container.make(ContainerDependentStub);
console.log(obj.impl instanceof ContainerImplementationStub); // true
container.bind(Symbol.for('IContainerContractStub'), ContainerImplementationStubTwo);
container.call([obj, 'setImplementation']);
console.log(obj.impl instanceof ContainerImplementationStubTwo); // true
console.log(callCounter); // 1
container.make(ContainerDependentStub);
console.log(callCounter); // 2