Skip to content

[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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions src/Symfony/Component/DependencyInjection/Annotation/Argument.php
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;
}
}
92 changes: 92 additions & 0 deletions src/Symfony/Component/DependencyInjection/Annotation/Service.php
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;
Copy link
Member

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?

Copy link
Member Author

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 :)


private $public;

private $synthetic;

private $abstract;

private $lazy;

public function __construct(array $data)
Copy link
Member

Choose a reason for hiding this comment

The 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
Expand Up @@ -54,6 +54,7 @@ public function __construct()
new CheckDefinitionValidityPass(),
new ResolveReferencesToAliasesPass(),
new ResolveInvalidReferencesPass(),
new ServiceAnnotationsPass(),
new AutowirePass(),
new AnalyzeServiceReferencesPass(true),
new CheckCircularReferencesPass(),
Expand Down
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();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about using $container->get('annotation_reader') instead? this service is already used at compile time by some other extensions.

Copy link
Member

Choose a reason for hiding this comment

The 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)

Copy link
Member

@nicolas-grekas nicolas-grekas Jan 24, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I more than remember since I worked on that again in #21381.
Yet it allowed to fix a core issue, which is that there should be no cache enabled at build time.
Thus, using annotation_reader could be also seen as a way for us to support using it at compile time a first class citizen - which we have to anyway, as history proved us.


$annotatedServiceIds = $container->findTaggedServiceIds('annotated');

foreach ($annotatedServiceIds as $annotatedServiceId => $params) {
$this->augmentServiceDefinition($container->getDefinition($annotatedServiceId));
}
}

private function augmentServiceDefinition(Definition $definition)
Copy link
Member

Choose a reason for hiding this comment

The 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;
}
}
Loading