@aedart/support
Version:
The Ion support package
327 lines (321 loc) • 7.71 kB
JavaScript
import { CONTAINER } from '@aedart/contracts/container';
import { isset } from '@aedart/support/misc';
import { AbstractClassError, LogicalError } from '@aedart/support/exceptions';
/**
* @aedart/support
*
* BSD-3-Clause, Copyright (c) 2023-present Alin Eugen Deac <aedart@gmail.com>.
*/
/**
* Facade
*
* Adaptation of Laravel's Facade abstraction.
*
* @see https://github.com/laravel/framework/blob/master/src/Illuminate/Support/Facades/Facade.php
*
* @abstract
*/
class Facade {
/**
* The facade's service container instance
*
* @type {Container|undefined}
*
* @protected
* @static
*/
static container = undefined;
/**
* The "type" of the resolved object instance.
*
* **Note**: _This property is not used for anything other
* than to provide a TypeScript return type for the `obtain()`
* method._
*
* @protected
* @static
*/
static type;
/**
* Resolved instances
*
* @type {Map<Identifier, any>}
*
* @protected
* @static
*/
static resolved = new Map();
/**
* Registered object spies
*
* @type {Set<Identifier>}
*
* @protected
* @static
*/
static spies = new Set();
/**
* Facade Constructor
*
* @protected
*/
constructor() {
/* @ts-expect-error TS2345 Facade constructor is abstract */
throw new AbstractClassError(this.constructor);
}
/**
* Returns identifier to be used for resolving facade's underlying object instance
*
* @return {Identifier}
*
* @abstract
*
* @static
*/
static getIdentifier() {
throw new LogicalError('Facade does not implement the getIdentifier() method');
}
/**
* Obtain the underlying object instance, or a "spy" (for testing)
*
* @see spy
*
* @return {any}
*
* @throws {NotFoundException}
* @throws {ContainerException}
* @throws {LogicalError}
*
* @abstract
*
* @static
*/
static obtain() {
return this.resolve(this.getIdentifier());
}
/**
* Register a "spy" (e.g. object mock) for this facade's identifier
*
* @template T = any
*
* @param {SpyFactoryCallback} callback Callback to be used for creating some kind of object spy
* or mock, with appropriate configuration and expectations for testing
* purposes.
*
* @return {T} Object instance (spy / object mock) to be registered in service container.
*
* @static
*/
static spy(callback) {
const identifier = this.getIdentifier();
const spy = callback(this.getContainer(), identifier);
this.swap(spy);
this.spies.add(identifier);
return spy;
}
/**
* Determine if a spy has been registered for this facade's identifier
*
* @return {boolean}
*
* @static
*/
static isSpy() {
return this.spies.has(this.getIdentifier());
}
/**
* Removes registered spy for this facade's identifier
*
* **Warning**: _Method does NOT perform any actual "spy" cleanup logic.
* It only removes the reference to the "spy" object._
*
* @return {boolean}
*
* @static
*/
static forgetSpy() {
const identifier = this.getIdentifier();
return this.forgetResolved(identifier)
&& this.spies.delete(identifier);
}
/**
* Removes all registered spies
*
* @return {void}
*
* @static
*/
static forgetAllSpies() {
for (const identifier of this.spies) {
this.forgetResolved(identifier);
}
this.spies.clear();
}
/**
* Swap the facade's underlying instance
*
* @param {any} instance
*
* @return {void}
*
* @static
*/
static swap(instance /* eslint-disable-line @typescript-eslint/no-explicit-any */) {
const identifier = this.getIdentifier();
this.resolved.set(identifier, instance);
if (this.hasContainer()) {
this.getContainer().instance(identifier, instance);
}
}
/**
* Set this facade's service container instance
*
* @param {Container | undefined} container
*
* @return {this}
*
* @static
*/
static setContainer(container) {
this.container = container;
return this;
}
/**
* Get this facade's service container instance
*
* @return {Container | undefined}
*
* @static
*/
static getContainer() {
return this.container;
}
/**
* Determine if facade's has a service container instance set
*
* @return {boolean}
*
* @static
*/
static hasContainer() {
return isset(this.container);
}
/**
* Removes this facade's service container instance
*
* @return {this}
*
* @static
*/
static forgetContainer() {
this.container = undefined;
return this;
}
/**
* Determine if resolved facade instance exists
*
* @param {Identifier} identifier
*
* @reurn {boolean}
*
* @static
*/
static hasResolved(identifier) {
return this.resolved.has(identifier);
}
/**
* Forget resolved facade instance
*
* @param {Identifier} identifier
*
* @return {boolean}
*
* @static
*/
static forgetResolved(identifier) {
return this.resolved.delete(identifier);
}
/**
* Forget all resolved facade instances
*
* @return {void}
*
* @static
*/
static forgetAllResolved() {
this.resolved.clear();
}
/**
* Clears all resolved instances, service container and evt. spies.
*
* @return {void}
*/
static destroy() {
this.forgetAllSpies();
this.forgetAllResolved();
this.forgetContainer();
}
/**
* Resolves the facade's underlying object instance from the service container,
* which matches given identifier.
*
* **Note**: _If a "spy" has been registered for given identifier, then that spy
* object is returned instead._
*
* @template T = any
*
* @param {Identifier} identifier
*
* @return {T}
*
* @throws {NotFoundException}
* @throws {ContainerException}
* @throws {LogicalError}
*
* @protected
*
* @static
*/
static resolve(identifier) {
if (!this.hasContainer()) {
throw new LogicalError('Facade has no service container instance set');
}
if (this.hasResolved(identifier)) {
return this.resolved.get(identifier);
}
const resolved = this.getContainer().make(identifier);
this.resolved.set(identifier, resolved);
return resolved;
}
}
/**
* Container Facade
*/
class Container extends Facade {
/**
* The "type" of the resolved object instance.
*
* **Note**: _This property is not used for anything other
* than to provide a TypeScript return type for the `obtain()`
* method._
*
* @type {import('@aedart/contracts/container').Container}
*
* @protected
* @static
*/
static type;
/**
* Returns identifier to be used for resolving facade's underlying object instance
*
* @return {Identifier}
*
* @abstract
*
* @static
*/
static getIdentifier() {
return CONTAINER;
}
}
export { Container, Facade };