Skip to content

Commit

Permalink
feat(Decorators) add afterBuildAll parameter
Browse files Browse the repository at this point in the history
  • Loading branch information
lucatume committed Apr 2, 2024
1 parent d39d1cb commit 2af303d
Show file tree
Hide file tree
Showing 5 changed files with 326 additions and 86 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 48 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
26 changes: 18 additions & 8 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}

/**
Expand All @@ -584,11 +586,15 @@ public function singletonDecorators($id, $decorators, array $afterBuildMethods =
* @param string $id The id to bind the decorator tail to.
* @param array<string>|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);

Expand All @@ -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;
Expand All @@ -616,15 +624,17 @@ private function getDecoratorBuilder(array $decorators, $id, array $afterBuildMe
* should be bound to.
* @param array<string|object|callable> $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));
}

/**
Expand Down
229 changes: 151 additions & 78 deletions tests/unit/DecoratorTest.php
Original file line number Diff line number Diff line change
@@ -1,95 +1,168 @@
<?php

use lucatume\DI52\Container;
use lucatume\DI52\ContainerException;
use PHPUnit\Framework\TestCase;

interface MessageInterface
{
interface MessageInterface {
}
class Message implements MessageInterface
{

class Message implements MessageInterface {
}
class PrivateMessage implements MessageInterface
{

class PrivateMessage implements MessageInterface {
}
class EncryptedMessage implements MessageInterface
{

class EncryptedMessage implements MessageInterface {
}

interface CacheInterface
{
interface CacheInterface {
}
class Cache implements CacheInterface
{

class Cache implements CacheInterface {
}
class ExternalCache implements CacheInterface
{

class ExternalCache implements CacheInterface {
}
class DbCache implements CacheInterface
{

class DbCache implements CacheInterface {
}
class NullCache implements CacheInterface
{

class NullCache implements CacheInterface {
}

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));
}
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() );
}
}
Loading

0 comments on commit 2af303d

Please sign in to comment.