-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[WIP] Annotation support for Services #21376
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3154e12
f679a2a
5296f01
c6ac896
f3da69a
ace5106
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* 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 <ryan@knpuniversity.com> | ||
*/ | ||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* 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 <ryan@knpuniversity.com> | ||
*/ | ||
class Service | ||
{ | ||
private $shared; | ||
|
||
private $public; | ||
|
||
private $synthetic; | ||
|
||
private $abstract; | ||
|
||
private $lazy; | ||
|
||
public function __construct(array $data) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not removing the constructor and using public properties, letting the annotation parser validate properties itself ? This is the recommended usage of doctrine/annotations when annnotation classes don't need to be reused for other purposes (which is why we don't do in the validator component: constraints themselves are used as annotations, but they are not only annotations) |
||
{ | ||
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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the Symfony package. | ||
* | ||
* (c) Fabien Potencier <fabien@symfony.com> | ||
* | ||
* 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 <ryan@knpuniversity.com> | ||
*/ | ||
class ServiceAnnotationsPass implements CompilerPassInterface | ||
{ | ||
/** | ||
* @var AnnotationReader | ||
*/ | ||
private $reader; | ||
|
||
/** | ||
* {@inheritdoc} | ||
*/ | ||
public function process(ContainerBuilder $container) | ||
{ | ||
if (!class_exists(AnnotationReader::class)) { | ||
return; | ||
} | ||
|
||
$this->reader = new AnnotationReader(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what about using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and it already causes issues regarding incomplete service configuration (remember the burden when switching the cache configuration in 3.2 ?). We always documented that getting service instances during a compiler pass is an unsupported usage of the container (and so you are on your own and it may break in weird ways), so we should not do it in Symfony itself (especially when it can still break because of a change done by another bundle) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I more than remember since I worked on that again in #21381. |
||
|
||
$annotatedServiceIds = $container->findTaggedServiceIds('annotated'); | ||
|
||
foreach ($annotatedServiceIds as $annotatedServiceId => $params) { | ||
$this->augmentServiceDefinition($container->getDefinition($annotatedServiceId)); | ||
} | ||
} | ||
|
||
private function augmentServiceDefinition(Definition $definition) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It should read recursively annotations from parent classes and implemented interfaces. |
||
{ | ||
// 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; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add
autowired
too?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely - it's one of my todos :)