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 8ec76b8
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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
2 changes: 1 addition & 1 deletion makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
35 changes: 26 additions & 9 deletions src/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}

/**
Expand All @@ -584,12 +589,20 @@ 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);

if ($decorator === null) {
Expand All @@ -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;
Expand All @@ -616,15 +631,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
103 changes: 96 additions & 7 deletions tests/unit/DecoratorTest.php
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
<?php

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

interface MessageInterface
{
}

class Message implements MessageInterface
{
}

class PrivateMessage implements MessageInterface
{
}

class EncryptedMessage implements MessageInterface
{
}

interface CacheInterface
{
}

class Cache implements CacheInterface
{
}

class ExternalCache implements CacheInterface
{
}

class DbCache implements CacheInterface
{
}

class NullCache implements CacheInterface
{
}
Expand All @@ -41,7 +49,7 @@ class DecoratorTest extends TestCase
*/
public function should_throw_if_trying_to_bind_empty_decorator_chain()
{
$container = new Container() ;
$container = new Container();

$this->expectException(ContainerException::class);

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

0 comments on commit 8ec76b8

Please sign in to comment.