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; } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy