From b3e1a82898a97b5550d3e0861791de84f86e881b Mon Sep 17 00:00:00 2001 From: Constantine Karnaukhov Date: Wed, 23 Sep 2020 19:13:07 +0400 Subject: [PATCH 1/2] feat: support definition tags --- src/Container.php | 25 +++++++++- src/ContainerAwareTrait.php | 7 +-- src/ContainerChain.php | 34 +++++++++++++ src/ContainerInterface.php | 13 +++++ src/Definition/Definition.php | 29 +++++++++++ src/Definition/DefinitionAggregate.php | 39 +++++++++++++++ .../DefinitionAggregateInterface.php | 16 +++++++ src/Definition/DefinitionInterface.php | 24 +++++++++- src/Definition/DefinitionTag.php | 20 ++++++++ src/ReflectionContainer.php | 18 +++++++ .../ServiceProviderAggregate.php | 20 ++++---- tests/ContainerChainTest.php | 48 +++++++++++++++++++ tests/ContainerTest.php | 29 +++++++++++ tests/Definition/DefinitionAggregateTest.php | 45 +++++++++++++++++ tests/Definition/DefinitionTest.php | 13 +++++ tests/Fixtures/MyClassProvider.php | 3 +- tests/ReflectionContainerTest.php | 12 +++++ .../ServiceProviderAggregateTest.php | 39 +++++++++++++-- 18 files changed, 410 insertions(+), 24 deletions(-) create mode 100644 src/Definition/DefinitionTag.php diff --git a/src/Container.php b/src/Container.php index e2d1216..bbd48a7 100755 --- a/src/Container.php +++ b/src/Container.php @@ -5,6 +5,7 @@ namespace spaceonfire\Container; use InvalidArgumentException; +use spaceonfire\Collection\CollectionInterface; use spaceonfire\Container\Argument\ArgumentResolver; use spaceonfire\Container\Argument\ResolverInterface; use spaceonfire\Container\Definition\DefinitionAggregate; @@ -45,7 +46,7 @@ final class Container implements ContainerWithServiceProvidersInterface, Contain /** * Container constructor. - * @param DefinitionAggregateInterface $definitions + * @param DefinitionAggregateInterface|null $definitions * @param ServiceProviderAggregateInterface|null $providers */ public function __construct( @@ -127,7 +128,7 @@ public function get($id, array $arguments = []) if ($this->providers->provides($id)) { $this->providers->register($id); - if (!$this->definitions->hasDefinition($id)) { + if (!$this->definitions->hasDefinition($id) || $this->definitions->hasTag($id)) { throw new ContainerException(sprintf('Service provider lied about providing (%s) service', $id)); } @@ -152,4 +153,24 @@ public function invoke(callable $callable, array $arguments = []) { return ($this->callableInvoker)($callable, $arguments); } + + /** + * @inheritDoc + */ + public function hasTagged(string $tag): bool + { + return $this->definitions->hasTag($tag); + } + + /** + * @inheritDoc + */ + public function getTagged(string $tag): CollectionInterface + { + if ($this->providers->provides($tag)) { + $this->providers->register($tag); + } + + return $this->definitions->resolveTagged($tag, $this->getContainer()); + } } diff --git a/src/ContainerAwareTrait.php b/src/ContainerAwareTrait.php index a58aefe..b8a9eed 100755 --- a/src/ContainerAwareTrait.php +++ b/src/ContainerAwareTrait.php @@ -12,9 +12,7 @@ trait ContainerAwareTrait protected $container; /** - * Set a container - * @param ContainerInterface $container - * @return $this|ContainerAwareInterface + * @inheritDoc */ public function setContainer(ContainerInterface $container): ContainerAwareInterface { @@ -23,8 +21,7 @@ public function setContainer(ContainerInterface $container): ContainerAwareInter } /** - * Get the container - * @return ContainerInterface + * @inheritDoc */ public function getContainer(): ContainerInterface { diff --git a/src/ContainerChain.php b/src/ContainerChain.php index cf5624f..a7d3fa7 100755 --- a/src/ContainerChain.php +++ b/src/ContainerChain.php @@ -5,6 +5,8 @@ namespace spaceonfire\Container; use Psr\Container\ContainerInterface as PsrContainerInterface; +use spaceonfire\Collection\Collection; +use spaceonfire\Collection\CollectionInterface; use spaceonfire\Container\Definition\DefinitionInterface; use spaceonfire\Container\Exception\ContainerException; use spaceonfire\Container\Exception\NotFoundException; @@ -185,4 +187,36 @@ public function addServiceProvider($provider): ContainerWithServiceProvidersInte throw new ContainerException('No container provided with support of service providers'); } + + /** + * @inheritDoc + */ + public function hasTagged(string $tag): bool + { + foreach ($this->chain as $container) { + if ($container instanceof ContainerInterface && $container->hasTagged($tag)) { + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function getTagged(string $tag): CollectionInterface + { + $result = new Collection(); + + foreach ($this->chain as $container) { + if (!$container instanceof ContainerInterface) { + continue; + } + + $result = $result->merge($container->getTagged($tag)); + } + + return $result; + } } diff --git a/src/ContainerInterface.php b/src/ContainerInterface.php index 0c3c4dd..e065073 100755 --- a/src/ContainerInterface.php +++ b/src/ContainerInterface.php @@ -5,6 +5,7 @@ namespace spaceonfire\Container; use Psr\Container\ContainerInterface as PsrContainerInterface; +use spaceonfire\Collection\CollectionInterface; use spaceonfire\Container\Definition\DefinitionInterface; interface ContainerInterface extends PsrContainerInterface @@ -47,4 +48,16 @@ public function add(string $id, $concrete = null, bool $shared = false): Definit * @return DefinitionInterface */ public function share(string $id, $concrete = null): DefinitionInterface; + + /** + * @param string $tag + * @return bool + */ + public function hasTagged(string $tag): bool; + + /** + * @param string $tag + * @return mixed[]|CollectionInterface + */ + public function getTagged(string $tag): CollectionInterface; } diff --git a/src/Definition/Definition.php b/src/Definition/Definition.php index 5c768f0..a81cbd9 100755 --- a/src/Definition/Definition.php +++ b/src/Definition/Definition.php @@ -30,6 +30,10 @@ final class Definition implements DefinitionInterface * @var array[] */ private $methods = []; + /** + * @var array + */ + private $tags = []; /** * @var object|null */ @@ -152,4 +156,29 @@ public function resolve(ContainerInterface $container) return $this->resolved = $resolved; } + + /** + * @inheritDoc + */ + public function addTag(string $tag): DefinitionInterface + { + $this->tags[$tag] = $tag; + return $this; + } + + /** + * @inheritDoc + */ + public function hasTag(string $tag): bool + { + return array_key_exists($tag, $this->tags); + } + + /** + * @inheritDoc + */ + public function getTags(): array + { + return array_keys($this->tags); + } } diff --git a/src/Definition/DefinitionAggregate.php b/src/Definition/DefinitionAggregate.php index 18d8912..94e37fb 100755 --- a/src/Definition/DefinitionAggregate.php +++ b/src/Definition/DefinitionAggregate.php @@ -5,6 +5,7 @@ namespace spaceonfire\Container\Definition; use spaceonfire\Collection\AbstractCollectionDecorator; +use spaceonfire\Collection\Collection; use spaceonfire\Collection\CollectionInterface; use spaceonfire\Collection\IndexedCollection; use spaceonfire\Collection\TypedCollection; @@ -18,6 +19,10 @@ final class DefinitionAggregate extends AbstractCollectionDecorator implements D * @var DefinitionFactoryInterface */ private $definitionFactory; + /** + * @var array + */ + private $tags = []; /** * DefinitionAggregate constructor. @@ -115,4 +120,38 @@ public function resolve(string $id, ContainerInterface $container) { return $this->getDefinition($id)->resolve($container); } + + /** + * @inheritDoc + */ + public function hasTag(string $tag): bool + { + if (array_key_exists($tag, $this->tags)) { + return true; + } + + /** @var DefinitionInterface $definition */ + foreach ($this->getIterator() as $definition) { + if ($definition->hasTag($tag)) { + $this->tags[$tag] = $tag; + return true; + } + } + + return false; + } + + /** + * @inheritDoc + */ + public function resolveTagged(string $tag, ContainerInterface $container): CollectionInterface + { + return (new Collection($this->getIterator())) + ->filter(static function (DefinitionInterface $definition) use ($tag) { + return $definition->hasTag($tag); + }) + ->map(static function (DefinitionInterface $definition) use ($container) { + return $definition->resolve($container); + }); + } } diff --git a/src/Definition/DefinitionAggregateInterface.php b/src/Definition/DefinitionAggregateInterface.php index 2c42959..832c917 100755 --- a/src/Definition/DefinitionAggregateInterface.php +++ b/src/Definition/DefinitionAggregateInterface.php @@ -5,6 +5,7 @@ namespace spaceonfire\Container\Definition; use IteratorAggregate; +use spaceonfire\Collection\CollectionInterface; use spaceonfire\Container\ContainerInterface; interface DefinitionAggregateInterface extends IteratorAggregate @@ -46,4 +47,19 @@ public function makeDefinition(string $abstract, $concrete, bool $shared = false * @return mixed */ public function resolve(string $id, ContainerInterface $container); + + /** + * Checks whether tag exists as definition. + * @param string $tag + * @return bool + */ + public function hasTag(string $tag): bool; + + /** + * Resolve and build a collection of concrete values by given tag. + * @param string $tag + * @param ContainerInterface $container + * @return mixed[]|CollectionInterface + */ + public function resolveTagged(string $tag, ContainerInterface $container): CollectionInterface; } diff --git a/src/Definition/DefinitionInterface.php b/src/Definition/DefinitionInterface.php index 391a9bc..dcaddd8 100755 --- a/src/Definition/DefinitionInterface.php +++ b/src/Definition/DefinitionInterface.php @@ -42,7 +42,7 @@ public function addArgument(Argument $argument): self; public function addArguments(array $arguments): self; /** - * Add a method to be invoked + * Add a method to be invoked. * @param string $method * @param array $arguments * @return $this @@ -50,7 +50,7 @@ public function addArguments(array $arguments): self; public function addMethodCall(string $method, array $arguments = []): self; /** - * Add multiple methods to be invoked + * Add multiple methods to be invoked. * @param array> $methods * @return $this */ @@ -62,4 +62,24 @@ public function addMethodCalls(array $methods = []): self; * @return mixed */ public function resolve(ContainerInterface $container); + + /** + * Add a tag to the definition. + * @param string $tag + * @return $this + */ + public function addTag(string $tag): self; + + /** + * Does the definition have a tag? + * @param string $tag + * @return bool + */ + public function hasTag(string $tag): bool; + + /** + * Returns tags of the definition. + * @return string[] + */ + public function getTags(): array; } diff --git a/src/Definition/DefinitionTag.php b/src/Definition/DefinitionTag.php new file mode 100644 index 0000000..bc9bf7f --- /dev/null +++ b/src/Definition/DefinitionTag.php @@ -0,0 +1,20 @@ +provides() as $service) { if (!isset($this->providesMap[$service])) { - $this->providesMap[$service] = $alias; + $this->providesMap[$service] = [$alias]; + } else { + $this->providesMap[$service][] = $alias; } } @@ -105,15 +107,15 @@ public function register(string $service): void throw new ContainerException(sprintf('(%s) is not provided by a service provider', $service)); } - $providerId = $this->providesMap[$service]; + foreach ($this->providesMap[$service] as $providerId) { + if (array_key_exists($providerId, $this->registered)) { + continue; + } - if (array_key_exists($providerId, $this->registered)) { - return; + /** @var ServiceProviderInterface $provider */ + $provider = $this->offsetGet($providerId); + $provider->register(); + $this->registered[$provider->getIdentifier()] = true; } - - /** @var ServiceProviderInterface $provider */ - $provider = $this->offsetGet($providerId); - $provider->register(); - $this->registered[$provider->getIdentifier()] = true; } } diff --git a/tests/ContainerChainTest.php b/tests/ContainerChainTest.php index 1eaab80..2a7eea5 100755 --- a/tests/ContainerChainTest.php +++ b/tests/ContainerChainTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\TestCase; use Prophecy\Argument; use Psr\Container\ContainerInterface as PsrContainerInterface; +use spaceonfire\Collection\Collection; use spaceonfire\Container\Exception\ContainerException; use spaceonfire\Container\Exception\NotFoundException; @@ -130,4 +131,51 @@ public function testShare(): void $chain->share('foo', 'bar'); } + + public function testHasTagged(): void + { + $chain = new ContainerChain([]); + + self::assertFalse($chain->hasTagged('tag')); + + $containerProphecy = $this->createContainerMock(); + $containerProphecy->hasTagged('tag')->willReturn(true)->shouldBeCalled(); + $chain->addContainer($containerProphecy->reveal()); + + self::assertTrue($chain->hasTagged('tag')); + } + + public function testGetTagged(): void + { + $chain = new ContainerChain([]); + + self::assertTrue($chain->getTagged('tag')->isEmpty()); + + $containerWithFoo = $this->createContainerMock(); + $containerWithFoo->hasTagged('tag')->willReturn(true); + $containerWithFoo->getTagged('tag')->willReturn(new Collection(['foo']))->shouldBeCalled(); + $chain->addContainer($containerWithFoo->reveal()); + + $containerWithBar = $this->createContainerMock(); + $containerWithBar->hasTagged('tag')->willReturn(true); + $containerWithBar->getTagged('tag')->willReturn(new Collection(['bar']))->shouldBeCalled(); + $chain->addContainer($containerWithBar->reveal()); + + // psr container without tags support should be skipped + $chain->addContainer($this->createContainerMock([], PsrContainerInterface::class)->reveal()); + + $resolved = $chain->getTagged('tag'); + + self::assertFalse($resolved->isEmpty()); + + $foo = $resolved->find(function ($v) { + return $v === 'foo'; + }); + self::assertSame('foo', $foo); + + $bar = $resolved->find(function ($v) { + return $v === 'bar'; + }); + self::assertSame('bar', $bar); + } } diff --git a/tests/ContainerTest.php b/tests/ContainerTest.php index fc47b43..56bf96d 100755 --- a/tests/ContainerTest.php +++ b/tests/ContainerTest.php @@ -132,4 +132,33 @@ public function testResolveDefinitionWithParentContainer(): void self::assertInstanceOf(B::class, $container->get('foo')); } + + public function testHasTagged(): void + { + $container = new Container(); + + self::assertFalse($container->hasTagged('tag')); + + $container->add('foo')->addTag('tag'); + + self::assertTrue($container->hasTagged('tag')); + } + + public function testGetTagged(): void + { + $container = new Container(); + + self::assertTrue($container->getTagged('tag')->isEmpty()); + + $container->addServiceProvider(MyClassProvider::class); + + $container->add('foo', function () { + return 'foo'; + })->addTag('tag'); + + $resolved = $container->getTagged('tag'); + + self::assertFalse($resolved->isEmpty()); + self::assertSame('foo', $resolved->first()); + } } diff --git a/tests/Definition/DefinitionAggregateTest.php b/tests/Definition/DefinitionAggregateTest.php index 513f490..bc1dd4a 100755 --- a/tests/Definition/DefinitionAggregateTest.php +++ b/tests/Definition/DefinitionAggregateTest.php @@ -10,6 +10,17 @@ class DefinitionAggregateTest extends TestCase { + public function testConstruct(): void + { + $emptyAggregate = new DefinitionAggregate(); + self::assertTrue($emptyAggregate->isEmpty()); + + $definition = $emptyAggregate->makeDefinition('foo', 'bar', true); + $notEmptyAggregate = new DefinitionAggregate([$definition]); + + self::assertFalse($notEmptyAggregate->isEmpty()); + } + public function testDefinitionOperations(): void { $aggregate = new DefinitionAggregate(); @@ -61,4 +72,38 @@ public function testResolve(): void self::assertSame('baz', $aggregate->resolve('foo', $container)); } + + public function testHasTag(): void + { + $aggregate = new DefinitionAggregate(); + self::assertFalse($aggregate->hasTag('tag')); + $definition = new Definition('foo', 'bar'); + $definition->addTag('tag'); + $aggregate->addDefinition($definition); + self::assertTrue($aggregate->hasTag('tag')); + // second call should returns true immediately + self::assertTrue($aggregate->hasTag('tag')); + } + + public function testResolveTagged(): void + { + $containerProphecy = $this->prophesize(ContainerInterface::class); + $containerProphecy->has('bar')->willReturn(true); + $containerProphecy->get('bar')->willReturn('baz'); + /** @var ContainerInterface $container */ + $container = $containerProphecy->reveal(); + + $aggregate = new DefinitionAggregate(); + + self::assertTrue($aggregate->resolveTagged('baz', $container)->isEmpty()); + + $definition = new Definition('foo', 'bar'); + $definition->addTag('tag'); + $aggregate->addDefinition($definition); + + $resolved = $aggregate->resolveTagged('tag', $container); + + self::assertCount(1, $resolved); + self::assertSame('baz', $resolved->first()); + } } diff --git a/tests/Definition/DefinitionTest.php b/tests/Definition/DefinitionTest.php index c546295..7f14bfc 100755 --- a/tests/Definition/DefinitionTest.php +++ b/tests/Definition/DefinitionTest.php @@ -128,4 +128,17 @@ public function testResolveFailed(): void $definition->resolve($container); } + + public function testTags(): void + { + $definition = new Definition('foo', 'bar'); + + self::assertFalse($definition->hasTag('baz')); + + $definition->addTag('baz'); + + self::assertTrue($definition->hasTag('baz')); + + self::assertSame(['baz'], $definition->getTags()); + } } diff --git a/tests/Fixtures/MyClassProvider.php b/tests/Fixtures/MyClassProvider.php index 23016f7..df9889c 100755 --- a/tests/Fixtures/MyClassProvider.php +++ b/tests/Fixtures/MyClassProvider.php @@ -15,6 +15,7 @@ public function provides(): array { return [ MyClass::class, + 'tag', ]; } @@ -23,6 +24,6 @@ public function provides(): array */ public function register(): void { - $this->getContainer()->add(MyClass::class, new MyClass()); + $this->getContainer()->add(MyClass::class, new MyClass())->addTag('tag'); } } diff --git a/tests/ReflectionContainerTest.php b/tests/ReflectionContainerTest.php index ea82636..17bcba1 100755 --- a/tests/ReflectionContainerTest.php +++ b/tests/ReflectionContainerTest.php @@ -71,4 +71,16 @@ public function testShare(): void $container = new ReflectionContainer(); $container->share('foo', 'bar'); } + + public function testHasTagged(): void + { + $container = new ReflectionContainer(); + self::assertFalse($container->hasTagged('tag')); + } + + public function testGetTagged(): void + { + $container = new ReflectionContainer(); + self::assertTrue($container->getTagged('tag')->isEmpty()); + } } diff --git a/tests/ServiceProvider/ServiceProviderAggregateTest.php b/tests/ServiceProvider/ServiceProviderAggregateTest.php index d62e078..dde11fc 100755 --- a/tests/ServiceProvider/ServiceProviderAggregateTest.php +++ b/tests/ServiceProvider/ServiceProviderAggregateTest.php @@ -32,30 +32,43 @@ private function createAggregate(?ContainerInterface $container = null): Service return $aggregate; } - private function createServiceProvider(?string $id = null, array $provides = []): ServiceProviderInterface + private function createServiceProvider(?string $id = null, array $provides = [], array $tags = []): ServiceProviderInterface { - return new class($id, $provides) extends AbstractServiceProvider { + return new class($id, $provides, $tags) extends AbstractServiceProvider { private $provides; + private $tags; + private $registered = false; - public function __construct(?string $id = null, array $provides = []) + public function __construct(?string $id = null, array $provides = [], array $tags = []) { if ($id) { $this->setIdentifier($id); } $this->provides = $provides; + $this->tags = $tags; } public function provides(): array { - return array_keys($this->provides); + return array_merge(array_keys($this->provides), $this->tags); } public function register(): void { foreach ($this->provides as $abstract => $concrete) { - $this->getContainer()->add($abstract, $concrete, true); + $def = $this->getContainer()->add($abstract, $concrete, true); + + foreach ($this->tags as $tag) { + $def->addTag($tag); + } } + $this->registered = true; + } + + public function isRegistered(): bool + { + return $this->registered; } }; } @@ -147,4 +160,20 @@ public function testRegisterFailed(): void $aggregate = $this->createAggregate(); $aggregate->register('foo'); } + + public function testRegisterMultipleProviders(): void + { + $aggregate = $this->createAggregate(); + + $fooProvider = $this->createServiceProvider('foo', ['foo' => 'foo'], ['tag']); + $barProvider = $this->createServiceProvider('bar', ['bar' => 'bar'], ['tag']); + + $aggregate->addProvider($fooProvider); + $aggregate->addProvider($barProvider); + + $aggregate->register('tag'); + + self::assertTrue($fooProvider->isRegistered()); + self::assertTrue($barProvider->isRegistered()); + } } From 6b8041d17b205391fdf5a8a7cafadea282263993 Mon Sep 17 00:00:00 2001 From: Constantine Karnaukhov Date: Wed, 23 Sep 2020 19:15:03 +0400 Subject: [PATCH 2/2] 2.1.0 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d10d4d8..eae3487 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,10 @@ Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) princip - Nothing --> +## [2.1.0] - 2020-09-23 +### Added +- Support definition tags + ## [2.0.1] - 2020-06-21 ### Fixed - Resolve definition in `Container` class using parent container