diff --git a/src/Symfony/Component/DependencyInjection/Annotation/Argument.php b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php new file mode 100644 index 0000000000000..25fcf951594f6 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Annotation; + +/** + * @Annotation + * @Target({"PROPERTY", "METHOD"}) + * + * @author Ryan Weaver + */ +class Argument +{ + private $name; + + private $value; + + private $type; + + private $id; + + private $onInvalid = 'exception'; + + private $method; + + public function __construct(array $data) + { + foreach ($data as $key => $value) { + $method = 'set'.str_replace('_', '', $key); + if (!method_exists($this, $method)) { + throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, get_class($this))); + } + $this->$method($value); + } + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + } + + public function getValue() + { + return $this->value; + } + + public function setValue($value) + { + $this->value = $value; + } + + public function getType() + { + return $this->type; + } + + public function setType($type) + { + $this->type = $type; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + $this->id = $id; + } + + public function getOnInvalid() + { + return $this->onInvalid; + } + + public function setOnInvalid($onInvalid) + { + $validOptions = array('exception', 'ignore', 'null'); + if (!in_array($onInvalid, $validOptions, true)) { + throw new \InvalidArgumentException(sprintf('Invalid onInvalid property "%s" set on annotation "%s. Expected on of: %s', $onInvalid, get_class($this), implode(', ', $validOptions))); + } + + $this->onInvalid = $onInvalid; + } + + public function getMethod() + { + return $this->method; + } + + public function setMethod($method) + { + $this->method = $method; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Annotation/Service.php b/src/Symfony/Component/DependencyInjection/Annotation/Service.php new file mode 100644 index 0000000000000..1775b98bc112b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Annotation/Service.php @@ -0,0 +1,92 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Annotation; + +/** + * @Annotation + * @Target({"CLASS"}) + * + * @author Ryan Weaver + */ +class Service +{ + private $shared; + + private $public; + + private $synthetic; + + private $abstract; + + private $lazy; + + public function __construct(array $data) + { + foreach ($data as $key => $value) { + $method = 'set'.str_replace('_', '', $key); + if (!method_exists($this, $method)) { + throw new \BadMethodCallException(sprintf('Unknown property "%s" on annotation "%s".', $key, get_class($this))); + } + $this->$method($value); + } + } + + public function isShared() + { + return $this->shared; + } + + public function setShared($shared) + { + $this->shared = $shared; + } + + public function isPublic() + { + return $this->public; + } + + public function setPublic($public) + { + $this->public = $public; + } + + public function isSynthetic() + { + return $this->synthetic; + } + + public function setSynthetic($synthetic) + { + $this->synthetic = $synthetic; + } + + public function isAbstract() + { + return $this->abstract; + } + + public function setAbstract($abstract) + { + $this->abstract = $abstract; + } + + public function isLazy() + { + return $this->lazy; + } + + public function setLazy($lazy) + { + $this->lazy = $lazy; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php index a7c2fab884f67..cb8f6f67e7ec7 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/PassConfig.php @@ -54,6 +54,7 @@ public function __construct() new CheckDefinitionValidityPass(), new ResolveReferencesToAliasesPass(), new ResolveInvalidReferencesPass(), + new ServiceAnnotationsPass(), new AutowirePass(), new AnalyzeServiceReferencesPass(true), new CheckCircularReferencesPass(), diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php new file mode 100644 index 0000000000000..f447eae09d1cd --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php @@ -0,0 +1,182 @@ + + * + * 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 Doctrine\Common\Annotations\AnnotationReader; +use Symfony\Component\DependencyInjection\Argument\ClosureProxyArgument; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Annotation as Annotations; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ExpressionLanguage\Expression; + +/** + * @author Ryan Weaver + */ +class ServiceAnnotationsPass implements CompilerPassInterface +{ + /** + * @var AnnotationReader + */ + private $reader; + + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!class_exists(AnnotationReader::class)) { + return; + } + + $this->reader = new AnnotationReader(); + + $annotatedServiceIds = $container->findTaggedServiceIds('annotated'); + + foreach ($annotatedServiceIds as $annotatedServiceId => $params) { + $this->augmentServiceDefinition($container->getDefinition($annotatedServiceId)); + } + } + + private function augmentServiceDefinition(Definition $definition) + { + // 1) read class annotation for Definition + $reflectionClass = new \ReflectionClass($definition->getClass()); + /** @var Annotations\Service $definitionAnnotation */ + $definitionAnnotation = $this->reader->getClassAnnotation( + $reflectionClass, + Annotations\Service::class + ); + + if ($definitionAnnotation) { + if (null !== $definitionAnnotation->isShared()) { + $definition->setShared($definitionAnnotation->isShared()); + } + + if (null !== $definitionAnnotation->isPublic()) { + $definition->setPublic($definitionAnnotation->isPublic()); + } + + if (null !== $definitionAnnotation->isSynthetic()) { + $definition->setSynthetic($definitionAnnotation->isSynthetic()); + } + + if (null !== $definitionAnnotation->isAbstract()) { + $definition->setAbstract($definitionAnnotation->isAbstract()); + } + + if (null !== $definitionAnnotation->isLazy()) { + $definition->setLazy($definitionAnnotation->isLazy()); + } + + // todo - add support for the other Definition properties + } + + // 2) read Argument from __construct + if ($constructor = $reflectionClass->getConstructor()) { + $newArgs = $this->updateMethodArguments($definition, $constructor, $definition->getArguments()); + $definition->setArguments($newArgs); + } + } + + private function updateMethodArguments(Definition $definition, \ReflectionMethod $reflectionMethod, array $arguments) + { + $argAnnotations = $this->getArgumentAnnotationsForMethod($reflectionMethod); + $argumentIndexes = $this->getMethodArguments($reflectionMethod); + foreach ($argAnnotations as $arg) { + if (!isset($argumentIndexes[$arg->getName()])) { + throw new \InvalidArgumentException(sprintf('Invalid argument name "%s" used on the Argument annotation of %s::%s', $arg->getName(), $definition->getClass(), $reflectionMethod->getName())); + } + $key = $argumentIndexes[$arg->getName()]; + + $onInvalid = $arg->getOnInvalid(); + $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; + if ('ignore' === $onInvalid) { + $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; + } elseif ('null' === $onInvalid) { + $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; + } + + $type = $arg->getType(); + // if "id" is set, default "type" to service + if (!$type && $arg->getId()) { + $type = 'service'; + } + + switch ($type) { + case 'service': + $arguments[$key] = new Reference($arg->getId(), $invalidBehavior); + break; + case 'expression': + $arguments[$key] = new Expression($arg->getValue()); + break; + case 'closure-proxy': + $arguments[$key] = new ClosureProxyArgument($arg->getId(), $arg->getMethod(), $invalidBehavior); + break; + case 'collection': + // todo + break; + case 'iterator': + // todo + break; + case 'constant': + $arguments[$key] = constant(trim($arg->getValue())); + break; + default: + $arguments[$key] = $arg->getValue(); + } + } + + // it's possible index 1 was set, then index 0, then 2, etc + // make sure that we re-order so they're injected as expected + ksort($arguments); + + return $arguments; + } + + /** + * @param \ReflectionMethod $method + * + * @return Annotations\Argument[] + */ + private function getArgumentAnnotationsForMethod(\ReflectionMethod $method) + { + $annotations = $this->reader->getMethodAnnotations($method); + $argAnnotations = array(); + foreach ($annotations as $annotation) { + if ($annotation instanceof Annotations\Argument) { + $argAnnotations[] = $annotation; + } + } + + return $argAnnotations; + } + + /** + * Returns arguments to a method, where the key is the *name* + * of the argument and the value is its index. + * + * @param \ReflectionMethod $method + * + * @return array + */ + private function getMethodArguments(\ReflectionMethod $method) + { + $arguments = array(); + foreach ($method->getParameters() as $i => $parameter) { + $arguments[$parameter->getName()] = $i; + } + + return $arguments; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php new file mode 100644 index 0000000000000..ddfe7b9ce1732 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Compiler; + +use Symfony\Component\DependencyInjection\Annotation as DI; +use Symfony\Component\DependencyInjection\Compiler\ServiceAnnotationsPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @author Ryan Weaver + */ +class DefinitionAnnotationPassTest extends \PHPUnit_Framework_TestCase +{ + public function testProcessReadsServiceAnnotation() + { + $container = new ContainerBuilder(); + + $container->register(ClassWithManyServiceOptions::class, ClassWithManyServiceOptions::class) + ->addTag('annotated'); + + $pass = new ServiceAnnotationsPass(); + $pass->process($container); + + $definition = $container->getDefinition(ClassWithManyServiceOptions::class); + $this->assertFalse($definition->isShared()); + $this->assertFalse($definition->isPublic()); + $this->assertTrue($definition->isSynthetic()); + $this->assertTrue($definition->isAbstract()); + $this->assertTrue($definition->isLazy()); + } + + public function testNonTaggedClassesAreNotChanged() + { + $container = new ContainerBuilder(); + + // register the service, but don't mark it as annotated + $container->register(ClassWithManyServiceOptions::class, ClassWithManyServiceOptions::class) + // redundant, but here for clarity + ->setShared(true); + + $pass = new ServiceAnnotationsPass(); + $pass->process($container); + + $definition = $container->getDefinition(ClassWithManyServiceOptions::class); + // the annotation that sets shared to false is not ready! + $this->assertTrue($definition->isShared()); + } + + public function testBasicConstructorArgumentAnnotations() + { + $container = new ContainerBuilder(); + + $container->register(ClassWithConstructorArgAnnotations::class, ClassWithConstructorArgAnnotations::class) + ->addTag('annotated'); + + $pass = new ServiceAnnotationsPass(); + $pass->process($container); + + $definition = $container->getDefinition(ClassWithConstructorArgAnnotations::class); + $this->assertEquals(new Reference('foo_service'), $definition->getArgument(0)); + $this->assertEquals('%bar_parameter%', $definition->getArgument(1)); + $this->assertEquals('scalar value', $definition->getArgument(2)); + } +} + +/** + * @DI\Service( + * shared=false, + * public=false, + * synthetic=true, + * abstract=true, + * lazy=true + * ) + */ +class ClassWithManyServiceOptions +{ +} + +class ClassWithConstructorArgAnnotations +{ + /** + * Annotations are purposefully out of order! + * + * @DI\Argument(name="thirdArg", value="scalar value") + * @DI\Argument(name="firstArg", id="foo_service") + * @DI\Argument(name="secondArg", value="%bar_parameter%") + */ + public function __construct($firstArg, $secondArg, $thirdArg) + { + } +}