diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index acafc44..90fb5d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,7 +6,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '5.6', '7.0' , '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2' ] + php: [ '5.6', '7.0' , '7.1', '7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3' ] steps: - name: Checkout code uses: actions/checkout@v3 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..51e8fb8 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 `register` 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` instance after it's built, and finally on the `LoggingEndpoint` instance after it's built. + +Different and more complex 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/makefile b/makefile index 0d6c449..0faf70a 100644 --- a/makefile +++ b/makefile @@ -19,7 +19,7 @@ define xdebug_src fi endef -php_versions :=5.6 7.0 7.1 7.2 7.3 7.4 8.0 8.1 8.2 +php_versions :=5.6 7.0 7.1 7.2 7.3 7.4 8.0 8.1 8.2 8.3 build: $(build_php_versions) ## Builds the project PHP images. mkdir -p var/cache/composer mkdir -p var/log diff --git a/src/Container.php b/src/Container.php index f86f7a3..c7ac788 100644 --- a/src/Container.php +++ b/src/Container.php @@ -568,13 +568,18 @@ 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,12 +589,20 @@ 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); if ($decorator === null) { @@ -600,7 +613,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 +631,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..dec9685 100644 --- a/tests/unit/DecoratorTest.php +++ b/tests/unit/DecoratorTest.php @@ -1,4 +1,5 @@ expectException(ContainerException::class); @@ -55,9 +63,9 @@ public function should_throw_if_trying_to_bind_empty_decorator_chain() */ public function should_allow_binding_a_decorator_chain_with_base_only() { - $container = new Container() ; + $container = new Container(); - $container->bindDecorators(Message::class, [Message::class]); + $container->bindDecorators(Message::class, [ Message::class ]); $this->assertInstanceOf(Message::class, $container->make(Message::class)); } @@ -69,9 +77,13 @@ public function should_allow_binding_a_decorator_chain_with_base_only() */ public function should_allow_binding_a_decorator_chain() { - $container = new Container() ; + $container = new Container(); - $container->bindDecorators(Message::class, [EncryptedMessage::class,PrivateMessage::class,Message::class]); + $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)); @@ -84,12 +96,89 @@ public function should_allow_binding_a_decorator_chain() */ public function should_allow_binding_a_decorator_chain_as_singleton() { - $container = new Container() ; + $container = new Container(); - $container->singletonDecorators(CacheInterface::class, [ExternalCache::class,DbCache::class,Cache::class]); + $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'; + } +}