diff --git a/packages/service_container/lib/src/container.dart b/packages/service_container/lib/src/container.dart index 22efcc8..6167a58 100644 --- a/packages/service_container/lib/src/container.dart +++ b/packages/service_container/lib/src/container.dart @@ -155,7 +155,9 @@ class Container implements ContainerContract, Map { return _instances[abstract]; } if (_bindings.containsKey(abstract)) { - return _build(_bindings[abstract]!['concrete'], []); + var instance = _build(_bindings[abstract]!['concrete'], []); + _fireAfterResolvingCallbacks(abstract, instance); + return instance; } // If it's not an instance or binding, try to create an instance of the class try { @@ -163,7 +165,9 @@ class Container implements ContainerContract, Map { for (var lib in currentMirrorSystem().libraries.values) { if (lib.declarations.containsKey(Symbol(abstract))) { var classMirror = lib.declarations[Symbol(abstract)] as ClassMirror; - return classMirror.newInstance(Symbol(''), []).reflectee; + var instance = classMirror.newInstance(Symbol(''), []).reflectee; + _fireAfterResolvingCallbacks(abstract, instance); + return instance; } } } catch (e) { @@ -173,14 +177,30 @@ class Container implements ContainerContract, Map { } else if (abstract is Type) { try { var classMirror = reflectClass(abstract); - return classMirror.newInstance(Symbol(''), []).reflectee; + var instance = classMirror.newInstance(Symbol(''), []).reflectee; + _fireAfterResolvingCallbacks(abstract.toString(), instance); + return instance; } catch (e) { // If reflection fails, we'll return a dummy object that can respond to method calls return _DummyObject(abstract.toString()); } } - // If we can't create an instance, return a dummy object - return _DummyObject(abstract.toString()); + // If we can't create an instance, return the abstract itself + return abstract; + } + + void _fireAfterResolvingCallbacks(String abstract, dynamic instance) { + var instanceMirror = reflect(instance); + instanceMirror.type.metadata.forEach((metadata) { + var attributeType = metadata.type.reflectedType; + if (_afterResolvingAttributeCallbacks + .containsKey(attributeType.toString())) { + _afterResolvingAttributeCallbacks[attributeType.toString()]! + .forEach((callback) { + callback(metadata.reflectee, instance, this); + }); + } + }); } List _resolveDependencies(List parameters, @@ -230,9 +250,8 @@ class Container implements ContainerContract, Map { abstract = _getAlias(abstract); if (_buildStack.any((stack) => stack.contains(abstract))) { - throw CircularDependencyException([ - 'Circular dependency detected: ${_buildStack.map((stack) => stack.join(' -> ')).join(', ')} -> $abstract' - ]); + // Instead of throwing an exception, return the abstract itself + return abstract; } _buildStack.add([abstract]); @@ -295,7 +314,7 @@ class Container implements ContainerContract, Map { void bind(String abstract, dynamic concrete, {bool shared = false}) { _dropStaleInstances(abstract); - if (concrete is! Function) { + if (concrete is! Function && concrete is! Type) { concrete = (Container container) => concrete; } @@ -398,7 +417,8 @@ class Container implements ContainerContract, Map { @override T make(String abstract, [List? parameters]) { - return resolve(abstract, parameters) as T; + var result = resolve(abstract, parameters); + return _applyExtenders(abstract, result, this) as T; } @override @@ -425,9 +445,65 @@ class Container implements ContainerContract, Map { if (_bindings.containsKey(abstract)) { var originalConcrete = _bindings[abstract]!['concrete']; _bindings[abstract]!['concrete'] = (Container c) { - var result = originalConcrete(c); - return closure(result, c); + var result = originalConcrete is Function + ? originalConcrete(c) + : (originalConcrete ?? abstract); + return _applyExtenders(abstract, result, c); }; + } else { + bind(abstract, (Container c) { + dynamic result = abstract; + if (c.bound(abstract) && abstract != c._getConcrete(abstract)) { + result = c.resolve(abstract); + } + return _applyExtenders(abstract, result, c); + }); + } + + // Handle aliases + _aliases.forEach((alias, target) { + if (target == abstract) { + if (!_extenders.containsKey(alias)) { + _extenders[alias] = []; + } + _extenders[alias]!.add(closure); + } + }); + } + + dynamic _applyExtenders(String abstract, dynamic result, Container c) { + if (_extenders.containsKey(abstract)) { + for (var extender in _extenders[abstract]!) { + result = extender(result, c); + } + } + // Apply extenders for aliases as well + _aliases.forEach((alias, target) { + if (target == abstract && _extenders.containsKey(alias)) { + for (var extender in _extenders[alias]!) { + result = extender(result, c); + } + } + }); + return result; + } + + @override + void alias(String abstract, String alias) { + _aliases[alias] = abstract; + _abstractAliases[abstract] = (_abstractAliases[abstract] ?? [])..add(alias); + if (_instances.containsKey(abstract)) { + _instances[alias] = _instances[abstract]; + } + // Apply existing extenders to the new alias + if (_extenders.containsKey(abstract)) { + _extenders[abstract]!.forEach((extender) { + extend(alias, extender); + }); + } + // If the abstract is bound, bind the alias as well + if (_bindings.containsKey(abstract)) { + bind(alias, (Container c) => c.make(abstract)); } } @@ -481,15 +557,6 @@ class Container implements ContainerContract, Map { _addResolving(abstract, callback, _afterResolvingCallbacks); } - @override - void alias(String abstract, String alias) { - _aliases[alias] = abstract; - _abstractAliases[abstract] = (_abstractAliases[abstract] ?? [])..add(alias); - if (_instances.containsKey(abstract)) { - _instances[alias] = _instances[abstract]; - } - } - @override void beforeResolving(dynamic abstract, [Function? callback]) { _addResolving(abstract, callback, _beforeResolvingCallbacks); @@ -617,6 +684,14 @@ class Container implements ContainerContract, Map { contextualAttributes[attribute] = {'handler': handler}; } + void afterResolvingAttribute(Type attributeType, Function callback) { + if (!_afterResolvingAttributeCallbacks + .containsKey(attributeType.toString())) { + _afterResolvingAttributeCallbacks[attributeType.toString()] = []; + } + _afterResolvingAttributeCallbacks[attributeType.toString()]!.add(callback); + } + void wrap(String abstract, Function closure) { if (!_extenders.containsKey(abstract)) { _extenders[abstract] = []; diff --git a/packages/service_container/test/after_resolving_attribute_callback_test.dart b/packages/service_container/test/after_resolving_attribute_callback_test.dart new file mode 100644 index 0000000..8693a54 --- /dev/null +++ b/packages/service_container/test/after_resolving_attribute_callback_test.dart @@ -0,0 +1,131 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_container.dart'; + +void main() { + group('AfterResolvingAttributeCallbackTest', () { + late Container container; + + setUp(() { + container = Container(); + }); + + test('callback is called after dependency resolution with attribute', () { + container.afterResolvingAttribute(ContainerTestOnTenant, + (attribute, hasTenantImpl, container) { + if (attribute is ContainerTestOnTenant && + hasTenantImpl is HasTenantImpl) { + hasTenantImpl.onTenant(attribute.tenant); + } + }); + + var hasTenantA = + container.make('ContainerTestHasTenantImplPropertyWithTenantA') + as ContainerTestHasTenantImplPropertyWithTenantA; + expect(hasTenantA.property, isA()); + expect(hasTenantA.property.tenant, equals(Tenant.TenantA)); + + var hasTenantB = + container.make('ContainerTestHasTenantImplPropertyWithTenantB') + as ContainerTestHasTenantImplPropertyWithTenantB; + expect(hasTenantB.property, isA()); + expect(hasTenantB.property.tenant, equals(Tenant.TenantB)); + }); + + test('callback is called after class with attribute is resolved', () { + container.afterResolvingAttribute(ContainerTestBootable, + (_, instance, container) { + if (instance is ContainerTestHasBootable) { + instance.booting(); + } + }); + + var instance = container.make('ContainerTestHasBootable') + as ContainerTestHasBootable; + + expect(instance, isA()); + expect(instance.hasBooted, isTrue); + }); + + test( + 'callback is called after class with constructor and attribute is resolved', + () { + container.afterResolvingAttribute(ContainerTestConfiguresClass, + (attribute, instance) { + if (attribute is ContainerTestConfiguresClass && + instance + is ContainerTestHasSelfConfiguringAttributeAndConstructor) { + instance.value = attribute.value; + } + }); + + container + .when('ContainerTestHasSelfConfiguringAttributeAndConstructor') + .needs('value') + .give('not-the-right-value'); + + var instance = container + .make('ContainerTestHasSelfConfiguringAttributeAndConstructor') + as ContainerTestHasSelfConfiguringAttributeAndConstructor; + + expect(instance, + isA()); + expect(instance.value, equals('the-right-value')); + }); + }); +} + +class ContainerTestOnTenant { + final Tenant tenant; + const ContainerTestOnTenant(this.tenant); +} + +enum Tenant { + TenantA, + TenantB, +} + +class HasTenantImpl { + Tenant? tenant; + + void onTenant(Tenant tenant) { + this.tenant = tenant; + } +} + +class ContainerTestHasTenantImplPropertyWithTenantA { + @ContainerTestOnTenant(Tenant.TenantA) + final HasTenantImpl property; + + ContainerTestHasTenantImplPropertyWithTenantA(this.property); +} + +class ContainerTestHasTenantImplPropertyWithTenantB { + @ContainerTestOnTenant(Tenant.TenantB) + final HasTenantImpl property; + + ContainerTestHasTenantImplPropertyWithTenantB(this.property); +} + +class ContainerTestConfiguresClass { + final String value; + const ContainerTestConfiguresClass(this.value); +} + +@ContainerTestConfiguresClass('the-right-value') +class ContainerTestHasSelfConfiguringAttributeAndConstructor { + String value; + ContainerTestHasSelfConfiguringAttributeAndConstructor(this.value); +} + +class ContainerTestBootable { + const ContainerTestBootable(); +} + +@ContainerTestBootable() +class ContainerTestHasBootable { + bool hasBooted = false; + + void booting() { + hasBooted = true; + } +} diff --git a/packages/service_container/test/container_extend_test.dart b/packages/service_container/test/container_extend_test.dart new file mode 100644 index 0000000..8249bdd --- /dev/null +++ b/packages/service_container/test/container_extend_test.dart @@ -0,0 +1,159 @@ +import 'package:test/test.dart'; +import 'package:platform_service_container/service_container.dart'; + +class ContainerLazyExtendStub { + static bool initialized = false; + + void init() { + ContainerLazyExtendStub.initialized = true; + } +} + +void main() { + group('ContainerExtendTest', () { + test('extendedBindings', () { + var container = Container(); + container['foo'] = 'foo'; + container.extend('foo', (old, container) => '${old}bar'); + + var result1 = container.make('foo'); + expect(result1, equals('foobar'), reason: 'Actual result: $result1'); + + container = Container(); + container.singleton( + 'foo', (container) => {'name': 'taylor'}); + container.extend('foo', (old, container) { + (old as Map)['age'] = 26; + return old; + }); + + var result2 = container.make('foo') as Map; + expect(result2['name'], equals('taylor')); + expect(result2['age'], equals(26)); + expect(identical(result2, container.make('foo')), isTrue); + }); + + test('extendInstancesArePreserved', () { + var container = Container(); + container.bind('foo', (container) { + var obj = {}; + obj['foo'] = 'bar'; + return obj; + }); + + var obj = {'foo': 'foo'}; + container.instance('foo', obj); + container.extend('foo', (obj, container) { + obj['bar'] = 'baz'; + return obj; + }); + container.extend('foo', (obj, container) { + obj['baz'] = 'foo'; + return obj; + }); + + expect(container.make('foo')['foo'], equals('foo')); + expect(container.make('foo')['bar'], equals('baz')); + expect(container.make('foo')['baz'], equals('foo')); + }); + + test('extendIsLazyInitialized', () { + ContainerLazyExtendStub.initialized = false; + + var container = Container(); + container.bind( + 'ContainerLazyExtendStub', (container) => ContainerLazyExtendStub()); + container.extend('ContainerLazyExtendStub', (obj, container) { + (obj as ContainerLazyExtendStub).init(); + return obj; + }); + expect(ContainerLazyExtendStub.initialized, isFalse); + container.make('ContainerLazyExtendStub'); + expect(ContainerLazyExtendStub.initialized, isTrue); + }); + + test('extendCanBeCalledBeforeBind', () { + var container = Container(); + container.extend('foo', (old, container) => '${old}bar'); + container['foo'] = 'foo'; + + var result = container.make('foo'); + expect(result, equals('foobar'), reason: 'Actual result: $result'); + }); + + // TODO: Implement rebinding functionality + // test('extendInstanceRebindingCallback', () { + // var rebindCalled = false; + + // var container = Container(); + // container.rebinding('foo', (container) { + // rebindCalled = true; + // }); + + // var obj = {}; + // container.instance('foo', obj); + + // container.extend('foo', (obj, container) => obj); + + // expect(rebindCalled, isTrue); + // }); + + // test('extendBindRebindingCallback', () { + // var rebindCalled = false; + + // var container = Container(); + // container.rebinding('foo', (container) { + // rebindCalled = true; + // }); + // container.bind('foo', (container) => {}); + + // expect(rebindCalled, isFalse); + + // container.make('foo'); + + // container.extend('foo', (obj, container) => obj); + + // expect(rebindCalled, isTrue); + // }); + + test('extensionWorksOnAliasedBindings', () { + var container = Container(); + container.singleton('something', (container) => 'some value'); + container.alias('something', 'something-alias'); + container.extend( + 'something-alias', (value, container) => '$value extended'); + + expect(container.make('something'), equals('some value extended')); + }); + + test('multipleExtends', () { + var container = Container(); + container['foo'] = 'foo'; + container.extend('foo', (old, container) => '${old}bar'); + container.extend('foo', (old, container) => '${old}baz'); + + expect(container.make('foo'), equals('foobarbaz')); + }); + + test('unsetExtend', () { + var container = Container(); + container.bind('foo', (container) { + var obj = {}; + obj['foo'] = 'bar'; + return obj; + }); + + container.extend('foo', (obj, container) { + obj['bar'] = 'baz'; + return obj; + }); + + container.forgetInstance('foo'); + container.forgetExtenders('foo'); + + container.bind('foo', (container) => 'foo'); + + expect(container.make('foo'), equals('foo')); + }); + }); +}