788 lines
26 KiB
Dart
788 lines
26 KiB
Dart
/*
|
|
* This file is part of the Protevus Platform.
|
|
*
|
|
* (C) Protevus <developers@protevus.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
import 'dart:async';
|
|
import 'exception.dart';
|
|
import 'reflector.dart';
|
|
import 'contextual_binding_builder.dart';
|
|
|
|
class Container {
|
|
/// The [Reflector] instance used by this container for reflection-based operations.
|
|
///
|
|
/// This reflector is used to instantiate objects and resolve dependencies
|
|
/// when no explicit factory or singleton is registered for a given type.
|
|
final Reflector reflector;
|
|
|
|
/// The container's bindings map
|
|
final Map<Type, dynamic> _singletons = {};
|
|
final Map<Type, dynamic Function(Container)> _factories = {};
|
|
final Map<String, dynamic> _namedSingletons = {};
|
|
|
|
/// The container's contextual bindings
|
|
final Map<Type, Map<Type, dynamic>> _contextual = {};
|
|
|
|
/// The container's method bindings
|
|
final Map<String, Function> _methodBindings = {};
|
|
|
|
/// The container's tags
|
|
final Map<String, List<Type>> _tags = {};
|
|
|
|
/// The container's scoped instances
|
|
final List<Type> _scopedInstances = [];
|
|
|
|
/// Resolution callbacks
|
|
final List<Function(Type, List, Container)> _beforeResolvingCallbacks = [];
|
|
final List<Function(dynamic, Container)> _resolvingCallbacks = [];
|
|
final List<Function(dynamic, Container)> _afterResolvingCallbacks = [];
|
|
|
|
/// The build stack for detecting circular dependencies
|
|
final List<Type> _buildStack = [];
|
|
|
|
/// The parent container
|
|
final Container? _parent;
|
|
|
|
/// Creates a new root [Container] instance with the given [Reflector].
|
|
///
|
|
/// This constructor initializes a new container without a parent, making it
|
|
/// a root container in the dependency injection hierarchy. The provided
|
|
/// [reflector] will be used for all reflection-based operations within this
|
|
/// container and its child containers.
|
|
///
|
|
/// Parameters:
|
|
/// - [reflector]: The [Reflector] instance to be used by this container
|
|
/// for reflection-based dependency resolution and object instantiation.
|
|
///
|
|
/// The [_parent] is set to null, indicating that this is a root container.
|
|
Container(this.reflector) : _parent = null;
|
|
|
|
/// Creates a child [Container] instance with the given parent container.
|
|
///
|
|
/// This constructor is used internally to create child containers in the
|
|
/// dependency injection hierarchy. It initializes a new container with a
|
|
/// reference to its parent container and uses the same [Reflector] instance
|
|
/// as the parent.
|
|
///
|
|
/// Parameters:
|
|
/// - [_parent]: The parent [Container] instance for this child container.
|
|
///
|
|
/// The [reflector] is initialized with the parent container's reflector,
|
|
/// ensuring consistency in reflection operations throughout the container
|
|
/// hierarchy.
|
|
Container._child(Container this._parent) : reflector = _parent.reflector;
|
|
|
|
/// Checks if this container is a root container.
|
|
///
|
|
/// Returns `true` if this container has no parent (i.e., it's a root container),
|
|
/// and `false` otherwise.
|
|
///
|
|
/// This property is useful for determining the position of a container in the
|
|
/// dependency injection hierarchy. Root containers are typically used as the
|
|
/// top-level containers in an application, while non-root containers are child
|
|
/// containers that may have more specific or localized dependencies.
|
|
bool get isRoot => _parent == null;
|
|
|
|
/// Creates a child [Container] that can define its own singletons and factories.
|
|
///
|
|
/// This method creates a new [Container] instance that is a child of the current container.
|
|
/// The child container inherits access to all dependencies registered in its parent containers,
|
|
/// but can also define its own singletons and factories that override or extend the parent's dependencies.
|
|
///
|
|
/// Child containers are useful for creating scoped dependency injection contexts, such as
|
|
/// for specific features, modules, or request-scoped dependencies in web applications.
|
|
///
|
|
/// The child container uses the same [Reflector] instance as its parent.
|
|
///
|
|
/// Returns:
|
|
/// A new [Container] instance that is a child of the current container.
|
|
///
|
|
/// Example:
|
|
/// ```dart
|
|
/// var parentContainer = Container(MyReflector());
|
|
/// var childContainer = parentContainer.createChild();
|
|
/// ```
|
|
Container createChild() {
|
|
return Container._child(this);
|
|
}
|
|
|
|
/// Determines if the container or any of its parent containers has an injection of the given type.
|
|
///
|
|
/// This method checks for both singleton and factory registrations of the specified type.
|
|
///
|
|
/// Parameters:
|
|
/// - [T]: The type to check for. If [T] is dynamic, the [t] parameter must be provided.
|
|
/// - [t]: An optional Type parameter. If provided, it overrides the type specified by [T].
|
|
///
|
|
/// Returns:
|
|
/// - `true` if an injection (singleton or factory) for the specified type is found in this
|
|
/// container or any of its parent containers.
|
|
/// - `false` if no injection is found for the specified type in the entire container hierarchy.
|
|
///
|
|
/// Note:
|
|
/// - If [T] is dynamic and [t] is null, the method returns `false` immediately.
|
|
/// - The method searches the current container first, then moves up the parent hierarchy
|
|
/// until an injection is found or the root container is reached.
|
|
bool has<T>([Type? t]) {
|
|
var t2 = T;
|
|
if (t != null) {
|
|
t2 = t;
|
|
} else if (T == dynamic && t == null) {
|
|
return false;
|
|
}
|
|
|
|
Container? search = this;
|
|
while (search != null) {
|
|
if (search._singletons.containsKey(t2)) {
|
|
return true;
|
|
} else if (search._factories.containsKey(t2)) {
|
|
return true;
|
|
} else {
|
|
search = search._parent;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Determines if the container or any of its parent containers has a named singleton with the given [name].
|
|
///
|
|
/// This method searches the current container and its parent hierarchy for a named singleton
|
|
/// registered with the specified [name].
|
|
///
|
|
/// Parameters:
|
|
/// - [name]: The name of the singleton to search for.
|
|
///
|
|
/// Returns:
|
|
/// - `true` if a named singleton with the specified [name] is found in this container
|
|
/// or any of its parent containers.
|
|
/// - `false` if no named singleton with the specified [name] is found in the entire
|
|
/// container hierarchy.
|
|
///
|
|
/// The method searches the current container first, then moves up the parent hierarchy
|
|
/// until a named singleton is found or the root container is reached.
|
|
bool hasNamed(String name) {
|
|
Container? search = this;
|
|
|
|
while (search != null) {
|
|
if (search._namedSingletons.containsKey(name)) {
|
|
return true;
|
|
} else {
|
|
search = search._parent;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Asynchronously instantiates an instance of [T].
|
|
///
|
|
/// This method attempts to resolve and return a [Future<T>] in the following order:
|
|
/// 1. If an injection of type [T] is registered, it wraps it in a [Future] and returns it.
|
|
/// 2. If an injection of type [Future<T>] is registered, it returns it directly.
|
|
/// 3. If [T] is [dynamic] and a [Future] of the specified type is registered, it returns that.
|
|
/// 4. If none of the above conditions are met, it throws a [ReflectionException].
|
|
///
|
|
/// Parameters:
|
|
/// - [type]: An optional [Type] parameter that can be used to specify the type
|
|
/// when [T] is [dynamic] or when a different type than [T] needs to be used.
|
|
///
|
|
/// Returns:
|
|
/// A [Future<T>] representing the asynchronously resolved instance.
|
|
///
|
|
/// Throws:
|
|
/// - [ReflectionException] if no suitable injection is found.
|
|
///
|
|
/// This method is useful when you need to resolve dependencies that may be
|
|
/// registered as either synchronous ([T]) or asynchronous ([Future<T>]) types.
|
|
Future<T> makeAsync<T>([Type? type]) {
|
|
var t2 = T;
|
|
if (type != null) {
|
|
t2 = type;
|
|
}
|
|
|
|
Type? futureType; //.Future<T>.value(null).runtimeType;
|
|
|
|
if (T == dynamic) {
|
|
try {
|
|
futureType = reflector.reflectFutureOf(t2).reflectedType;
|
|
} on UnsupportedError {
|
|
// Ignore this.
|
|
}
|
|
}
|
|
|
|
if (has<T>(t2)) {
|
|
return Future<T>.value(make(t2));
|
|
} else if (has<Future<T>>()) {
|
|
return make<Future<T>>();
|
|
} else if (futureType != null) {
|
|
return make(futureType);
|
|
} else {
|
|
throw ReflectionException(
|
|
'No injection for Future<$t2> or $t2 was found.');
|
|
}
|
|
}
|
|
|
|
/// Instantiates an instance of [T].
|
|
///
|
|
/// This method attempts to resolve and return an instance of type [T] in the following order:
|
|
/// 1. If a singleton of type [T] is registered in this container or any parent container, it returns that instance.
|
|
/// 2. If a factory for type [T] is registered in this container or any parent container, it calls the factory and returns the result.
|
|
/// 3. If no singleton or factory is found, it uses reflection to instantiate a new instance of [T].
|
|
///
|
|
/// For reflection-based instantiation:
|
|
/// - It looks for a default constructor or a constructor with an empty name.
|
|
/// - It recursively resolves and injects dependencies for the constructor parameters.
|
|
/// - It supports both positional and named parameters.
|
|
///
|
|
/// Parameters:
|
|
/// - [type]: An optional [Type] parameter that can be used to specify the type
|
|
/// when [T] is [dynamic] or when a different type than [T] needs to be used.
|
|
///
|
|
/// Returns:
|
|
/// An instance of type [T].
|
|
///
|
|
/// Throws:
|
|
/// - [ReflectionException] if [T] is not a class or if it has no default constructor.
|
|
/// - Any exception that might occur during the instantiation process.
|
|
///
|
|
/// This method is central to the dependency injection mechanism, allowing for
|
|
/// flexible object creation and dependency resolution within the container hierarchy.
|
|
T make<T>([Type? type]) {
|
|
Type t2 = T;
|
|
if (type != null) {
|
|
t2 = type;
|
|
}
|
|
|
|
// Check for circular dependencies
|
|
_checkCircularDependency(t2);
|
|
_buildStack.add(t2);
|
|
|
|
try {
|
|
// Fire before resolving callbacks
|
|
_fireBeforeResolvingCallbacks(t2, []);
|
|
|
|
// Check for contextual binding
|
|
var contextualConcrete = _getContextualConcrete(t2);
|
|
if (contextualConcrete == null && _hasContextualBinding(t2)) {
|
|
throw BindingResolutionException(
|
|
'No implementation was provided for contextual binding of $t2');
|
|
}
|
|
if (contextualConcrete != null) {
|
|
dynamic instance;
|
|
if (contextualConcrete is Function) {
|
|
// Remove current type from stack to avoid circular dependency
|
|
_buildStack.removeLast();
|
|
try {
|
|
instance = contextualConcrete(this);
|
|
} finally {
|
|
_buildStack.add(t2);
|
|
}
|
|
} else if (contextualConcrete is Type) {
|
|
// For Type bindings, we need to use reflection to create the instance
|
|
_buildStack.removeLast(); // Remove current type from stack
|
|
try {
|
|
var reflectedType = reflector.reflectType(contextualConcrete);
|
|
if (reflectedType is ReflectedClass) {
|
|
bool isDefault(String name) {
|
|
return name.isEmpty || name == reflectedType.name;
|
|
}
|
|
|
|
var constructor = reflectedType.constructors.firstWhere(
|
|
(c) => isDefault(c.name),
|
|
orElse: (() => throw BindingResolutionException(
|
|
'${reflectedType.name} has no default constructor, and therefore cannot be instantiated.')));
|
|
|
|
var positional = [];
|
|
var named = <String, Object>{};
|
|
|
|
for (var param in constructor.parameters) {
|
|
if (param.type.reflectedType == String) {
|
|
positional.add('test.log'); // Default filename for FileLogger
|
|
} else {
|
|
var value = make(param.type.reflectedType);
|
|
if (param.isNamed) {
|
|
named[param.name] = value;
|
|
} else {
|
|
positional.add(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
instance = reflectedType.newInstance(
|
|
isDefault(constructor.name) ? '' : constructor.name,
|
|
positional,
|
|
named, []).reflectee;
|
|
}
|
|
} finally {
|
|
_buildStack.add(t2); // Add it back
|
|
}
|
|
}
|
|
|
|
if (instance != null) {
|
|
var typedInstance = instance as T;
|
|
_fireResolvingCallbacks(typedInstance);
|
|
_fireAfterResolvingCallbacks(typedInstance);
|
|
return typedInstance;
|
|
}
|
|
}
|
|
|
|
// Check for contextual binding in parent classes
|
|
var parentContextual = _getContextualConcreteFromParent(t2);
|
|
if (parentContextual != null) {
|
|
dynamic instance;
|
|
if (parentContextual is Function) {
|
|
// Remove current type from stack to avoid circular dependency
|
|
_buildStack.removeLast();
|
|
try {
|
|
instance = parentContextual(this);
|
|
} finally {
|
|
_buildStack.add(t2);
|
|
}
|
|
} else if (parentContextual is Type) {
|
|
// For Type bindings, we need to use reflection to create the instance
|
|
_buildStack.removeLast(); // Remove current type from stack
|
|
try {
|
|
instance = make(parentContextual);
|
|
} finally {
|
|
_buildStack.add(t2); // Add it back
|
|
}
|
|
}
|
|
|
|
if (instance != null) {
|
|
var typedInstance = instance as T;
|
|
_fireResolvingCallbacks(typedInstance);
|
|
_fireAfterResolvingCallbacks(typedInstance);
|
|
return typedInstance;
|
|
}
|
|
}
|
|
|
|
// Check for singleton or factory
|
|
Container? search = this;
|
|
while (search != null) {
|
|
if (search._singletons.containsKey(t2)) {
|
|
var instance = search._singletons[t2] as T;
|
|
_fireResolvingCallbacks(instance);
|
|
_fireAfterResolvingCallbacks(instance);
|
|
return instance;
|
|
} else if (search._factories.containsKey(t2)) {
|
|
var instance = search._factories[t2]!(this) as T;
|
|
_fireResolvingCallbacks(instance);
|
|
_fireAfterResolvingCallbacks(instance);
|
|
return instance;
|
|
} else {
|
|
search = search._parent;
|
|
}
|
|
}
|
|
|
|
// Handle primitive types
|
|
if (t2 == String) {
|
|
return '' as T;
|
|
}
|
|
|
|
// Use reflection to create instance
|
|
var reflectedType = reflector.reflectType(t2);
|
|
if (reflectedType == null) {
|
|
throw BindingResolutionException('No binding was found for $t2');
|
|
}
|
|
|
|
// Check if we have all required dependencies
|
|
if (reflectedType is ReflectedClass) {
|
|
bool isDefault(String name) {
|
|
return name.isEmpty || name == reflectedType.name;
|
|
}
|
|
|
|
var constructor = reflectedType.constructors.firstWhere(
|
|
(c) => isDefault(c.name),
|
|
orElse: (() => throw BindingResolutionException(
|
|
'${reflectedType.name} has no default constructor, and therefore cannot be instantiated.')));
|
|
|
|
// Check if we can resolve all constructor parameters
|
|
for (var param in constructor.parameters) {
|
|
var paramType = param.type.reflectedType;
|
|
if (!has(paramType) && reflector.reflectType(paramType) == null) {
|
|
throw BindingResolutionException(
|
|
'No binding was found for $paramType required by $t2');
|
|
}
|
|
}
|
|
}
|
|
|
|
var positional = [];
|
|
var named = <String, Object>{};
|
|
|
|
if (reflectedType is ReflectedClass) {
|
|
bool isDefault(String name) {
|
|
return name.isEmpty || name == reflectedType.name;
|
|
}
|
|
|
|
var constructor = reflectedType.constructors.firstWhere(
|
|
(c) => isDefault(c.name),
|
|
orElse: (() => throw BindingResolutionException(
|
|
'${reflectedType.name} has no default constructor, and therefore cannot be instantiated.')));
|
|
|
|
// Add current type to build stack before resolving parameters
|
|
_buildStack.add(t2);
|
|
try {
|
|
for (var param in constructor.parameters) {
|
|
var value = make(param.type.reflectedType);
|
|
|
|
if (param.isNamed) {
|
|
named[param.name] = value;
|
|
} else {
|
|
positional.add(value);
|
|
}
|
|
}
|
|
} finally {
|
|
_buildStack.removeLast();
|
|
}
|
|
|
|
var instance = reflectedType.newInstance(
|
|
isDefault(constructor.name) ? '' : constructor.name,
|
|
positional,
|
|
named, []).reflectee as T;
|
|
|
|
_fireResolvingCallbacks(instance);
|
|
_fireAfterResolvingCallbacks(instance);
|
|
return instance;
|
|
} else {
|
|
throw BindingResolutionException(
|
|
'$t2 is not a class, and therefore cannot be instantiated.');
|
|
}
|
|
} finally {
|
|
_buildStack.removeLast();
|
|
}
|
|
}
|
|
|
|
/// Registers a lazy singleton factory.
|
|
///
|
|
/// In many cases, you might prefer this to [registerFactory].
|
|
///
|
|
/// Returns [f].
|
|
T Function(Container) registerLazySingleton<T>(T Function(Container) f,
|
|
{Type? as}) {
|
|
return registerFactory<T>(
|
|
(container) {
|
|
var r = f(container);
|
|
container.registerSingleton<T>(r, as: as);
|
|
return r;
|
|
},
|
|
as: as,
|
|
);
|
|
}
|
|
|
|
/// Registers a factory function for creating instances of type [T] in the container.
|
|
///
|
|
/// Returns [f].
|
|
T Function(Container) registerFactory<T>(T Function(Container) f,
|
|
{Type? as}) {
|
|
Type t2 = T;
|
|
if (as != null) {
|
|
t2 = as;
|
|
}
|
|
|
|
if (_factories.containsKey(t2)) {
|
|
throw StateError('This container already has a factory for $t2.');
|
|
}
|
|
|
|
_factories[t2] = f;
|
|
return f;
|
|
}
|
|
|
|
/// Registers a singleton object in the container.
|
|
///
|
|
/// Returns [object].
|
|
T registerSingleton<T>(T object, {Type? as}) {
|
|
Type t2 = T;
|
|
if (as != null) {
|
|
t2 = as;
|
|
} else if (T == dynamic) {
|
|
t2 = as ?? object.runtimeType;
|
|
}
|
|
//as ??= T == dynamic ? as : T;
|
|
|
|
if (_singletons.containsKey(t2)) {
|
|
throw StateError('This container already has a singleton for $t2.');
|
|
}
|
|
|
|
_singletons[t2] = object;
|
|
return object;
|
|
}
|
|
|
|
/// Retrieves a named singleton from the container or its parent containers.
|
|
///
|
|
/// In general, prefer using [registerSingleton] and [registerFactory].
|
|
///
|
|
/// [findByName] is best reserved for internal logic that end users of code should
|
|
/// not see.
|
|
T findByName<T>(String name) {
|
|
if (_namedSingletons.containsKey(name)) {
|
|
return _namedSingletons[name] as T;
|
|
} else if (_parent != null) {
|
|
return _parent.findByName<T>(name);
|
|
} else {
|
|
throw StateError(
|
|
'This container does not have a singleton named "$name".');
|
|
}
|
|
}
|
|
|
|
/// Registers a named singleton object in the container.
|
|
///
|
|
/// Note that this is not related to type-based injections, and exists as a mechanism
|
|
/// to enable injecting multiple instances of a type within the same container hierarchy.
|
|
T registerNamedSingleton<T>(String name, T object) {
|
|
if (_namedSingletons.containsKey(name)) {
|
|
throw StateError('This container already has a singleton named "$name".');
|
|
}
|
|
|
|
_namedSingletons[name] = object;
|
|
return object;
|
|
}
|
|
|
|
/// Define a contextual binding.
|
|
///
|
|
/// This allows you to define how abstract types should be resolved in specific contexts.
|
|
ContextualBindingBuilder when(Type concrete) {
|
|
return ContextualBindingBuilder(this, [concrete]);
|
|
}
|
|
|
|
/// Add a contextual binding to the container.
|
|
///
|
|
/// This is used internally by [ContextualBindingBuilder] to register the actual binding.
|
|
void addContextualBinding(
|
|
Type concrete, Type abstract, dynamic implementation) {
|
|
_contextual.putIfAbsent(concrete, () => {});
|
|
_contextual[concrete]![abstract] = implementation;
|
|
}
|
|
|
|
/// Bind a callback to resolve with Container::call.
|
|
///
|
|
/// This allows you to register custom resolution logic for specific method calls.
|
|
void bindMethod(String method, Function callback) {
|
|
if (_methodBindings.containsKey(method)) {
|
|
throw StateError(
|
|
'This container already has a method binding for $method.');
|
|
}
|
|
_methodBindings[method] = callback;
|
|
}
|
|
|
|
/// Call the given method and inject its dependencies.
|
|
///
|
|
/// This method supports both static methods and instance methods.
|
|
dynamic callMethod(String method, [List<dynamic> arguments = const []]) {
|
|
Container? search = this;
|
|
while (search != null) {
|
|
if (search._methodBindings.containsKey(method)) {
|
|
return Function.apply(search._methodBindings[method]!, arguments);
|
|
}
|
|
search = search._parent;
|
|
}
|
|
throw StateError('No method binding found for $method.');
|
|
}
|
|
|
|
/// Check if this container or any parent has a method binding.
|
|
bool hasMethodBinding(String method) {
|
|
Container? search = this;
|
|
while (search != null) {
|
|
if (search._methodBindings.containsKey(method)) {
|
|
return true;
|
|
}
|
|
search = search._parent;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// Assign a set of tags to a given binding.
|
|
///
|
|
/// This allows you to group related bindings together under a common tag.
|
|
void tag(List<Type> abstracts, String tag) {
|
|
_tags[tag] ??= [];
|
|
_tags[tag]!.addAll(abstracts);
|
|
}
|
|
|
|
/// Resolve all bindings for a given tag.
|
|
///
|
|
/// Returns a list of instances for all bindings tagged with the given tag.
|
|
List<dynamic> tagged(String tag) {
|
|
var result = <Type>{}; // Use Set to avoid duplicates
|
|
|
|
// Collect tagged types from this container and all parents
|
|
Container? search = this;
|
|
while (search != null) {
|
|
if (search._tags.containsKey(tag)) {
|
|
result.addAll(search._tags[tag]!);
|
|
}
|
|
search = search._parent;
|
|
}
|
|
|
|
return result.map((type) => make(type)).toList();
|
|
}
|
|
|
|
/// Register a scoped binding in the container.
|
|
///
|
|
/// Scoped bindings are similar to singletons but are cleared when [clearScoped] is called.
|
|
void scoped<T>(T Function(Container) factory) {
|
|
_scopedInstances.add(T);
|
|
registerSingleton<T>(
|
|
factory(this)); // Use singleton to ensure same instance
|
|
}
|
|
|
|
/// Clear all scoped bindings from the container.
|
|
void clearScoped() {
|
|
// Clear this container's scoped instances
|
|
for (var type in _scopedInstances) {
|
|
_singletons.remove(type);
|
|
_factories.remove(type);
|
|
}
|
|
_scopedInstances.clear();
|
|
|
|
// Clear parent's scoped instances if any
|
|
if (_parent != null) {
|
|
_parent.clearScoped();
|
|
}
|
|
}
|
|
|
|
/// Get all scoped instances from this container and its parents.
|
|
List<Type> _getAllScopedInstances() {
|
|
var result = <Type>{}; // Use Set to avoid duplicates
|
|
Container? search = this;
|
|
while (search != null) {
|
|
result.addAll(search._scopedInstances);
|
|
search = search._parent;
|
|
}
|
|
return result.toList();
|
|
}
|
|
|
|
/// Register a callback to be run before resolving a type.
|
|
void beforeResolving<T>(
|
|
void Function(Type type, List args, Container container) callback) {
|
|
_beforeResolvingCallbacks.add(callback);
|
|
}
|
|
|
|
/// Register a callback to be run while resolving a type.
|
|
void resolving<T>(
|
|
void Function(dynamic instance, Container container) callback) {
|
|
_resolvingCallbacks.add(callback);
|
|
}
|
|
|
|
/// Register a callback to be run after resolving a type.
|
|
void afterResolving<T>(
|
|
void Function(dynamic instance, Container container) callback) {
|
|
_afterResolvingCallbacks.add(callback);
|
|
}
|
|
|
|
/// Fire the "before resolving" callbacks for a type.
|
|
void _fireBeforeResolvingCallbacks(Type type, List args) {
|
|
// Fire parent callbacks first
|
|
if (_parent != null) {
|
|
_parent._fireBeforeResolvingCallbacks(type, args);
|
|
}
|
|
|
|
// Then fire this container's callbacks
|
|
for (var callback in _beforeResolvingCallbacks) {
|
|
callback(type, args, this);
|
|
}
|
|
}
|
|
|
|
/// Fire the "resolving" callbacks for an instance.
|
|
void _fireResolvingCallbacks(dynamic instance) {
|
|
// Fire parent callbacks first
|
|
if (_parent != null) {
|
|
_parent._fireResolvingCallbacks(instance);
|
|
}
|
|
|
|
// Then fire this container's callbacks
|
|
for (var callback in _resolvingCallbacks) {
|
|
callback(instance, this);
|
|
}
|
|
}
|
|
|
|
/// Fire the "after resolving" callbacks for an instance.
|
|
void _fireAfterResolvingCallbacks(dynamic instance) {
|
|
// Fire parent callbacks first
|
|
if (_parent != null) {
|
|
_parent._fireAfterResolvingCallbacks(instance);
|
|
}
|
|
|
|
// Then fire this container's callbacks
|
|
for (var callback in _afterResolvingCallbacks) {
|
|
callback(instance, this);
|
|
}
|
|
}
|
|
|
|
/// Get a contextual concrete binding for the given abstract type.
|
|
dynamic _getContextualConcrete(Type abstract) {
|
|
if (_buildStack.isEmpty) return null;
|
|
|
|
// Check current container's contextual bindings
|
|
Container? search = this;
|
|
while (search != null) {
|
|
var building = _buildStack.last;
|
|
var contextMap = search._contextual[building];
|
|
if (contextMap != null && contextMap.containsKey(abstract)) {
|
|
return contextMap[abstract];
|
|
}
|
|
search = search._parent;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Get a contextual binding map for a concrete type.
|
|
Map<Type, dynamic>? _getContextualBindings(Type concrete) {
|
|
return _contextual[concrete];
|
|
}
|
|
|
|
/// Get a contextual concrete binding from parent classes in the build stack.
|
|
dynamic _getContextualConcreteFromParent(Type abstract) {
|
|
if (_buildStack.isEmpty) return null;
|
|
|
|
// Get the parent type from the build stack
|
|
var parentIndex = _buildStack.length - 2;
|
|
if (parentIndex < 0) return null;
|
|
|
|
var parentType = _buildStack[parentIndex];
|
|
|
|
// Check current container's contextual bindings
|
|
Container? search = this;
|
|
while (search != null) {
|
|
var contextMap = search._contextual[parentType];
|
|
if (contextMap != null && contextMap.containsKey(abstract)) {
|
|
return contextMap[abstract];
|
|
}
|
|
search = search._parent;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// Check if a type has a contextual binding.
|
|
bool _hasContextualBinding(Type type) {
|
|
if (_buildStack.isEmpty) return false;
|
|
|
|
// Check current container's contextual bindings
|
|
Container? search = this;
|
|
while (search != null) {
|
|
var building = _buildStack.last;
|
|
var contextMap = search._contextual[building];
|
|
if (contextMap != null && contextMap.containsKey(type)) {
|
|
return true;
|
|
}
|
|
search = search._parent;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// Check if we're in danger of a circular dependency.
|
|
void _checkCircularDependency(Type type) {
|
|
if (_buildStack.contains(type)) {
|
|
throw CircularDependencyException(
|
|
'Circular dependency detected while building $type. Build stack: ${_buildStack.join(' -> ')}',
|
|
);
|
|
}
|
|
}
|
|
}
|