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) + { + } +} 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