From 3154e1296258fdd42374f41467c5cff1dd0462b6 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sun, 22 Jan 2017 20:40:42 -0500 Subject: [PATCH 1/6] [WIP] Initial system for adding DI annotation support to classes --- .../Annotation/Argument.php | 104 ++++++++++ .../Annotation/Service.php | 92 +++++++++ .../Compiler/PassConfig.php | 1 + .../Compiler/ServiceAnnotationsPass.php | 183 ++++++++++++++++++ .../Compiler/DefinitionAnnotationPassTest.php | 101 ++++++++++ 5 files changed, 481 insertions(+) create mode 100644 src/Symfony/Component/DependencyInjection/Annotation/Argument.php create mode 100644 src/Symfony/Component/DependencyInjection/Annotation/Service.php create mode 100644 src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php diff --git a/src/Symfony/Component/DependencyInjection/Annotation/Argument.php b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php new file mode 100644 index 0000000000000..b9f14d24122a2 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php @@ -0,0 +1,104 @@ + + * + * 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) + { + $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..a3083dda83a27 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php @@ -0,0 +1,183 @@ + + * + * 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 = []; + 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 = []; + $i = 0; + foreach ($method->getParameters() as $parameter) { + $arguments[$parameter->getName()] = $i; + + $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..0ee585df6098e --- /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) + { + } +} \ No newline at end of file From f679a2ae47e5645b59b761d134f8cffcdd40bace Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Sun, 22 Jan 2017 21:04:35 -0500 Subject: [PATCH 2/6] Thanks fabbot! --- .../Compiler/ServiceAnnotationsPass.php | 8 +++++--- .../Tests/Compiler/DefinitionAnnotationPassTest.php | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php index a3083dda83a27..1b5a2242b8ddf 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php @@ -146,12 +146,13 @@ private function updateMethodArguments(Definition $definition, \ReflectionMethod /** * @param \ReflectionMethod $method + * * @return Annotations\Argument[] */ private function getArgumentAnnotationsForMethod(\ReflectionMethod $method) { $annotations = $this->reader->getMethodAnnotations($method); - $argAnnotations = []; + $argAnnotations = array(); foreach ($annotations as $annotation) { if ($annotation instanceof Annotations\Argument) { $argAnnotations[] = $annotation; @@ -166,16 +167,17 @@ private function getArgumentAnnotationsForMethod(\ReflectionMethod $method) * of the argument and the value is its index. * * @param \ReflectionMethod $method + * * @return array */ private function getMethodArguments(\ReflectionMethod $method) { - $arguments = []; + $arguments = array(); $i = 0; foreach ($method->getParameters() as $parameter) { $arguments[$parameter->getName()] = $i; - $i++; + ++$i; } return $arguments; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php index 0ee585df6098e..ddfe7b9ce1732 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DefinitionAnnotationPassTest.php @@ -98,4 +98,4 @@ class ClassWithConstructorArgAnnotations public function __construct($firstArg, $secondArg, $thirdArg) { } -} \ No newline at end of file +} From 5296f011bc9c8f893f8c78c8658484106f3d1926 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 23 Jan 2017 09:54:54 -0500 Subject: [PATCH 3/6] Fixing my reversed logic! --- .../DependencyInjection/Compiler/ServiceAnnotationsPass.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php index 1b5a2242b8ddf..0b522383a96e4 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php @@ -35,7 +35,7 @@ class ServiceAnnotationsPass implements CompilerPassInterface */ public function process(ContainerBuilder $container) { - if (class_exists(AnnotationReader::class)) { + if (!class_exists(AnnotationReader::class)) { return; } From c6ac8962ccf9d9325f6a919b6e7ad7cf4a3df7f2 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 23 Jan 2017 10:01:31 -0500 Subject: [PATCH 4/6] Simply using the internal index, which is equal --- .../DependencyInjection/Compiler/ServiceAnnotationsPass.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php index 0b522383a96e4..e1d34f3422ccc 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php @@ -173,11 +173,8 @@ private function getArgumentAnnotationsForMethod(\ReflectionMethod $method) private function getMethodArguments(\ReflectionMethod $method) { $arguments = array(); - $i = 0; - foreach ($method->getParameters() as $parameter) { + foreach ($method->getParameters() as $i => $parameter) { $arguments[$parameter->getName()] = $i; - - ++$i; } return $arguments; From f3da69a69f8f95032d9b9de72531ce22e106ca7e Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 23 Jan 2017 10:07:10 -0500 Subject: [PATCH 5/6] Adding strict checking, and clear error handling --- .../Component/DependencyInjection/Annotation/Argument.php | 5 +++++ .../DependencyInjection/Compiler/ServiceAnnotationsPass.php | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Symfony/Component/DependencyInjection/Annotation/Argument.php b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php index b9f14d24122a2..f8d0c72d79ab1 100644 --- a/src/Symfony/Component/DependencyInjection/Annotation/Argument.php +++ b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php @@ -89,6 +89,11 @@ public function getOnInvalid() 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; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php index e1d34f3422ccc..f447eae09d1cd 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ServiceAnnotationsPass.php @@ -101,9 +101,9 @@ private function updateMethodArguments(Definition $definition, \ReflectionMethod $onInvalid = $arg->getOnInvalid(); $invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE; - if ('ignore' == $onInvalid) { + if ('ignore' === $onInvalid) { $invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE; - } elseif ('null' == $onInvalid) { + } elseif ('null' === $onInvalid) { $invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE; } From ace5106e135c7c52e6a0550a2809b379115b8a08 Mon Sep 17 00:00:00 2001 From: Ryan Weaver Date: Mon, 23 Jan 2017 10:11:25 -0500 Subject: [PATCH 6/6] Thanks fabbot! --- .../Component/DependencyInjection/Annotation/Argument.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Symfony/Component/DependencyInjection/Annotation/Argument.php b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php index f8d0c72d79ab1..25fcf951594f6 100644 --- a/src/Symfony/Component/DependencyInjection/Annotation/Argument.php +++ b/src/Symfony/Component/DependencyInjection/Annotation/Argument.php @@ -92,7 +92,7 @@ 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; }