From 987e5bd09920488e1d63b77c67f1d7beaba9afc4 Mon Sep 17 00:00:00 2001 From: Michal Kruczek Date: Wed, 10 Jan 2024 08:27:32 +0100 Subject: [PATCH 1/5] porting service locator feat for php-di v7 --- src/CompiledContainer.php | 20 ++ src/Compiler/Compiler.php | 6 + src/Definition/Reference.php | 55 ++++- src/Definition/ServiceLocatorDefinition.php | 89 ++++++++ .../Source/ReflectionBasedAutowiring.php | 6 +- src/ServiceLocator.php | 90 ++++++++ src/ServiceLocatorRepository.php | 98 +++++++++ src/ServiceSubscriberException.php | 11 + src/ServiceSubscriberInterface.php | 35 +++ .../ServiceLocatorDefinitionTest.php | 54 +++++ tests/IntegrationTest/ServiceLocatorTest.php | 202 ++++++++++++++++++ tests/UnitTest/Definition/ReferenceTest.php | 51 +++++ .../ServiceLocatorDefinitionTest.php | 57 +++++ .../Source/ReflectionBasedAutowiringTest.php | 4 +- .../ServiceLocatorRepositoryTest.php | 114 ++++++++++ .../ServiceLocator/ServiceLocatorTest.php | 90 ++++++++ 16 files changed, 974 insertions(+), 8 deletions(-) create mode 100644 src/Definition/ServiceLocatorDefinition.php create mode 100644 src/ServiceLocator.php create mode 100644 src/ServiceLocatorRepository.php create mode 100644 src/ServiceSubscriberException.php create mode 100644 src/ServiceSubscriberInterface.php create mode 100644 tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php create mode 100644 tests/IntegrationTest/ServiceLocatorTest.php create mode 100644 tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php create mode 100644 tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php create mode 100644 tests/UnitTest/ServiceLocator/ServiceLocatorTest.php diff --git a/src/CompiledContainer.php b/src/CompiledContainer.php index 3120600aa..2b7ec5d27 100644 --- a/src/CompiledContainer.php +++ b/src/CompiledContainer.php @@ -112,4 +112,24 @@ protected function resolveFactory($callable, $entryName, array $extraParameters throw new InvalidDefinition("Entry \"$entryName\" cannot be resolved: " . $e->getMessage()); } } + + /** + * Resolve ServiceLocator for given subscriber class (based on \DI\Definition\ServiceLocatorDefinition::resolve). + * + * @param string $requestingName class name of a subscriber, implementing ServiceSubscriberInterface + * @param string $repositoryClass ServiceLocatorRepository + * @throws ServiceSubscriberException + */ + protected function resolveServiceLocator(string $requestingName, string $repositoryClass) : ServiceLocator + { + if (!method_exists($requestingName, 'getSubscribedServices')) { + throw new ServiceSubscriberException(sprintf('The class %s does not implement ServiceSubscriberInterface.', $requestingName)); + } + + /** @var ServiceLocatorRepository $repository */ + $repository = $this->delegateContainer->get($repositoryClass); + $services = $requestingName::getSubscribedServices(); + + return $repository->create($requestingName, $services); + } } diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index eeea24039..5f96b5d22 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -206,6 +206,12 @@ private function compileDefinition(string $entryName, Definition $definition) : $code = 'return ' . $this->compileValue($value) . ';'; break; case $definition instanceof Reference: + if ($definition->isServiceLocatorEntry()) { + $requestingEntry = $definition->getRequestingName(); + $serviceLocatorDefinition = $definition->getServiceLocatorDefinition(); + $code = 'return $this->resolveServiceLocator(' . $this->compileValue($requestingEntry) . ', ' . $this->compileValue($serviceLocatorDefinition::$serviceLocatorRepositoryClass) . ');'; + break; + } $targetEntryName = $definition->getTargetEntryName(); $code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');'; // If this method is not yet compiled we store it for compilation diff --git a/src/Definition/Reference.php b/src/Definition/Reference.php index 839e7aa2a..e45f5701a 100644 --- a/src/Definition/Reference.php +++ b/src/Definition/Reference.php @@ -4,6 +4,8 @@ namespace DI\Definition; +use DI\Definition\Exception\InvalidDefinition; +use DI\ServiceLocator; use Psr\Container\ContainerInterface; /** @@ -13,15 +15,25 @@ */ class Reference implements Definition, SelfResolvingDefinition { + public static $serviceLocatorClass = ServiceLocator::class; + /** Entry name. */ private string $name = ''; - /** - * @param string $targetEntryName Name of the target entry - */ + private bool $isServiceLocatorEntry; + public function __construct( + /** + * @var string Name of the target entry + */ private string $targetEntryName, + /** + * @var string|null name of an entry - holder of a definition requesting this entry + */ + private ?string $requestingName = null, + private ?ServiceLocatorDefinition $serviceLocatorDefinition = null ) { + $this->isServiceLocatorEntry = $targetEntryName === self::$serviceLocatorClass; } public function getName() : string @@ -39,13 +51,50 @@ public function getTargetEntryName() : string return $this->targetEntryName; } + /** + * Returns the name of the entity requesting this entry. + */ + public function getRequestingName() : string + { + return $this->requestingName; + } + + public function isServiceLocatorEntry() : bool + { + return $this->isServiceLocatorEntry; + } + + public function getServiceLocatorDefinition() : ServiceLocatorDefinition + { + if (!$this->isServiceLocatorEntry || $this->requestingName === null) { + throw new InvalidDefinition(sprintf( + "Invalid service locator definition ('%s' for '%s')", + $this->targetEntryName, + $this->requestingName + )); + } + if (!$this->serviceLocatorDefinition) { + $this->serviceLocatorDefinition = new ServiceLocatorDefinition($this->getTargetEntryName(), $this->requestingName); + } + + return $this->serviceLocatorDefinition; + } + public function resolve(ContainerInterface $container) : mixed { + if ($this->isServiceLocatorEntry) { + return $this->getServiceLocatorDefinition()->resolve($container); + } + return $container->get($this->getTargetEntryName()); } public function isResolvable(ContainerInterface $container) : bool { + if ($this->isServiceLocatorEntry) { + return $this->getServiceLocatorDefinition()->isResolvable($container); + } + return $container->has($this->getTargetEntryName()); } diff --git a/src/Definition/ServiceLocatorDefinition.php b/src/Definition/ServiceLocatorDefinition.php new file mode 100644 index 000000000..ecab27e1b --- /dev/null +++ b/src/Definition/ServiceLocatorDefinition.php @@ -0,0 +1,89 @@ +name; + } + + public function setName(string $name) : void + { + $this->name = $name; + } + + /** + * Returns the name of the holder of the definition requesting service locator. + */ + public function getRequestingName() : string + { + return $this->requestingName; + } + + /** + * Resolve the definition and return the resulting value. + * + * @throws ServiceSubscriberException + */ + public function resolve(ContainerInterface $container) : ServiceLocator + { + if (!method_exists($this->requestingName, 'getSubscribedServices')) { + throw new ServiceSubscriberException(sprintf('The class %s does not implement ServiceSubscriberInterface.', $this->requestingName)); + } + + /** @var ServiceLocatorRepository $repository */ + $repository = $container->get(self::$serviceLocatorRepositoryClass); + $services = $this->requestingName::getSubscribedServices(); + + return $repository->create($this->requestingName, $services); + } + + /** + * Check if a definition can be resolved. + */ + public function isResolvable(ContainerInterface $container) : bool + { + return method_exists($this->requestingName, 'getSubscribedServices'); + } + + public function replaceNestedDefinitions(callable $replacer) : void + { + // no nested definitions + } + + /** + * Definitions can be cast to string for debugging information. + */ + public function __toString() : string + { + return sprintf( + 'get(%s) for \'%s\'', + $this->name, + $this->requestingName + ); + } +} diff --git a/src/Definition/Source/ReflectionBasedAutowiring.php b/src/Definition/Source/ReflectionBasedAutowiring.php index 781269f1a..bb573598b 100644 --- a/src/Definition/Source/ReflectionBasedAutowiring.php +++ b/src/Definition/Source/ReflectionBasedAutowiring.php @@ -30,7 +30,7 @@ public function autowire(string $name, ObjectDefinition $definition = null) : Ob $class = new \ReflectionClass($className); $constructor = $class->getConstructor(); if ($constructor && $constructor->isPublic()) { - $constructorInjection = MethodInjection::constructor($this->getParametersDefinition($constructor)); + $constructorInjection = MethodInjection::constructor($this->getParametersDefinition($constructor, $class->getName())); $definition->completeConstructorInjection($constructorInjection); } @@ -53,7 +53,7 @@ public function getDefinitions() : array /** * Read the type-hinting from the parameters of the function. */ - private function getParametersDefinition(\ReflectionFunctionAbstract $constructor) : array + private function getParametersDefinition(\ReflectionFunctionAbstract $constructor, string $className) : array { $parameters = []; @@ -77,7 +77,7 @@ private function getParametersDefinition(\ReflectionFunctionAbstract $constructo continue; } - $parameters[$index] = new Reference($parameterType->getName()); + $parameters[$index] = new Reference($parameterType->getName(), $className); } return $parameters; diff --git a/src/ServiceLocator.php b/src/ServiceLocator.php new file mode 100644 index 000000000..60925225e --- /dev/null +++ b/src/ServiceLocator.php @@ -0,0 +1,90 @@ +setServices($services); + } + + protected function setServices(array $services) : void + { + foreach ($services as $key => $value) { + if (is_numeric($key)) { + $key = $value; + } + $this->services[$key] = $value; + } + } + + /** + * Get defined services. + */ + public function getServices() : array + { + return $this->services; + } + + /** + * Get name of a class to which this service locator instance belongs to. + */ + public function getSubscriber() : string + { + return $this->subscriber; + } + + /** + * Finds a service by its identifier. + * + * @param string $id Identifier of the entry to look for. + * + * @throws NotFoundExceptionInterface No entry was found for **this** identifier. + * @throws ContainerExceptionInterface Error while retrieving the entry. + */ + public function get(string $id) : mixed + { + if (!isset($this->services[$id])) { + throw new NotFoundException("Service '$id' is not defined."); + } + + return $this->container->get($this->services[$id]); + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * `has($id)` returning true does not mean that `get($id)` will not throw an exception. + * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. + * + * @param string $id Identifier of the entry to look for. + */ + public function has(string $id) : bool + { + if (!isset($this->services[$id])) { + return false; + } + + return $this->container->has($this->services[$id]); + } +} diff --git a/src/ServiceLocatorRepository.php b/src/ServiceLocatorRepository.php new file mode 100644 index 000000000..77161236e --- /dev/null +++ b/src/ServiceLocatorRepository.php @@ -0,0 +1,98 @@ +overrides[$entry])) { + $services = array_merge($services, $this->overrides[$entry]); + } + if (!isset($this->locators[$entry])) { + $this->locators[$entry] = new ServiceLocator($this->container, $services, $entry); + } else { + // the service locator cannot be re-created - the existing locator may be returned only if expected services are identical + // compare passed services and those in the already created ServiceLocator + $locatorServices = $this->locators[$entry]->getServices(); + foreach ($services as $key => $value) { + if (is_numeric($key)) { + $key = $value; + } + if (!array_key_exists($key, $locatorServices) || $locatorServices[$key] !== $value) { + throw new \LogicException(sprintf( + "ServiceLocator for '%s' cannot be recreated with different services.", + $entry + )); + } + } + } + + return $this->locators[$entry]; + } + + /** + * Override a single service for a service locator. + * This can be only used before the service locator for the given entry is created. + * + * @return $this + */ + public function override(string $entry, string $serviceId, string $serviceEntry = null) + { + if (isset($this->locators[$entry])) { + throw new \LogicException(sprintf( + "Service '%s' for '%s' cannot be overridden - ServiceLocator is already created.", + $serviceId, + $entry + )); + } + + $serviceEntry ??= $serviceId; + $this->overrides[$entry][$serviceId] = $serviceEntry; + + return $this; + } + + /** + * Get a service locator for an entry. + * @throws NotFoundException + */ + public function get(string $id) : ServiceLocator + { + if (!isset($this->locators[$id])) { + throw new NotFoundException("Service locator for entry '$id' is not initialized."); + } + + return $this->locators[$id]; + } + + public function has(string $id) : bool + { + return isset($this->locators[$id]); + } +} diff --git a/src/ServiceSubscriberException.php b/src/ServiceSubscriberException.php new file mode 100644 index 000000000..d207d7c11 --- /dev/null +++ b/src/ServiceSubscriberException.php @@ -0,0 +1,11 @@ +>> Suggested as a lightweight alternative for heavyweight proxies from ocramius/proxy-manager. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + */ +interface ServiceSubscriberInterface +{ + /** + * Lazy instantiate heavy dependencies on-demand + * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * + * * ['logger' => Psr\Log\LoggerInterface::class] means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * ['Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] + * + * @return array The required service types, optionally keyed by service names + */ + public static function getSubscribedServices() : array; +} diff --git a/tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php b/tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php new file mode 100644 index 000000000..e163537e2 --- /dev/null +++ b/tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php @@ -0,0 +1,54 @@ +addDefinitions([ + ServiceLocatorDefinitionTest\TestClass::class => autowire() + ]); + $container = $builder->build(); + + self::assertEntryIsCompiled($container, ServiceLocatorDefinitionTest\TestClass::class); + + $instance = $container->get(ServiceLocatorDefinitionTest\TestClass::class); + $this->assertInstanceOf(ServiceLocator::class, $instance->serviceLocator); + $this->assertEquals(ServiceLocatorDefinitionTest\TestClass::class, $instance->serviceLocator->getSubscriber()); + $this->assertEquals(['foo' => 'foo'], $instance->serviceLocator->getServices()); + } +} + +namespace DI\Test\IntegrationTest\Definitions\ServiceLocatorDefinitionTest; + +use DI\ServiceLocator; +use DI\ServiceSubscriberInterface; + +class TestClass implements ServiceSubscriberInterface +{ + public $serviceLocator; + + public function __construct(ServiceLocator $serviceLocator) + { + $this->serviceLocator = $serviceLocator; + } + + public static function getSubscribedServices(): array + { + return ['foo']; + } +} diff --git a/tests/IntegrationTest/ServiceLocatorTest.php b/tests/IntegrationTest/ServiceLocatorTest.php new file mode 100644 index 000000000..d4d271fb8 --- /dev/null +++ b/tests/IntegrationTest/ServiceLocatorTest.php @@ -0,0 +1,202 @@ +addDefinitions([ + 'foo' => 'value of foo', + 'baz' => 'baz', + ]); + + $container = $builder->build(); + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertEquals('value of foo', $instance->getFoo()); + $this->assertEquals('baz', $instance->getBar()); + $this->assertInstanceOf(ServiceLocatorTest\SomeService::class, $instance->getClass()); + } + + /** + * @dataProvider provideContainer + */ + public function testServiceLocatorThrowsForInvalidService(ContainerBuilder $builder) + { + $this->expectException(\DI\NotFoundException::class); + $this->expectExceptionMessage('Service \'baz\' is not defined.'); + + $builder->addDefinitions([ + 'baz' => 'baz', + ]); + + $container = $builder->build(); + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $instance->getInvalid(); + } + + /** + * @dataProvider provideContainer + */ + public function testServicesLazyResolve(ContainerBuilder $builder) + { + $container = $builder->build(); + + // services should not be resolved on instantiation of a subscriber class + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertNotContains(ServiceLocatorTest\SomeService::class, $container->getKnownEntryNames()); + + // resolve on demand + $instance->getClass(); + $this->assertContains(ServiceLocatorTest\SomeService::class, $container->getKnownEntryNames()); + } + + /** + * @dataProvider provideContainer + */ + public function testOverrideService(ContainerBuilder $builder) + { + $builder->addDefinitions([ + 'foo' => 'foo', + 'baz' => 'baz', + 'anotherFoo' => 'overridden foo', + ]); + $container = $builder->build(); + $repository = $container->get(ServiceLocatorRepository::class); + $repository->override(ServiceLocatorTest\ServiceSubscriber::class, 'foo', 'anotherFoo'); + + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertEquals('overridden foo', $instance->getFoo()); + } + + /** + * @dataProvider provideContainer + */ + public function testOverrideServiceInRepositoryDefinition(ContainerBuilder $builder) + { + $builder->addDefinitions([ + ServiceLocatorRepository::class => autowire() + ->method('override', ServiceLocatorTest\ServiceSubscriber::class, 'foo', 'anotherFoo'), + 'anotherFoo' => 'overridden foo', + ]); + + $container = $builder->build(); + + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertEquals('overridden foo', $instance->getFoo()); + } + + /** + * @dataProvider provideContainer + */ + public function testCannotOverrideServiceForAlreadyInstantiatedSubscriber(ContainerBuilder $builder) + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service \'foo\' for \'DI\Test\IntegrationTest\ServiceLocatorTest\ServiceSubscriber\' cannot be overridden - ServiceLocator is already created.'); + + $container = $builder->build(); + + $container->get(ServiceLocatorTest\ServiceSubscriber::class); + + $repository = $container->get(ServiceLocatorRepository::class); + $repository->override(ServiceLocatorTest\ServiceSubscriber::class, 'foo', 'anotherFoo'); + } + + /** + * @dataProvider provideContainer + */ + public function testMultipleSubscriberInstances(ContainerBuilder $builder) + { + $container = $builder->build(); + $instance1 = $container->make(ServiceLocatorTest\ServiceSubscriber::class); + $instance2 = $container->make(ServiceLocatorTest\ServiceSubscriber::class); + + // different instances + $this->assertNotSame($instance1, $instance2); + // but the same service locator instance + $this->assertSame($instance1->getServiceLocator(), $instance2->getServiceLocator()); + // and an instance of a service should be shared too + $this->assertSame($instance1->getClass(), $instance2->getClass()); + } + +} + +namespace DI\Test\IntegrationTest\ServiceLocatorTest; + +use DI\ServiceLocator; +use DI\ServiceSubscriberInterface; + +/** + * Fixture class for testing service locators + */ +class ServiceSubscriber implements ServiceSubscriberInterface +{ + /** + * @var ServiceLocator + */ + protected $serviceLocator; + + /** + * @param ServiceLocator $serviceLocator + */ + public function __construct(ServiceLocator $serviceLocator) + { + $this->serviceLocator = $serviceLocator; + } + + /** + * Lazy instantiate heavy dependencies on-demand + */ + public static function getSubscribedServices(): array + { + return [ + 'foo', + 'bar' => 'baz', + SomeService::class, + ]; + } + + public function getFoo() + { + return $this->serviceLocator->get('foo'); + } + + public function getBar() + { + return $this->serviceLocator->get('bar'); + } + + public function getClass() + { + return $this->serviceLocator->get(SomeService::class); + } + + /** + * @throws \DI\NotFoundException + */ + public function getInvalid() + { + return $this->serviceLocator->get('baz'); + } + + public function getServiceLocator() + { + return $this->serviceLocator; + } +} + +class SomeService +{ +} diff --git a/tests/UnitTest/Definition/ReferenceTest.php b/tests/UnitTest/Definition/ReferenceTest.php index 9b59d6960..72b3484f7 100644 --- a/tests/UnitTest/Definition/ReferenceTest.php +++ b/tests/UnitTest/Definition/ReferenceTest.php @@ -72,4 +72,55 @@ public function should_cast_to_string() { $this->assertEquals('get(bar)', (string) new Reference('bar')); } + + /** + * @test + */ + public function should_have_a_requesting_name() + { + $definition = new Reference('bar', 'foo'); + $this->assertEquals('foo', $definition->getRequestingName()); + } + + /** + * @test + */ + public function should_be_a_service_locator_entry() + { + $definition = new Reference(Reference::$serviceLocatorClass, 'foo'); + $this->assertTrue($definition->isServiceLocatorEntry()); + } + + /** + * @test + */ + public function should_not_be_a_service_locator_entry() + { + $definition = new Reference('bar', 'foo'); + $this->assertFalse($definition->isServiceLocatorEntry()); + } + + /** + * @test + */ + public function should_throw_on_invalid_service_locator_entry() + { + $this->expectException(\DI\Definition\Exception\InvalidDefinition::class); + $this->expectExceptionMessage('Invalid service locator definition (\'bar\' for \'foo\')'); + + $definition = new Reference('bar', 'foo'); + $definition->getServiceLocatorDefinition(); + } + + /** + * @test + */ + public function should_throw_on_invalid_service_locator_entry2() + { + $this->expectException(\DI\Definition\Exception\InvalidDefinition::class); + $this->expectExceptionMessage('Invalid service locator definition (\'DI\ServiceLocator\' for \'\')'); + + $definition = new Reference(Reference::$serviceLocatorClass); + $definition->getServiceLocatorDefinition(); + } } diff --git a/tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php b/tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php new file mode 100644 index 000000000..a984fd65e --- /dev/null +++ b/tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php @@ -0,0 +1,57 @@ +assertEquals('ServiceLocator', $definition->getName()); + $definition->setName('foo'); + $this->assertEquals('foo', $definition->getName()); + + $this->assertEquals('subscriber', $definition->getRequestingName()); + } + + /** + * @test + */ + public function cannot_resolve_without_proper_subscriber() + { + $this->expectException(\DI\ServiceSubscriberException::class); + $this->expectExceptionMessage('The class DI\Test\UnitTest\Fixtures\Singleton does not implement ServiceSubscriberInterface.'); + + $container = $this->easyMock(ContainerInterface::class); + $definition = new ServiceLocatorDefinition(ServiceLocator::class, Singleton::class); + + $this->assertFalse($definition->isResolvable($container)); + $definition->resolve($container); + } + + /** + * @test + */ + public function should_cast_to_string() + { + $definition = new ServiceLocatorDefinition('bar', 'subscriber'); + $this->assertEquals("get(bar) for 'subscriber'", (string) $definition); + } +} diff --git a/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php b/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php index 9ae98b9a2..3123df644 100644 --- a/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php +++ b/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php @@ -35,7 +35,7 @@ public function testConstructor() $this->assertCount(1, $parameters); $param1 = $parameters[0]; - $this->assertEquals(new Reference(AutowiringFixture::class), $param1); + $this->assertEquals(new Reference(AutowiringFixture::class, AutowiringFixture::class), $param1); } public function testConstructorInParentClass() @@ -50,6 +50,6 @@ public function testConstructorInParentClass() $this->assertCount(1, $parameters); $param1 = $parameters[0]; - $this->assertEquals(new Reference(AutowiringFixture::class), $param1); + $this->assertEquals(new Reference(AutowiringFixture::class, AutowiringFixtureChild::class), $param1); } } diff --git a/tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php b/tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php new file mode 100644 index 000000000..9c8ad0477 --- /dev/null +++ b/tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php @@ -0,0 +1,114 @@ +container = $containerBuilder->build(); + } + + protected function tearDown(): void + { + $this->container = null; + } + + public function testCreateServiceLocator() + { + $repository = new ServiceLocatorRepository($this->container); + + $services = ['SomeServiceClass']; + $expectedServices = ['SomeServiceClass' => 'SomeServiceClass']; + + $serviceLocator = $repository->create('test', $services); + + $this->assertEquals('test', $serviceLocator->getSubscriber()); + $this->assertEquals($expectedServices, $serviceLocator->getServices()); + } + + public function testServiceLocatorNotCreated() + { + $this->expectException(\DI\NotFoundException::class); + $this->expectExceptionMessage('Service locator for entry \'something\' is not initialized.'); + + $repository = new ServiceLocatorRepository($this->container); + $repository->get('something'); + } + + public function testGetServiceLocator() + { + $repository = new ServiceLocatorRepository($this->container); + $repository->create('test'); + + $this->assertInstanceOf(ServiceLocator::class, $repository->get('test')); + } + + public function testHasServiceLocator() + { + $repository = new ServiceLocatorRepository($this->container); + $repository->create('test'); + + $this->assertTrue($repository->has('test')); + $this->assertFalse($repository->has('something-else')); + } + + public function testOverrideService() + { + $repository = new ServiceLocatorRepository($this->container); + $repository->override('test', 'foo'); + $repository->override('test', 'bar', 'baz'); + + $locator = $repository->create('test'); + $this->assertEquals(['foo' => 'foo', 'bar' => 'baz'], $locator->getServices()); + } + + public function testCanCreateMultipleWithSameServices() + { + $repository = new ServiceLocatorRepository($this->container); + $locator1 = $repository->create('test', ['foo']); + $locator2 = $repository->create('test', ['foo']); + + // same instance + $this->assertSame($locator1, $locator2); + + $repository->override('test2', 'bar', 'baz'); + $locator3 = $repository->create('test2'); + $locator4 = $repository->create('test2'); + $this->assertSame($locator3, $locator4); + + // still same services, because that matches the initial override + $locator5 = $repository->create('test2', ['bar' => 'baz']); + $this->assertSame($locator3, $locator5); + } + + public function testCannotCreateMultipleWithDifferentServices() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('ServiceLocator for \'test\' cannot be recreated with different services.'); + + $repository = new ServiceLocatorRepository($this->container); + + $repository->create('test', ['foo']); + $repository->create('test', ['foo2']); + } +} diff --git a/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php b/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php new file mode 100644 index 000000000..7cbb905b6 --- /dev/null +++ b/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php @@ -0,0 +1,90 @@ +container = $containerBuilder->build(); + } + + protected function tearDown(): void + { + $this->container = null; + } + + public function testInstantiation() + { + $services = [ + 'foo' => 'bar', + 'baz', + ]; + $serviceLocator = new ServiceLocator($this->container, $services, 'test'); + + $this->assertEquals([ + 'foo' => 'bar', + 'baz' => 'baz', + ], $serviceLocator->getServices()); + $this->assertEquals('test', $serviceLocator->getSubscriber()); + } + + public function testServiceNotDefined() + { + $this->expectException(\DI\NotFoundException::class); + $this->expectExceptionMessage('Service \'something\' is not defined.'); + + $serviceLocator = new ServiceLocator($this->container, [], 'test'); + $serviceLocator->get('something'); + } + + public function testGetService() + { + $services = [ + 'stdClass', + 'service' => Singleton::class, + ]; + $services2 = [ + Singleton::class, + ]; + + $serviceLocator = new ServiceLocator($this->container, $services, 'test'); + $serviceLocator2 = new ServiceLocator($this->container, $services2, 'test2'); + + $this->assertInstanceOf('stdClass', $serviceLocator->get('stdClass')); + + $service1 = $serviceLocator->get('service'); + $this->assertInstanceOf(Singleton::class, $service1); + + $service2 = $serviceLocator2->get(Singleton::class); + $this->assertInstanceOf(Singleton::class, $service2); + + // it should be the same instances shared from the container + $this->assertSame($service1, $service2); + } + + public function testHasService() + { + $services = [ + 'service' => Singleton::class, + ]; + + $serviceLocator = new ServiceLocator($this->container, $services, 'test'); + + $this->assertTrue($serviceLocator->has('service')); + $this->assertFalse($serviceLocator->has(Singleton::class)); + } +} From f885a5b31bbcdc56c5976ae2f53ffce017234ca8 Mon Sep 17 00:00:00 2001 From: Marcin Gil Date: Mon, 29 Jan 2024 20:03:01 +0100 Subject: [PATCH 2/5] ServiceLocatorTest: fixes "creation of dynamic property is deprecated" --- tests/UnitTest/ServiceLocator/ServiceLocatorTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php b/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php index 7cbb905b6..b6ff66694 100644 --- a/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php +++ b/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php @@ -4,6 +4,7 @@ namespace DI\Test\UnitTest; +use DI\Container; use DI\ContainerBuilder; use DI\ServiceLocator; use DI\Test\UnitTest\Fixtures\Singleton; @@ -16,6 +17,8 @@ */ class ServiceLocatorTest extends TestCase { + private ?Container $container; + public function setUp(): void { $containerBuilder = new ContainerBuilder(); From 4395f1c76a8d01ed9ab18afa60aec915ed21d1ee Mon Sep 17 00:00:00 2001 From: Michal Kruczek Date: Tue, 30 Jan 2024 16:35:27 +0100 Subject: [PATCH 3/5] missing patches from #7 --- src/Definition/Reference.php | 2 +- src/Definition/Source/AttributeBasedAutowiring.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Definition/Reference.php b/src/Definition/Reference.php index e45f5701a..30c18e7ad 100644 --- a/src/Definition/Reference.php +++ b/src/Definition/Reference.php @@ -54,7 +54,7 @@ public function getTargetEntryName() : string /** * Returns the name of the entity requesting this entry. */ - public function getRequestingName() : string + public function getRequestingName() : ?string { return $this->requestingName; } diff --git a/src/Definition/Source/AttributeBasedAutowiring.php b/src/Definition/Source/AttributeBasedAutowiring.php index d3c90b408..f71ac6fb5 100644 --- a/src/Definition/Source/AttributeBasedAutowiring.php +++ b/src/Definition/Source/AttributeBasedAutowiring.php @@ -177,7 +177,7 @@ private function readProperty(ReflectionProperty $property, ObjectDefinition $de } $definition->addPropertyInjection( - new PropertyInjection($property->getName(), new Reference($entryName), $classname) + new PropertyInjection($property->getName(), new Reference($entryName, $classname), $classname) ); } @@ -312,7 +312,7 @@ private function readConstructor(ReflectionClass $class, ObjectDefinition $defin $entryName = $this->getMethodParameter($index, $parameter, []); if ($entryName !== null) { - $parameters[$index] = new Reference($entryName); + $parameters[$index] = new Reference($entryName, $class->getName()); } } From cd87c881f5faa6e4a05c2826ab4a4971a093409d Mon Sep 17 00:00:00 2001 From: Michal Kruczek Date: Tue, 30 Jan 2024 20:13:44 +0100 Subject: [PATCH 4/5] sort Reference out --- src/Definition/Reference.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Definition/Reference.php b/src/Definition/Reference.php index 30c18e7ad..beafa8f5f 100644 --- a/src/Definition/Reference.php +++ b/src/Definition/Reference.php @@ -22,6 +22,8 @@ class Reference implements Definition, SelfResolvingDefinition private bool $isServiceLocatorEntry; + private ?ServiceLocatorDefinition $serviceLocatorDefinition = null; + public function __construct( /** * @var string Name of the target entry @@ -30,8 +32,7 @@ public function __construct( /** * @var string|null name of an entry - holder of a definition requesting this entry */ - private ?string $requestingName = null, - private ?ServiceLocatorDefinition $serviceLocatorDefinition = null + private ?string $requestingName = null ) { $this->isServiceLocatorEntry = $targetEntryName === self::$serviceLocatorClass; } From e8efad64f1663676ad3591bc8cb8bbaeb9b033e1 Mon Sep 17 00:00:00 2001 From: Michal Kruczek Date: Wed, 31 Jan 2024 10:35:04 +0100 Subject: [PATCH 5/5] service-locator - pass requesting name when reading constructor --- src/Definition/Source/AttributeBasedAutowiring.php | 4 +++- .../Definition/Source/AttributeBasedAutowiringTest.php | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Definition/Source/AttributeBasedAutowiring.php b/src/Definition/Source/AttributeBasedAutowiring.php index f71ac6fb5..91e41b8aa 100644 --- a/src/Definition/Source/AttributeBasedAutowiring.php +++ b/src/Definition/Source/AttributeBasedAutowiring.php @@ -227,7 +227,9 @@ private function getMethodInjection(ReflectionMethod $method) : ?MethodInjection $entryName = $this->getMethodParameter($index, $parameter, $attributeParameters); if ($entryName !== null) { - $parameters[$index] = new Reference($entryName); + $parameters[$index] = $method->isConstructor() ? + new Reference($entryName, $method->getDeclaringClass()->getName()) : + new Reference($entryName); } } diff --git a/tests/UnitTest/Definition/Source/AttributeBasedAutowiringTest.php b/tests/UnitTest/Definition/Source/AttributeBasedAutowiringTest.php index 1d837e32d..1c89c2189 100644 --- a/tests/UnitTest/Definition/Source/AttributeBasedAutowiringTest.php +++ b/tests/UnitTest/Definition/Source/AttributeBasedAutowiringTest.php @@ -92,8 +92,8 @@ public function testConstructor() $parameters = $constructorInjection->getParameters(); $this->assertCount(2, $parameters); - $this->assertEquals(new Reference('foo'), $parameters[0]); - $this->assertEquals(new Reference('bar'), $parameters[1]); + $this->assertEquals(new Reference('foo', AttributeFixture::class), $parameters[0]); + $this->assertEquals(new Reference('bar', AttributeFixture::class), $parameters[1]); } public function testMethod1() @@ -253,7 +253,7 @@ public function testPromotedProperties(): void $parameters = $constructorInjection->getParameters(); $this->assertCount(1, $parameters); - $this->assertEquals(new Reference('foo'), $parameters[0]); + $this->assertEquals(new Reference('foo', AttributeFixturePromotedProperty::class), $parameters[0]); } private function getMethodInjection(ObjectDefinition $definition, $name) : ?MethodInjection