Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

[FrameworkBundle] Add new "controller.service_arguments" tag to inject services into actions #21771

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

Merged
merged 1 commit into from
Mar 22, 2017
Merged
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
1 change: 1 addition & 0 deletions 1 src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ CHANGELOG
3.3.0
-----

* Added support for the `controller.service_arguments` tag, for injecting services into controllers' actions
* Deprecated `cache:clear` with warmup (always call it with `--no-warmup`)
* Changed default configuration for
assets/forms/validation/translation/serialization/csrf from `canBeEnabled()` to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class UnusedTagsPass implements CompilerPassInterface
'console.command',
'container.service_locator',
'container.service_subscriber',
'controller.service_arguments',
'config_cache.resource_checker',
'data_collector',
'form.type',
Expand Down
4 changes: 4 additions & 0 deletions 4 src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
use Symfony\Component\Config\DependencyInjection\ConfigCachePass;
use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass;
use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass;
use Symfony\Component\HttpKernel\DependencyInjection\RegisterControllerArgumentLocatorsPass;
use Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass;
use Symfony\Component\PropertyInfo\DependencyInjection\PropertyInfoPass;
use Symfony\Component\Routing\DependencyInjection\RoutingResolverPass;
use Symfony\Component\Serializer\DependencyInjection\SerializerPass;
Expand Down Expand Up @@ -76,6 +78,8 @@ public function build(ContainerBuilder $container)
{
parent::build($container);

$container->addCompilerPass(new RegisterControllerArgumentLocatorsPass());
$container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING);
$container->addCompilerPass(new RoutingResolverPass());
$container->addCompilerPass(new ProfilerPass());
// must be registered before removing private services as some might be listeners/subscribers
Expand Down
5 changes: 5 additions & 0 deletions 5 src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@
<tag name="controller.argument_value_resolver" priority="50" />
</service>

<service id="argument_resolver.service" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\ServiceValueResolver" public="false">
<tag name="controller.argument_value_resolver" priority="-50" />
<argument />
</service>

<service id="argument_resolver.default" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver" public="false">
<tag name="controller.argument_value_resolver" priority="-100" />
</service>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?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\HttpKernel\Controller\ArgumentResolver;

use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;

