diff --git a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php index bbe869b935e5c..0cd9ef503100d 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/AbstractRecursivePass.php @@ -22,6 +22,9 @@ */ abstract class AbstractRecursivePass implements CompilerPassInterface { + /** + * @var ContainerBuilder|null + */ protected $container; protected $currentId; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index 184c7b25a0883..138e20ad09a84 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -49,6 +49,7 @@ public function __construct() $this->optimizationPasses = array(array( new ExtensionCompilerPass(), new ResolveDefinitionTemplatesPass(), + new ServiceFactoryTagPass(), new ServiceLocatorTagPass(), new DecoratorServicePass(), new ResolveParameterPlaceHoldersPass(false), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceFactoryTagPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceFactoryTagPass.php new file mode 100644 index 0000000000000..59ad9c52aabca --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceFactoryTagPass.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Applies the "container.service_factory" tag by scraping class methods used for Definition instances. + * + * @author Roland Franssen + */ +final class ServiceFactoryTagPass extends AbstractRecursivePass +{ + /** + * {@inheritdoc} + */ + protected function processValue($value, $isRoot = false) + { + if (!$value instanceof Definition || !$value->hasTag('container.service_factory')) { + return parent::processValue($value, $isRoot); + } + + if (!$r = $this->container->getReflectionClass($class = $value->getClass())) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service factory "%s" cannot be found.', $class, $this->currentId)); + } + + if ($r->isAbstract()) { + throw new InvalidArgumentException(sprintf('Class "%s" used for service factory "%s" cannot be abstract.', $class, $this->currentId)); + } + + foreach ($this->getMethodsToFactorize($r) as $id => $method) { + $this->container->register($id) + ->setFactory(array(new Reference($this->currentId), $method->getName())) + ->setClass($this->getClass($method)); + } + } + + /** + * Gets the list of methods to factorize. + * + * @param \ReflectionClass $reflectionClass + * + * @return \ReflectionMethod[] + */ + private function getMethodsToFactorize(\ReflectionClass $reflectionClass) + { + $methodsToFactorize = array(); + + foreach ($reflectionClass->getMethods() as $method) { + if ($method->isConstructor()) { + continue; + } + + while (true) { + if (false !== $doc = $method->getDocComment()) { + if (false !== stripos($doc, '@service') && preg_match('#(?:^/\*\*|\n\s*+\*)\s*+@service(?:\s(\S+)?|\*/$)#i', $doc, $matches)) { + if (!$method->isPublic()) { + throw new InvalidArgumentException(sprintf('Method "%s::%s()" must be public in order to create a service from it.', $reflectionClass->getName(), $name)); + } + + $id = isset($matches[1]) ? $matches[1] : Container::underscore($method->getName()); + if (isset($methodsToFactorize[$id]) || $this->container->has($id)) { + throw new InvalidArgumentException(sprintf('Cannot create service "%s" from service factory "%s" as it already exists.', $id, $this->currentId)); + } + + $methodsToFactorize[$id] = $method; + break; + } + if (false === stripos($doc, '@inheritdoc') || !preg_match('#(?:^/\*\*|\n\s*+\*)\s*+(?:\{@inheritdoc\}|@inheritdoc)(?:\s|\*/$)#i', $doc)) { + break; + } + } + try { + $method = $method->getPrototype(); + } catch (\ReflectionException $e) { + break; // method has no prototype + } + } + } + + return $methodsToFactorize; + } + + private function getClass(\ReflectionMethod $reflectionMethod) + { + if (method_exists(\ReflectionMethod::class, 'getReturnType')) { + $returnType = $reflectionMethod->getReturnType(); + if (null !== $returnType && !$returnType->isBuiltin()) { + $returnType = $returnType instanceof \ReflectionNamedType ? $returnType->getName() : $returnType; + switch (strtolower($returnType)) { + case 'self': + return $reflectionMethod->getDeclaringClass()->getName(); + case 'parent': + return get_parent_class($reflectionMethod->getDeclaringClass()->getName()) ?: null; + default: + return $returnType; + } + } + } + + return null; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php index 7ab2e7c6f6c31..cb3f616de8b87 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/ContainerBuilderTest.php @@ -1111,6 +1111,23 @@ public function testPrivateServiceTriggersDeprecation() $container->get('bar'); } + + public function testServiceFactoryTag() + { + $container = new ContainerBuilder(); + $container->register(ServiceFactory::class) + ->addTag('container.service_factory'); + $container->register('foo', 'stdClass') + ->setProperty('foo', new Reference('global_service')) + ->setProperty('bar', new Reference('dep_service')) + ->setProperty('baz', new Reference('my.service')); + + $container->compile(); + + $this->assertSame($container->get('global_service'), $container->get('foo')->foo); + $this->assertSame($container->get('dep_service'), $container->get('foo')->bar); + $this->assertSame($container->get('my.service'), $container->get('foo')->baz); + } } class FooClass @@ -1127,3 +1144,43 @@ public function __construct(A $a) { } } + +abstract class ParentServiceFactory +{ + /** + * @service + */ + public static function globalService(): A + { + return new A(); + } + + /** + * @service + */ + abstract public static function depService(): FooClass; +} + +class ServiceFactory extends ParentServiceFactory +{ + /** + * @service my.service + */ + public function myService(): B + { + return new B(self::globalService()); + } + + /** + * {@inheritdoc} + */ + public static function depService(): FooClass + { + return new FooClass(); + } + + protected function internalService(): FooClass + { + return new FooClass(); + } +}