diff --git a/CHANGELOG.md b/CHANGELOG.md index 236429b..29e6588 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ to [Semantic Versioning](http://semver.org/). ## [unreleased] Unreleased +### Added + +- The `afterBuildAll` parameter to the `bindDecorators` and `singletonDecorators` method, fixes #61. + ## [3.3.5] 2023-09-01; ### Changed diff --git a/README.md b/README.md index 6543d8e..ad4cea8 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,54 @@ $container->bindDecorators(PostEndpoint::class, [ BaseEndpoint::class ]); ``` + +Similarly to a `bind` or `singleton` call, you can specify a set of methods to call after the decorator chain is built +with the `afterBuildMethods` parameter: + +```php +use lucatume\DI52\Container; + +$container = new Container(); + +$container->bind(RepositoryInterface::class, PostRepository::class); +$container->bind(CacheInterface::class, ArrayCache::class); +$container->bind(LoggerInterface::class, FileLogger::class); +// Decorators are built left to right, outer decorators are listed first. +$container->bindDecorators(PostEndpoint::class, [ + LoggingEndpoint::class, + CachingEndpoint::class, + BaseEndpoint::class +], ['register']); +``` + +By default, the `init` method will be called **only on the base instance**, the one on the right of the decorator chain. +In the example above only `BaseEndpoint::register` would be called. + +If you need to call the same set of after-build methods on all instances after each is build, you can set the value of +the `afterBuildAll` parameter to `true`: + +```php +use lucatume\DI52\Container; + +$container = new Container(); + +$container->bind(RepositoryInterface::class, PostRepository::class); +$container->bind(CacheInterface::class, ArrayCache::class); +$container->bind(LoggerInterface::class, FileLogger::class); +// Decorators are built left to right, outer decorators are listed first. +$container->bindDecorators(PostEndpoint::class, [ + LoggingEndpoint::class, + CachingEndpoint::class, + BaseEndpoint::class +], ['register'], true); +``` + +In this example the `register` method will be called on the `BaseEndpoint` after it's built, then on the +`CachingEndpoint` class after it's built, and finally on the `LoggingEndpoint` class after it's built. + +Different combinations of decorators and after-build methods should be handled binding, with a `bind` or `singleton` +call, a Closure to build the decorator chain. + ## Tagging Tagging allows grouping similar implementations for the purpose of referencing them by group. diff --git a/src/Container.php b/src/Container.php index f86f7a3..6f8f27f 100644 --- a/src/Container.php +++ b/src/Container.php @@ -568,13 +568,15 @@ public function boot() * @param string[]|null $afterBuildMethods An array of methods that should be called on the * instance after it has been built; the methods should * not require any argument. + * @param bool $afterBuildAll Whether to call the after build methods on only the + * base instance or all instances of the decorator chain. * * @return void This method does not return any value. * @throws ContainerException */ - public function singletonDecorators($id, $decorators, array $afterBuildMethods = null) + public function singletonDecorators($id, $decorators, array $afterBuildMethods = null, $afterBuildAll = false) { - $this->resolver->singleton($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods)); + $this->resolver->singleton($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods, $afterBuildAll)); } /** @@ -584,11 +586,15 @@ public function singletonDecorators($id, $decorators, array $afterBuildMethods = * @param string $id The id to bind the decorator tail to. * @param array|null $afterBuildMethods A set of method to run on the built decorated instance * after it's built. + * @param bool $afterBuildAll Whether to run the after build methods only on the base + * instance (default, false) or on all instances of the + * decorator chain. + * * @return BuilderInterface The callable or Closure that will start building the decorator chain. * * @throws ContainerException If there's any issue while trying to register any decorator step. */ - private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMethods = null) + private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMethods = null, $afterBuildAll = false) { $decorator = array_pop($decorators); @@ -600,7 +606,9 @@ private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMe $previous = isset($builder) ? $builder : null; $builder = $this->builders->getBuilder($id, $decorator, $afterBuildMethods, $previous); $decorator = array_pop($decorators); - $afterBuildMethods = []; + if (!$afterBuildAll) { + $afterBuildMethods = []; + } } while ($decorator !== null); return $builder; @@ -616,15 +624,17 @@ private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMe * should be bound to. * @param array $decorators An array of implementations that decorate an object. * @param string[]|null $afterBuildMethods An array of methods that should be called on the - * instance after it has been built; the methods should - * not require any argument. + * base instance after it has been built; the methods + * should not require any argument. + * @param bool $afterBuildAll Whether to call the after build methods on only the + * base instance or all instances of the decorator chain. * * @return void This method does not return any value. * @throws ContainerException If there's any issue binding the decorators. */ - public function bindDecorators($id, array $decorators, array $afterBuildMethods = null) + public function bindDecorators($id, array $decorators, array $afterBuildMethods = null, $afterBuildAll = false) { - $this->resolver->bind($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods)); + $this->resolver->bind($id, $this->getDecoratorBuilder($decorators, $id, $afterBuildMethods, $afterBuildAll)); } /** diff --git a/tests/unit/DecoratorTest.php b/tests/unit/DecoratorTest.php index 29ab9d3..648e417 100644 --- a/tests/unit/DecoratorTest.php +++ b/tests/unit/DecoratorTest.php @@ -1,95 +1,168 @@ expectException(ContainerException::class); - - $container->bindDecorators('test', []); - } - - /** - * It should allow binding a decorator chain with base only - * - * @test - */ - public function should_allow_binding_a_decorator_chain_with_base_only() - { - $container = new Container() ; - - $container->bindDecorators(Message::class, [Message::class]); - - $this->assertInstanceOf(Message::class, $container->make(Message::class)); - } - - /** - * It should allow binding a decorator chain - * - * @test - */ - public function should_allow_binding_a_decorator_chain() - { - $container = new Container() ; - - $container->bindDecorators(Message::class, [EncryptedMessage::class,PrivateMessage::class,Message::class]); - - $this->assertInstanceOf(EncryptedMessage::class, $container->make(Message::class)); - $this->assertInstanceOf(MessageInterface::class, $container->make(Message::class)); - } - - /** - * It should allow binding a decorator chain as singleton - * - * @test - */ - public function should_allow_binding_a_decorator_chain_as_singleton() - { - $container = new Container() ; - - $container->singletonDecorators(CacheInterface::class, [ExternalCache::class,DbCache::class,Cache::class]); - - $this->assertInstanceOf(CacheInterface::class, $container->make(CacheInterface::class)); - $this->assertInstanceOf(ExternalCache::class, $container->make(CacheInterface::class)); - $this->assertSame($container->make(CacheInterface::class), $container->make(CacheInterface::class)); - } +class DecoratorTest extends TestCase { + /** + * It should throw if trying to bind empty decorator chain + * + * @test + */ + public function should_throw_if_trying_to_bind_empty_decorator_chain() { + $container = new Container(); + + $this->expectException( ContainerException::class ); + + $container->bindDecorators( 'test', [] ); + } + + /** + * It should allow binding a decorator chain with base only + * + * @test + */ + public function should_allow_binding_a_decorator_chain_with_base_only() { + $container = new Container(); + + $container->bindDecorators( Message::class, [ Message::class ] ); + + $this->assertInstanceOf( Message::class, $container->make( Message::class ) ); + } + + /** + * It should allow binding a decorator chain + * + * @test + */ + public function should_allow_binding_a_decorator_chain() { + $container = new Container(); + + $container->bindDecorators( Message::class, [ + EncryptedMessage::class, + PrivateMessage::class, + Message::class + ] ); + + $this->assertInstanceOf( EncryptedMessage::class, $container->make( Message::class ) ); + $this->assertInstanceOf( MessageInterface::class, $container->make( Message::class ) ); + } + + /** + * It should allow binding a decorator chain as singleton + * + * @test + */ + public function should_allow_binding_a_decorator_chain_as_singleton() { + $container = new Container(); + + $container->singletonDecorators( CacheInterface::class, [ + ExternalCache::class, + DbCache::class, + Cache::class + ] ); + + $this->assertInstanceOf( CacheInterface::class, $container->make( CacheInterface::class ) ); + $this->assertInstanceOf( ExternalCache::class, $container->make( CacheInterface::class ) ); + $this->assertSame( $container->make( CacheInterface::class ), $container->make( CacheInterface::class ) ); + } + + /** + * It should allow calling after build methods on all decorators + * + * @test + */ + public function should_allow_calling_after_build_methods_on_all_decorators() { + require_once( __DIR__ . '/data/AfterBuildDecoratorClasses.php' ); + AfterBuildDecoratorThree::reset(); + AfterBuildDecoratorTwo::reset(); + AfterBuildDecoratorOne::reset(); + AfterBuildBase::reset(); + + $container = new Container(); + + $container->bindDecorators( + ZorpMaker::class, + [ + AfterBuildDecoratorThree::class, + AfterBuildDecoratorTwo::class, + AfterBuildDecoratorOne::class, + AfterBuildBase::class + ], + [ 'setupTheZorps' ], + true + ); + + $zorpMaker = $container->get( ZorpMaker::class ); + + $this->assertTrue( AfterBuildDecoratorOne::$didSetUpTheZorps ); + $this->assertTrue( AfterBuildDecoratorTwo::$didSetUpTheZorps ); + $this->assertTrue( AfterBuildDecoratorThree::$didSetUpTheZorps ); + $this->assertTrue( AfterBuildBase::$didSetUpTheZorps ); + $this->assertInstanceOf( AfterBuildDecoratorThree::class, $zorpMaker ); + $this->assertEquals( '3 - 2 - 1 - base', $zorpMaker->makeZorps() ); + } + + /** + * It should only call afterBuild method on base instance of decorator chain by default + * + * @test + */ + public function should_only_call_after_build_method_on_base_instance_of_decorator_chain_by_default() { + require_once( __DIR__ . '/data/AfterBuildDecoratorClasses.php' ); + AfterBuildDecoratorThree::reset(); + AfterBuildDecoratorTwo::reset(); + AfterBuildDecoratorOne::reset(); + AfterBuildBase::reset(); + + $container = new Container(); + + $container->bindDecorators( + ZorpMaker::class, + [ + AfterBuildDecoratorThree::class, + AfterBuildDecoratorTwo::class, + AfterBuildDecoratorOne::class, + AfterBuildBase::class + ], + [ 'setupTheZorps' ] + ); + + $zorpMaker = $container->get( ZorpMaker::class ); + + $this->assertFalse( AfterBuildDecoratorOne::$didSetUpTheZorps ); + $this->assertFalse( AfterBuildDecoratorTwo::$didSetUpTheZorps ); + $this->assertFalse( AfterBuildDecoratorThree::$didSetUpTheZorps ); + $this->assertTrue( AfterBuildBase::$didSetUpTheZorps ); + $this->assertInstanceOf( AfterBuildDecoratorThree::class, $zorpMaker ); + $this->assertEquals( '3 - 2 - 1 - base', $zorpMaker->makeZorps() ); + } } diff --git a/tests/unit/data/AfterBuildDecoratorClasses.php b/tests/unit/data/AfterBuildDecoratorClasses.php new file mode 100644 index 0000000..d309cef --- /dev/null +++ b/tests/unit/data/AfterBuildDecoratorClasses.php @@ -0,0 +1,105 @@ +decorated = $decorated; + } + + public function setupTheZorps() + { + self::$didSetUpTheZorps = true; + } + + public function makeZorps() + { + return '1 - ' . $this->decorated->makeZorps(); + } +} + +class AfterBuildDecoratorTwo implements ZorpMaker +{ + public static $didSetUpTheZorps = false; + private $decorated; + + public static function reset() + { + self::$didSetUpTheZorps = false; + } + + public function __construct(ZorpMaker $decorated) + { + $this->decorated = $decorated; + } + + public function setupTheZorps() + { + self::$didSetUpTheZorps = true; + } + + public function makeZorps() + { + return '2 - ' . $this->decorated->makeZorps(); + } +} + +class AfterBuildDecoratorThree implements ZorpMaker +{ + public static $didSetUpTheZorps = false; + private $decorated; + + public static function reset() + { + self::$didSetUpTheZorps = false; + } + + public function __construct(ZorpMaker $decorated) + { + $this->decorated = $decorated; + } + + public function setupTheZorps() + { + self::$didSetUpTheZorps = true; + } + + public function makeZorps() + { + return '3 - ' . $this->decorated->makeZorps(); + } +} + +class AfterBuildBase implements ZorpMaker +{ + public static $didSetUpTheZorps = false; + + public static function reset() + { + self::$didSetUpTheZorps = false; + } + + public function setupTheZorps() + { + self::$didSetUpTheZorps = true; + } + + public function makeZorps() + { + return 'base'; + } +}