You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
lib/src/di/Container.ts

301 lines
10 KiB

import {DependencyKey, InstanceRef, Instantiable, isInstantiable} from './types'
import {AbstractFactory} from './factory/AbstractFactory'
import {collect, Collection, globalRegistry, logIfDebugging} from '../util'
import {Factory} from './factory/Factory'
import {DuplicateFactoryKeyError} from './error/DuplicateFactoryKeyError'
import {ClosureFactory} from './factory/ClosureFactory'
import NamedFactory from './factory/NamedFactory'
import SingletonFactory from './factory/SingletonFactory'
import {InvalidDependencyKeyError} from './error/InvalidDependencyKeyError'
export type MaybeFactory<T> = AbstractFactory<T> | undefined
export type MaybeDependency = any | undefined
export type ResolvedDependency = { paramIndex: number, key: DependencyKey, resolved: any }
/**
* A container of resolve-able dependencies that are created via inversion-of-control.
*/
export class Container {
/**
* Get the global instance of this container.
*/
public static getContainer(): Container {
const existing = <Container | undefined> globalRegistry.getGlobal('extollo/injector')
if ( !existing ) {
const container = new Container()
globalRegistry.setGlobal('extollo/injector', container)
return container
}
return existing
}
/**
* Collection of factories registered with this container.
* @type Collection<AbstractFactory>
*/
protected factories: Collection<AbstractFactory<unknown>> = new Collection<AbstractFactory<unknown>>()
/**
* Collection of singleton instances produced by this container.
* @type Collection<InstanceRef>
*/
protected instances: Collection<InstanceRef> = new Collection<InstanceRef>()
constructor() {
this.registerSingletonInstance<Container>(Container, this)
this.registerSingleton('injector', this)
}
/**
* Register a basic instantiable class as a standard Factory with this container.
* @param {Instantiable} dependency
*/
register(dependency: Instantiable<any>): this {
if ( this.resolve(dependency) ) {
throw new DuplicateFactoryKeyError(dependency)
}
const factory = new Factory(dependency)
this.factories.push(factory)
return this
}
/**
* Register the given function as a factory within the container.
* @param {string} name - unique name to identify the factory in the container
* @param {function} producer - factory to produce a value
*/
registerProducer(name: DependencyKey, producer: () => any): this {
if ( this.resolve(name) ) {
throw new DuplicateFactoryKeyError(name)
}
const factory = new ClosureFactory(name, producer)
this.factories.push(factory)
return this
}
/**
* Register a basic instantiable class as a standard Factory with this container,
* identified by a string name rather than static class.
* @param {string} name - unique name to identify the factory in the container
* @param {Instantiable} dependency
*/
registerNamed(name: string, dependency: Instantiable<any>): this {
if ( this.resolve(name) ) {
throw new DuplicateFactoryKeyError(name)
}
const factory = new NamedFactory(name, dependency)
this.factories.push(factory)
return this
}
/**
* Register a value as a singleton in the container. It will not be instantiated, but
* can be injected by its unique name.
* @param {string} key - unique name to identify the singleton in the container
* @param value
*/
registerSingleton<T>(key: DependencyKey, value: T): this {
if ( this.resolve(key) ) {
throw new DuplicateFactoryKeyError(key)
}
this.factories.push(new SingletonFactory(key, value))
return this
}
/**
* Register a static class to the container along with its already-instantiated
* instance that will be used to resolve the class.
* @param staticClass
* @param instance
*/
registerSingletonInstance<T>(staticClass: Instantiable<T>, instance: T): this {
if ( this.resolve(staticClass) ) {
throw new DuplicateFactoryKeyError(staticClass)
}
this.register(staticClass)
this.instances.push({
key: staticClass,
value: instance,
})
return this
}
/**
* Register a given factory with the container.
* @param {AbstractFactory} factory
*/
registerFactory(factory: AbstractFactory<unknown>): this {
if ( !this.factories.includes(factory) ) {
this.factories.push(factory)
}
return this
}
/**
* Returns true if the container has an already-produced value for the given key.
* @param {DependencyKey} key
*/
hasInstance(key: DependencyKey): boolean {
return this.instances.where('key', '=', key).isNotEmpty()
}
/**
* Returns true if the container has a factory for the given key.
* @param {DependencyKey} key
*/
hasKey(key: DependencyKey): boolean {
return Boolean(this.resolve(key))
}
/**
* Get the already-produced value for the given key, if one exists.
* @param {DependencyKey} key
*/
getExistingInstance(key: DependencyKey): MaybeDependency {
const instances = this.instances.where('key', '=', key)
if ( instances.isNotEmpty() ) {
return instances.first()
}
}
/**
* Find the factory for the given key, if one is registered with this container.
* @param {DependencyKey} key
*/
resolve(key: DependencyKey): MaybeFactory<unknown> {
const factory = this.factories.firstWhere(item => item.match(key))
if ( factory ) {
return factory
} else {
logIfDebugging('extollo.di.injector', 'unable to resolve factory', factory, this.factories)
}
}
/**
* Resolve the dependency key. If a singleton value for that key already exists in this container,
* return that value. Otherwise, use the factory an given parameters to produce and return the value.
* @param {DependencyKey} key
* @param {...any} parameters
*/
resolveAndCreate(key: DependencyKey, ...parameters: any[]): any {
logIfDebugging('extollo.di.injector', 'resolveAndCreate', key, {parameters})
// If we've already instantiated this, just return that
const instance = this.getExistingInstance(key)
logIfDebugging('extollo.di.injector', 'resolveAndCreate existing instance?', instance)
if ( typeof instance !== 'undefined' ) {
return instance.value
}
// Otherwise, attempt to create it
const factory = this.resolve(key)
logIfDebugging('extollo.di.injector', 'resolveAndCreate factory', factory)
if ( !factory ) {
throw new InvalidDependencyKeyError(key)
}
// Produce and store a new instance
const newInstance = this.produceFactory(factory, parameters)
this.instances.push({
key,
value: newInstance,
})
return newInstance
}
/**
* Given a factory and manually-provided parameters, resolve the dependencies for the
* factory and produce its value.
* @param {AbstractFactory} factory
* @param {array} parameters
*/
protected produceFactory<T>(factory: AbstractFactory<T>, parameters: any[]): T {
// Create the dependencies for the factory
const keys = factory.getDependencyKeys().filter(req => this.hasKey(req.key))
const dependencies = keys.map<ResolvedDependency>(req => {
return {
paramIndex: req.paramIndex,
key: req.key,
resolved: this.resolveAndCreate(req.key),
}
}).sortBy('paramIndex')
// Build the arguments for the factory, using dependencies in the
// correct paramIndex positions, or parameters of we don't have
// the dependency.
const constructorArguments = []
const params = collect(parameters).reverse()
for ( let i = 0; i <= dependencies.max('paramIndex'); i++ ) {
const dep = dependencies.firstWhere('paramIndex', '=', i)
if ( dep ) {
constructorArguments.push(dep.resolved)
} else {
constructorArguments.push(params.pop())
}
}
// Produce a new instance
const inst = factory.produce(constructorArguments, params.reverse().all())
factory.getInjectedProperties().each(dependency => {
if ( dependency.key && inst ) {
(inst as any)[dependency.property] = this.resolveAndCreate(dependency.key)
}
})
return inst
}
/**
* Create an instance of the given target. The target can either be a DependencyKey registered with
* this container (in which case, the singleton value will be returned), or an instantiable class.
*
* If the instantiable class has the Injectable decorator, its injectable parameters will be automatically
* injected into the instance.
* @param {DependencyKey} target
* @param {...any} parameters
*/
make<T>(target: DependencyKey, ...parameters: any[]): T {
if ( this.hasKey(target) ) {
return this.resolveAndCreate(target, ...parameters)
} else if ( typeof target !== 'string' && isInstantiable(target) ) {
return this.produceFactory(new Factory(target), parameters)
} else {
throw new TypeError(`Invalid or unknown make target: ${target}`)
}
}
/**
* Get a collection of dependency keys required by the given target, if it is registered with this container.
* @param {DependencyKey} target
*/
getDependencies(target: DependencyKey): Collection<DependencyKey> {
const factory = this.resolve(target)
if ( !factory ) {
throw new InvalidDependencyKeyError(target)
}
return factory.getDependencyKeys().pluck('key')
}
/**
* Given a different container, copy the factories and instances from this container over to it.
* @param container
*/
cloneTo(container: Container): this {
container.factories = this.factories.clone()
container.instances = this.instances.clone()
return this
}
}