diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index fb9d0ef90c82a..4393b8c44c40d 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +4.2.0 +----- + + * added `ServiceSubscriberTrait` + 4.1.0 ----- diff --git a/src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.php b/src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.php new file mode 100644 index 0000000000000..0f55c742fc9a8 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/ServiceSubscriberTrait.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection; + +use Psr\Container\ContainerInterface; + +/** + * Implementation of ServiceSubscriberInterface that determines subscribed services from + * private method return types. Service ids are available as "ClassName::methodName". + * + * @author Kevin Bond + */ +trait ServiceSubscriberTrait +{ + /** @var ContainerInterface */ + private $container; + + public static function getSubscribedServices(): array + { + static $services; + + if (null !== $services) { + return $services; + } + + $services = \is_callable(array('parent', __FUNCTION__)) ? parent::getSubscribedServices() : array(); + + foreach ((new \ReflectionClass(self::class))->getMethods() as $method) { + if ($method->isStatic() || $method->isAbstract() || $method->isGenerator() || $method->isInternal() || $method->getNumberOfRequiredParameters()) { + continue; + } + + if (self::class === $method->getDeclaringClass()->name && ($returnType = $method->getReturnType()) && !$returnType->isBuiltin()) { + $services[self::class.'::'.$method->name] = '?'.$returnType->getName(); + } + } + + return $services; + } + + /** + * @required + */ + public function setContainer(ContainerInterface $container) + { + $this->container = $container; + + if (\is_callable(array('parent', __FUNCTION__))) { + return parent::setContainer($container); + } + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php index f7314948aeef6..3a0f2b93353cf 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/RegisterServiceSubscribersPassTest.php @@ -21,7 +21,12 @@ use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition1; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition2; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestDefinition3; use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriber; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberChild; +use Symfony\Component\DependencyInjection\Tests\Fixtures\TestServiceSubscriberParent; use Symfony\Component\DependencyInjection\TypedReference; require_once __DIR__.'/../Fixtures/includes/classes.php'; @@ -136,4 +141,29 @@ public function testExtraServiceSubscriber() $container->register(TestServiceSubscriber::class, TestServiceSubscriber::class); $container->compile(); } + + public function testServiceSubscriberTrait() + { + $container = new ContainerBuilder(); + + $container->register('foo', TestServiceSubscriberChild::class) + ->addMethodCall('setContainer', array(new Reference(PsrContainerInterface::class))) + ->addTag('container.service_subscriber') + ; + + (new RegisterServiceSubscribersPass())->process($container); + (new ResolveServiceSubscribersPass())->process($container); + + $foo = $container->getDefinition('foo'); + $locator = $container->getDefinition((string) $foo->getMethodCalls()[0][1][0]); + + $expected = array( + TestServiceSubscriberChild::class.'::invalidDefinition' => new ServiceClosureArgument(new TypedReference('Symfony\Component\DependencyInjection\Tests\Fixtures\InvalidDefinition', 'Symfony\Component\DependencyInjection\Tests\Fixtures\InvalidDefinition', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + TestServiceSubscriberChild::class.'::testDefinition2' => new ServiceClosureArgument(new TypedReference(TestDefinition2::class, TestDefinition2::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + TestServiceSubscriberChild::class.'::testDefinition3' => new ServiceClosureArgument(new TypedReference(TestDefinition3::class, TestDefinition3::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + TestServiceSubscriberParent::class.'::testDefinition1' => new ServiceClosureArgument(new TypedReference(TestDefinition1::class, TestDefinition1::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE)), + ); + + $this->assertEquals($expected, $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0)); + } } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php new file mode 100644 index 0000000000000..0b12c21e09125 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestDefinition1.php @@ -0,0 +1,9 @@ +container->get(__METHOD__); + } + + private function invalidDefinition(): InvalidDefinition + { + return $this->container->get(__METHOD__); + } + + private function privateFunction1(): string + { + } + + private function privateFunction2(): string + { + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php new file mode 100644 index 0000000000000..d2abb7a859cdf --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberParent.php @@ -0,0 +1,16 @@ +container->get(__METHOD__); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php new file mode 100644 index 0000000000000..ea98a0f2f13e1 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/TestServiceSubscriberTrait.php @@ -0,0 +1,11 @@ +container->get(__CLASS__.'::'.__FUNCTION__); + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.php b/src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.php new file mode 100644 index 0000000000000..e230b2069ef9a --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/ServiceSubscriberTraitTest.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ServiceSubscriberInterface; +use Symfony\Component\DependencyInjection\ServiceSubscriberTrait; + +class ServiceSubscriberTraitTest extends TestCase +{ + public function testMethodsOnParentsAndChildrenAreIgnoredInGetSubscribedServices() + { + $expected = array(TestService::class.'::aService' => '?Symfony\Component\DependencyInjection\Tests\Service2'); + + $this->assertEquals($expected, ChildTestService::getSubscribedServices()); + } + + public function testSetContainerIsCalledOnParent() + { + $container = new Container(); + + $this->assertSame($container, (new TestService())->setContainer($container)); + } +} + +class ParentTestService +{ + public function aParentService(): Service1 + { + } + + public function setContainer(ContainerInterface $container) + { + return $container; + } +} + +class TestService extends ParentTestService implements ServiceSubscriberInterface +{ + use ServiceSubscriberTrait; + + public function aService(): Service2 + { + } +} + +class ChildTestService extends TestService +{ + public function aChildService(): Service3 + { + } +}