/**
* Yields a service keyed by _controller and argument name.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class ServiceValueResolver implements ArgumentValueResolverInterface
{
private $container;

public function __construct(ContainerInterface $container)
{
$this->container = $container;
}

/**
* {@inheritdoc}
*/
public function supports(Request $request, ArgumentMetadata $argument)
{
return is_string($controller = $request->attributes->get('_controller')) && $this->container->has($controller) && $this->container->get($controller)->has($argument->getName());
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any way to optimize the calls? AFAIU $request->attributes->get('_controller'), $this->container->has($controller), $this->container->get($controller)->has($argument->getName() will be called for every arguments of the controller although we could keep a map of controllers by request that can be reused even in resolve(), wdyt?

Copy link
Member Author

@nicolas-grekas nicolas-grekas Mar 8, 2017

Choose a reason for hiding this comment

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

The calls resolve to immediate isset checks, cannot be faster

Copy link
Contributor

@linaori linaori Mar 9, 2017

Choose a reason for hiding this comment

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

What if I would use the other notation here? Foo::bar, with controller as a service, this would be supported.

Copy link
Member Author

Choose a reason for hiding this comment

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

@iltar it will work: both variants are now registered as references when id==class

Copy link
Contributor

Choose a reason for hiding this comment

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

}

/**
* {@inheritdoc}
*/
public function resolve(Request $request, ArgumentMetadata $argument)
{
yield $this->container->get($request->attributes->get('_controller'))->get($argument->getName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
<?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\HttpKernel\DependencyInjection;

use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\DependencyInjection\LazyProxy\InheritanceProxyHelper;
use Symfony\Component\DependencyInjection\Reference;
use Symfony\Component\DependencyInjection\ServiceLocator;
use Symfony\Component\DependencyInjection\TypedReference;

/**
* Creates the service-locators required by ServiceArgumentValueResolver.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
{
private $resolverServiceId;
private $controllerTag;

public function __construct($resolverServiceId = 'argument_resolver.service', $controllerTag = 'controller.service_arguments')
{
$this->resolverServiceId = $resolverServiceId;
$this->controllerTag = $controllerTag;
}

public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition($this->resolverServiceId)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not the other way around? If some tags are found then register the resolver.

Because currently the resolver is asked to resolve before DefaultValueResolver even if there is no controller tagged.

Copy link
Member Author

@nicolas-grekas nicolas-grekas Apr 10, 2017

Choose a reason for hiding this comment

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

I'd say because "less dynamic" is better. The extra check you're talking about is not a perf issue: it is really fast - and happens once per action (you don't call that many actions for constructing one response, do you?)

Copy link
Contributor

Choose a reason for hiding this comment

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

and happens once per action

Isn't it once per argument of an action, and sub-action(s)? Although I'm fine with it.

return;
}

$parameterBag = $container->getParameterBag();
$controllers = array();

foreach ($container->findTaggedServiceIds($this->controllerTag) as $id => $tags) {
$def = $container->getDefinition($id);

if ($def->isAbstract()) {
continue;
}
$class = $def->getClass();
$isAutowired = $def->isAutowired();

// resolve service class, taking parent definitions into account
while (!$class && $def instanceof ChildDefinition) {
$def = $container->findDefinition($def->getParent());
$class = $def->getClass();
}
$class = $parameterBag->resolveValue($class);

if (!$r = $container->getReflectionClass($class)) {
throw new InvalidArgumentException(sprintf('Class "%s" used for service "%s" cannot be found.', $class, $id));
}

// get regular public methods
$methods = array();
$arguments = array();
foreach ($r->getMethods(\ReflectionMethod::IS_PUBLIC) as $r) {
if (!$r->isConstructor() && !$r->isDestructor() && !$r->isAbstract()) {
$methods[strtolower($r->name)] = array($r, $r->getParameters());
}
}

// validate and collect explicit per-actions and per-arguments service references
foreach ($tags as $attributes) {
if (!isset($attributes['action']) && !isset($attributes['argument']) && !isset($attributes['id'])) {
continue;
}
foreach (array('action', 'argument', 'id') as $k) {
if (!isset($attributes[$k][0])) {
throw new InvalidArgumentException(sprintf('Missing "%s" attribute on tag "%s" %s for service "%s".', $k, $this->controllerTag, json_encode($attributes, JSON_UNESCAPED_UNICODE), $id));
}
}
if (!isset($methods[$action = strtolower($attributes['action'])])) {
throw new InvalidArgumentException(sprintf('Invalid "action" attribute on tag "%s" for service "%s": no public "%s()" method found on class "%s".', $this->controllerTag, $id, $attributes['action'], $class));
}
list($r, $parameters) = $methods[$action];
$found = false;

foreach ($parameters as $p) {
if ($attributes['argument'] === $p->name) {
if (!isset($arguments[$r->name][$p->name])) {
$arguments[$r->name][$p->name] = $attributes['id'];
}
$found = true;
break;
}
}

if (!$found) {
throw new InvalidArgumentException(sprintf('Invalid "%s" tag for service "%s": method "%s()" has no "%s" argument on class "%s".', $this->controllerTag, $id, $r->name, $attributes['argument'], $class));
}
}

foreach ($methods as list($r, $parameters)) {
// create a per-method map of argument-names to service/type-references
$args = array();
foreach ($parameters as $p) {
$type = $target = InheritanceProxyHelper::getTypeHint($r, $p, true);
$invalidBehavior = ContainerInterface::IGNORE_ON_INVALID_REFERENCE;

if (isset($arguments[$r->name][$p->name])) {
$target = $arguments[$r->name][$p->name];
if ('?' !== $target[0]) {
$invalidBehavior = ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE;
} elseif ('' === $target = (string) substr($target, 1)) {
throw new InvalidArgumentException(sprintf('A "%s" tag must have non-empty "id" attributes for service "%s".', $this->controllerTag, $id));
} elseif ($p->allowsNull() && !$p->isOptional()) {
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
}
} elseif (!$type) {
continue;
}

$args[$p->name] = new ServiceClosureArgument($type ? new TypedReference($target, $type, $invalidBehavior, false) : new Reference($target, $invalidBehavior));
}
// register the maps as a per-method service-locators
if ($args) {
$argsId = sprintf('arguments.%s:%s', $id, $r->name);
$container->register($argsId, ServiceLocator::class)
->addArgument($args)
->setPublic(false)
->setAutowired($isAutowired)
->addTag('controller.arguments_locator', array($class, $id, $r->name));
$controllers[$id.':'.$r->name] = new ServiceClosureArgument(new Reference($argsId));
if ($id === $class) {
$controllers[$id.'::'.$r->name] = new ServiceClosureArgument(new Reference($argsId));
}
}
}
}

$container->getDefinition($this->resolverServiceId)
->replaceArgument(0, (new Definition(ServiceLocator::class, array($controllers)))->addTag('container.service_locator'));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?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\HttpKernel\DependencyInjection;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
* Removes empty service-locators registered for ServiceArgumentValueResolver.
*
* @author Nicolas Grekas <p@tchwork.com>
*/
class RemoveEmptyControllerArgumentLocatorsPass implements CompilerPassInterface
{
private $resolverServiceId;

public function __construct($resolverServiceId = 'argument_resolver.service')
{
$this->resolverServiceId = $resolverServiceId;
}

public function process(ContainerBuilder $container)
{
if (false === $container->hasDefinition($this->resolverServiceId)) {
return;
}

$serviceResolver = $container->getDefinition($this->resolverServiceId);
$controllers = $serviceResolver->getArgument(0)->getArgument(0);

foreach ($container->findTaggedServiceIds('controller.arguments_locator') as $id => $tags) {
$argumentLocator = $container->getDefinition($id)->clearTag('controller.arguments_locator');
list($class, $service, $action) = $tags[0];

if (!$argumentLocator->getArgument(0)) {
// remove empty argument locators
$reason = sprintf('Removing service-argument-resolver for controller "%s:%s": no corresponding definitions were found for the referenced services/types.%s', $service, $action, !$argumentLocator->isAutowired() ? ' Did you forget to enable autowiring?' : '');
} else {
// any methods listed for call-at-instantiation cannot be actions
$reason = false;
foreach ($container->getDefinition($service)->getMethodCalls() as list($method, $args)) {
if (0 === strcasecmp($action, $method)) {
$reason = sprintf('Removing method "%s" of service "%s" from controller candidates: the method is called at instantiation, thus cannot be an action.', $action, $service);
break;
}
}
if (!$reason) {
continue;
}
}

$container->removeDefinition($id);
unset($controllers[$service.':'.$action]);
if ($service === $class) {
unset($controllers[$service.'::'.$action]);
}
$container->log($this, $reason);
}

$serviceResolver->getArgument(0)->replaceArgument(0, $controllers);
}
}
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.