[DependencyInjection] Add #[Target]
to tell how a dependency is used and hint named autowiring aliases
This commit is contained in:
parent
501c3104c7
commit
cc76eab795
@ -81,8 +81,9 @@ EOF
|
||||
$serviceIds = array_filter($serviceIds, [$this, 'filterToServiceTypes']);
|
||||
|
||||
if ($search = $input->getArgument('search')) {
|
||||
$serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) {
|
||||
return false !== stripos(str_replace('\\', '', $serviceId), $search) && 0 !== strpos($serviceId, '.');
|
||||
$searchNormalized = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', '', $search);
|
||||
$serviceIds = array_filter($serviceIds, function ($serviceId) use ($searchNormalized) {
|
||||
return false !== stripos(str_replace('\\', '', $serviceId), $searchNormalized) && 0 !== strpos($serviceId, '.');
|
||||
});
|
||||
|
||||
if (empty($serviceIds)) {
|
||||
|
@ -0,0 +1,54 @@
|
||||
<?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\Attribute;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* An attribute to tell how a dependency is used and hint named autowiring aliases.
|
||||
*
|
||||
* @author Nicolas Grekas <p@tchwork.com>
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||
final class Target
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $name;
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name))));
|
||||
}
|
||||
|
||||
public static function parseName(\ReflectionParameter $parameter): string
|
||||
{
|
||||
if (80000 > \PHP_VERSION_ID || !$target = $parameter->getAttributes(self::class)[0] ?? null) {
|
||||
return $parameter->name;
|
||||
}
|
||||
|
||||
$name = $target->newInstance()->name;
|
||||
|
||||
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
|
||||
if (($function = $parameter->getDeclaringFunction()) instanceof \ReflectionMethod) {
|
||||
$function = $function->class.'::'.$function->name;
|
||||
} else {
|
||||
$function = $function->name;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('Invalid #[Target] name "%s" on parameter "$%s" of "%s()": the first character must be a letter.', $name, $parameter->name, $function));
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
@ -16,6 +16,7 @@ CHANGELOG
|
||||
* Add `env()` and `EnvConfigurator` in the PHP-DSL
|
||||
* Add support for `ConfigBuilder` in the `PhpFileLoader`
|
||||
* Add `ContainerConfigurator::env()` to get the current environment
|
||||
* Add `#[Target]` to tell how a dependency is used and hint named autowiring aliases
|
||||
|
||||
5.2.0
|
||||
-----
|
||||
|
@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\Attribute\TaggedIterator;
|
||||
use Symfony\Component\DependencyInjection\Attribute\TaggedLocator;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
|
||||
@ -252,7 +253,7 @@ class AutowirePass extends AbstractRecursivePass
|
||||
}
|
||||
|
||||
$getValue = function () use ($type, $parameter, $class, $method) {
|
||||
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, $parameter->name))) {
|
||||
if (!$value = $this->getAutowiredReference($ref = new TypedReference($type, $type, ContainerBuilder::EXCEPTION_ON_INVALID_REFERENCE, Target::parseName($parameter)))) {
|
||||
$failureMessage = $this->createTypeNotFoundMessageCallback($ref, sprintf('argument "$%s" of method "%s()"', $parameter->name, $class !== $this->currentId ? $class.'::'.$method : $method));
|
||||
|
||||
if ($parameter->isDefaultValueAvailable()) {
|
||||
|
@ -14,6 +14,7 @@ namespace Symfony\Component\DependencyInjection\Compiler;
|
||||
use Symfony\Component\DependencyInjection\Argument\BoundArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
@ -177,15 +178,16 @@ class ResolveBindingsPass extends AbstractRecursivePass
|
||||
}
|
||||
|
||||
$typeHint = ProxyHelper::getTypeHint($reflectionMethod, $parameter);
|
||||
$name = Target::parseName($parameter);
|
||||
|
||||
if (\array_key_exists($k = ltrim($typeHint, '\\').' $'.$parameter->name, $bindings)) {
|
||||
if (\array_key_exists($k = ltrim($typeHint, '\\').' $'.$name, $bindings)) {
|
||||
$arguments[$key] = $this->getBindingValue($bindings[$k]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (\array_key_exists('$'.$parameter->name, $bindings)) {
|
||||
$arguments[$key] = $this->getBindingValue($bindings['$'.$parameter->name]);
|
||||
if (\array_key_exists('$'.$name, $bindings)) {
|
||||
$arguments[$key] = $this->getBindingValue($bindings['$'.$name]);
|
||||
|
||||
continue;
|
||||
}
|
||||
@ -196,7 +198,7 @@ class ResolveBindingsPass extends AbstractRecursivePass
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($bindingNames[$parameter->name])) {
|
||||
if (isset($bindingNames[$name]) || isset($bindingNames[$parameter->name])) {
|
||||
$bindingKey = array_search($binding, $bindings, true);
|
||||
$argumentType = substr($bindingKey, 0, strpos($bindingKey, ' '));
|
||||
$this->errorMessages[] = sprintf('Did you forget to add the type "%s" to argument "$%s" of method "%s::%s()"?', $argumentType, $parameter->name, $reflectionMethod->class, $reflectionMethod->name);
|
||||
|
@ -27,6 +27,7 @@ use Symfony\Component\DependencyInjection\Argument\RewindableGenerator;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceLocator;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\Compiler\Compiler;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
@ -1341,7 +1342,7 @@ class ContainerBuilder extends Container implements TaggedContainerInterface
|
||||
*/
|
||||
public function registerAliasForArgument(string $id, string $type, string $name = null): Alias
|
||||
{
|
||||
$name = lcfirst(str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name ?? $id))));
|
||||
$name = (new Target($name ?? $id))->name;
|
||||
|
||||
if (!preg_match('/^[a-zA-Z_\x7f-\xff]/', $name)) {
|
||||
throw new InvalidArgumentException(sprintf('Invalid argument name "%s" for service "%s": the first character must be a letter.', $name, $id));
|
||||
|
@ -24,9 +24,11 @@ use Symfony\Component\DependencyInjection\Exception\AutowiringFailedException;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Loader\XmlFileLoader;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\FooVariadic;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\includes\MultipleArgumentsOptionalScalarNotReallyOptional;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
|
||||
use Symfony\Component\DependencyInjection\TypedReference;
|
||||
use Symfony\Contracts\Service\Attribute\Required;
|
||||
|
||||
@ -1068,4 +1070,21 @@ class AutowirePassTest extends TestCase
|
||||
];
|
||||
$this->assertEquals($expected, $container->getDefinition('setter_injection_collision')->getMethodCalls());
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 8
|
||||
*/
|
||||
public function testArgumentWithTarget()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$container->register(BarInterface::class, BarInterface::class);
|
||||
$container->register(BarInterface::class.' $imageStorage', BarInterface::class);
|
||||
$container->register('with_target', WithTarget::class)
|
||||
->setAutowired(true);
|
||||
|
||||
(new AutowirePass())->process($container);
|
||||
|
||||
$this->assertSame(BarInterface::class.' $imageStorage', (string) $container->getDefinition('with_target')->getArgument(0));
|
||||
}
|
||||
}
|
||||
|
@ -23,9 +23,11 @@ use Symfony\Component\DependencyInjection\Definition;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Exception\RuntimeException;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\ParentNotExists;
|
||||
use Symfony\Component\DependencyInjection\Tests\Fixtures\WithTarget;
|
||||
use Symfony\Component\DependencyInjection\TypedReference;
|
||||
|
||||
require_once __DIR__.'/../Fixtures/includes/autowiring_classes.php';
|
||||
@ -186,4 +188,19 @@ class ResolveBindingsPassTest extends TestCase
|
||||
$pass = new ResolveBindingsPass();
|
||||
$pass->process($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 8
|
||||
*/
|
||||
public function testBindWithTarget()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
|
||||
$container->register('with_target', WithTarget::class)
|
||||
->setBindings([BarInterface::class.' $imageStorage' => new Reference('bar')]);
|
||||
|
||||
(new ResolveBindingsPass())->process($container);
|
||||
|
||||
$this->assertSame('bar', (string) $container->getDefinition('with_target')->getArgument(0));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,23 @@
|
||||
<?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\Tests\Fixtures;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
|
||||
class WithTarget
|
||||
{
|
||||
public function __construct(
|
||||
#[Target('image.storage')]
|
||||
BarInterface $bar
|
||||
) {
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@
|
||||
namespace Symfony\Component\HttpKernel\DependencyInjection;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
|
||||
@ -148,7 +149,7 @@ class RegisterControllerArgumentLocatorsPass implements CompilerPassInterface
|
||||
} elseif ($p->allowsNull() && !$p->isOptional()) {
|
||||
$invalidBehavior = ContainerInterface::NULL_ON_INVALID_REFERENCE;
|
||||
}
|
||||
} elseif (isset($bindings[$bindingName = $type.' $'.$p->name]) || isset($bindings[$bindingName = '$'.$p->name]) || isset($bindings[$bindingName = $type])) {
|
||||
} elseif (isset($bindings[$bindingName = $type.' $'.$name = Target::parseName($p)]) || isset($bindings[$bindingName = '$'.$name]) || isset($bindings[$bindingName = $type])) {
|
||||
$binding = $bindings[$bindingName];
|
||||
|
||||
[$bindingValue, $bindingId, , $bindingType, $bindingFile] = $binding->getValues();
|
||||
|
@ -13,6 +13,7 @@ namespace Symfony\Component\HttpKernel\Tests\DependencyInjection;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Target;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
|
||||
@ -397,6 +398,27 @@ class RegisterControllerArgumentLocatorsPassTest extends TestCase
|
||||
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
|
||||
$this->assertSame([RegisterTestController::class.'::fooAction', 'foo::fooAction'], array_keys($locator));
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP 8
|
||||
*/
|
||||
public function testBindWithTarget()
|
||||
{
|
||||
$container = new ContainerBuilder();
|
||||
$resolver = $container->register('argument_resolver.service')->addArgument([]);
|
||||
|
||||
$container->register('foo', WithTarget::class)
|
||||
->setBindings(['string $someApiKey' => new Reference('the_api_key')])
|
||||
->addTag('controller.service_arguments');
|
||||
|
||||
(new RegisterControllerArgumentLocatorsPass())->process($container);
|
||||
|
||||
$locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0);
|
||||
$locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]);
|
||||
|
||||
$expected = ['apiKey' => new ServiceClosureArgument(new Reference('the_api_key'))];
|
||||
$this->assertEquals($expected, $locator->getArgument(0));
|
||||
}
|
||||
}
|
||||
|
||||
class RegisterTestController
|
||||
@ -458,3 +480,12 @@ class ArgumentWithoutTypeController
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
class WithTarget
|
||||
{
|
||||
public function fooAction(
|
||||
#[Target('some.api.key')]
|
||||
string $apiKey
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,7 @@
|
||||
"symfony/config": "^5.0",
|
||||
"symfony/console": "^4.4|^5.0",
|
||||
"symfony/css-selector": "^4.4|^5.0",
|
||||
"symfony/dependency-injection": "^5.1.8",
|
||||
"symfony/dependency-injection": "^5.3",
|
||||
"symfony/dom-crawler": "^4.4|^5.0",
|
||||
"symfony/expression-language": "^4.4|^5.0",
|
||||
"symfony/finder": "^4.4|^5.0",
|
||||
@ -53,7 +53,7 @@
|
||||
"symfony/config": "<5.0",
|
||||
"symfony/console": "<4.4",
|
||||
"symfony/form": "<5.0",
|
||||
"symfony/dependency-injection": "<5.1.8",
|
||||
"symfony/dependency-injection": "<5.3",
|
||||
"symfony/doctrine-bridge": "<5.0",
|
||||
"symfony/http-client": "<5.0",
|
||||
"symfony/mailer": "<5.0",
|
||||
|
Reference in New Issue
Block a user