From df11660015038945e33ecd679b8a053077a68c82 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Tue, 20 Feb 2024 14:38:14 -0500 Subject: [PATCH] [DependencyInjection] Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable --- .../Attribute/AutowireMethodOf.php | 38 +++++++++++++++++++ .../DependencyInjection/CHANGELOG.md | 1 + ...xceptionOnInvalidReferenceBehaviorPass.php | 4 +- .../Tests/Attribute/AutowireMethodOfTest.php | 34 +++++++++++++++++ .../ArgumentResolver/ServiceValueResolver.php | 6 ++- ...RegisterControllerArgumentLocatorsPass.php | 10 ++--- ...oveEmptyControllerArgumentLocatorsPass.php | 4 ++ .../ServiceValueResolverTest.php | 2 +- ...sterControllerArgumentLocatorsPassTest.php | 9 ++++- ...mptyControllerArgumentLocatorsPassTest.php | 13 ++++--- 10 files changed, 103 insertions(+), 18 deletions(-) create mode 100644 src/Symfony/Component/DependencyInjection/Attribute/AutowireMethodOf.php create mode 100644 src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireMethodOfTest.php diff --git a/src/Symfony/Component/DependencyInjection/Attribute/AutowireMethodOf.php b/src/Symfony/Component/DependencyInjection/Attribute/AutowireMethodOf.php new file mode 100644 index 0000000000000..4edcb8fc74ec5 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Attribute/AutowireMethodOf.php @@ -0,0 +1,38 @@ + + * + * 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\Definition; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Tells which method should be turned into a Closure based on the name of the parameter it's attached to. + */ +#[\Attribute(\Attribute::TARGET_PARAMETER)] +class AutowireMethodOf extends AutowireCallable +{ + /** + * @param string $service The service containing the method to autowire + * @param bool|class-string $lazy Whether to use lazy-loading for this argument + */ + public function __construct(string $service, bool|string $lazy = false) + { + parent::__construct([new Reference($service)], lazy: $lazy); + } + + public function buildDefinition(mixed $value, ?string $type, \ReflectionParameter $parameter): Definition + { + $value[1] = $parameter->name; + + return parent::buildDefinition($value, $type, $parameter); + } +} diff --git a/src/Symfony/Component/DependencyInjection/CHANGELOG.md b/src/Symfony/Component/DependencyInjection/CHANGELOG.md index 9e14c79fe0aec..a3f055088ec4d 100644 --- a/src/Symfony/Component/DependencyInjection/CHANGELOG.md +++ b/src/Symfony/Component/DependencyInjection/CHANGELOG.md @@ -8,6 +8,7 @@ CHANGELOG * Add argument `$prepend` to `ContainerConfigurator::extension()` to prepend the configuration instead of appending it * Have `ServiceLocator` implement `ServiceCollectionInterface` * Add `#[Lazy]` attribute as shortcut for `#[Autowire(lazy: [bool|string])]` and `#[Autoconfigure(lazy: [bool|string])]` + * Add `#[AutowireMethodOf]` attribute to autowire a method of a service as a callable 7.0 --- diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php index 72e2a366817ed..e81db66e3d3bb 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckExceptionOnInvalidReferenceBehaviorPass.php @@ -47,7 +47,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed if (!$value instanceof Reference) { return parent::processValue($value, $isRoot); } - if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $value->getInvalidBehavior() || $this->container->has($id = (string) $value)) { + if (ContainerInterface::EXCEPTION_ON_INVALID_REFERENCE < $value->getInvalidBehavior() || $this->container->has((string) $value)) { return $value; } @@ -83,7 +83,7 @@ protected function processValue(mixed $value, bool $isRoot = false): mixed $this->throwServiceNotFoundException($value, $currentId, $value); } - private function throwServiceNotFoundException(Reference $ref, string $sourceId, $value): void + private function throwServiceNotFoundException(Reference $ref, string $sourceId, mixed $value): void { $id = (string) $ref; $alternatives = []; diff --git a/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireMethodOfTest.php b/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireMethodOfTest.php new file mode 100644 index 0000000000000..dc744eca1b687 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Attribute/AutowireMethodOfTest.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Tests\Attribute; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\Attribute\AutowireMethodOf; +use Symfony\Component\DependencyInjection\Reference; + +class AutowireMethodOfTest extends TestCase +{ + public function testConstructor() + { + $a = new AutowireMethodOf('foo'); + + $this->assertEquals([new Reference('foo')], $a->value); + } + + public function testBuildDefinition(?\Closure $dummy = null) + { + $a = new AutowireMethodOf('foo'); + $r = new \ReflectionParameter([__CLASS__, __FUNCTION__], 0); + + $this->assertEquals([[new Reference('foo'), 'dummy']], $a->buildDefinition($a->value, 'Closure', $r)->getArguments()); + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php index 4ac10a45b0868..a7f61dbcc6581 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/ServiceValueResolver.php @@ -55,8 +55,10 @@ public function resolve(Request $request, ArgumentMetadata $argument): array try { return [$this->container->get($controller)->get($argument->getName())]; } catch (RuntimeException $e) { - $what = sprintf('argument $%s of "%s()"', $argument->getName(), $controller); - $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $e->getMessage()); + $what = 'argument $'.$argument->getName(); + $message = str_replace(sprintf('service "%s"', $argument->getName()), $what, $e->getMessage()); + $what .= sprintf(' of "%s()"', $controller); + $message = preg_replace('/service "\.service_locator\.[^"]++"/', $what, $message); if ($e->getMessage() === $message) { $message = sprintf('Cannot resolve %s: %s', $what, $message); diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php index ff15502ce74fa..2d68956c59de7 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RegisterControllerArgumentLocatorsPass.php @@ -123,6 +123,7 @@ public function process(ContainerBuilder $container): void // create a per-method map of argument-names to service/type-references $args = []; + $erroredIds = 0; foreach ($parameters as $p) { /** @var \ReflectionParameter $p */ $type = preg_replace('/(^|[(|&])\\\\/', '\1', $target = ltrim(ProxyHelper::exportType($p) ?? '', '?')); @@ -171,10 +172,8 @@ public function process(ContainerBuilder $container): void $value = $parameterBag->resolveValue($attribute->value); if ($attribute instanceof AutowireCallable) { - $value = $attribute->buildDefinition($value, $type, $p); - } - - if ($value instanceof Reference) { + $args[$p->name] = $attribute->buildDefinition($value, $type, $p); + } elseif ($value instanceof Reference) { $args[$p->name] = $type ? new TypedReference($value, $type, $invalidBehavior, $p->name) : new Reference($value, $invalidBehavior); } else { $args[$p->name] = new Reference('.value.'.$container->hash($value)); @@ -198,6 +197,7 @@ public function process(ContainerBuilder $container): void ->addError($message); $args[$p->name] = new Reference($erroredId, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE); + ++$erroredIds; } else { $target = preg_replace('/(^|[(|&])\\\\/', '\1', $target); $args[$p->name] = $type ? new TypedReference($target, $type, $invalidBehavior, Target::parseName($p)) : new Reference($target, $invalidBehavior); @@ -205,7 +205,7 @@ public function process(ContainerBuilder $container): void } // register the maps as a per-method service-locators if ($args) { - $controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args); + $controllers[$id.'::'.$r->name] = ServiceLocatorTagPass::register($container, $args, \count($args) !== $erroredIds ? $id.'::'.$r->name.'()' : null); foreach ($publicAliases[$id] ?? [] as $alias) { $controllers[$alias.'::'.$r->name] = clone $controllers[$id.'::'.$r->name]; diff --git a/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php b/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php index f9b16befbddb2..b2e7832e6e486 100644 --- a/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php +++ b/src/Symfony/Component/HttpKernel/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPass.php @@ -29,6 +29,10 @@ public function process(ContainerBuilder $container): void foreach ($controllers as $controller => $argumentRef) { $argumentLocator = $container->getDefinition((string) $argumentRef->getValues()[0]); + if ($argumentLocator->getFactory()) { + $argumentLocator = $container->getDefinition($argumentLocator->getFactory()[0]); + } + if (!$argumentLocator->getArgument(0)) { // remove empty argument locators $reason = sprintf('Removing service-argument resolver for controller "%s": no corresponding services exist for the referenced types.', $controller); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php index 59e81a9ae5ad0..a1a80fe82f2c2 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/ServiceValueResolverTest.php @@ -89,7 +89,7 @@ public function testControllerNameIsAnArray() public function testErrorIsTruncated() { $this->expectException(NearMissValueResolverException::class); - $this->expectExceptionMessage('Cannot autowire argument $dummy of "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.'); + $this->expectExceptionMessage('Cannot autowire argument $dummy required by "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyController::index()": it references class "Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\DummyService" but no such service exists.'); $container = new ContainerBuilder(); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php index c74338c081a9e..e34a808625831 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RegisterControllerArgumentLocatorsPassTest.php @@ -143,6 +143,7 @@ public function testAllActions() $this->assertInstanceof(ServiceClosureArgument::class, $locator['foo::fooAction']); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $this->assertSame(ServiceLocator::class, $locator->getClass()); $this->assertFalse($locator->isPublic()); @@ -166,6 +167,7 @@ public function testExplicitArgument() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::RUNTIME_EXCEPTION_ON_INVALID_REFERENCE))]; $this->assertEquals($expected, $locator->getArgument(0)); @@ -185,6 +187,7 @@ public function testOptionalArgument() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new TypedReference('bar', ControllerDummy::class, ContainerInterface::IGNORE_ON_INVALID_REFERENCE))]; $this->assertEquals($expected, $locator->getArgument(0)); @@ -306,8 +309,8 @@ public function testBindings($bindingName) $pass->process($container); $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); - $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = ['bar' => new ServiceClosureArgument(new Reference('foo'))]; $this->assertEquals($expected, $locator->getArgument(0)); @@ -372,7 +375,8 @@ public function testBindingsOnChildDefinitions() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertInstanceOf(ServiceClosureArgument::class, $locator['child::fooAction']); - $locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0])->getArgument(0); + $locator = $container->getDefinition((string) $locator['child::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0])->getArgument(0); $this->assertInstanceOf(ServiceClosureArgument::class, $locator['someArg']); $this->assertEquals(new Reference('parent'), $locator['someArg']->getValues()[0]); } @@ -439,6 +443,7 @@ public function testBindWithTarget() $locator = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $locator = $container->getDefinition((string) $locator['foo::fooAction']->getValues()[0]); + $locator = $container->getDefinition((string) $locator->getFactory()[0]); $expected = [ 'apiKey' => new ServiceClosureArgument(new Reference('the_api_key')), diff --git a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php index 8c99b882d32ca..21e0eb29ec08a 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DependencyInjection/RemoveEmptyControllerArgumentLocatorsPassTest.php @@ -35,22 +35,23 @@ public function testProcess() $pass->process($container); $controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); + $getLocator = fn ($controllers, $k) => $container->getDefinition((string) $container->getDefinition((string) $controllers[$k]->getValues()[0])->getFactory()[0])->getArgument(0); - $this->assertCount(2, $container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0)); - $this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0)); - $this->assertCount(1, $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0)); + $this->assertCount(2, $getLocator($controllers, 'c1::fooAction')); + $this->assertCount(1, $getLocator($controllers, 'c2::setTestCase')); + $this->assertCount(1, $getLocator($controllers, 'c2::fooAction')); (new ResolveInvalidReferencesPass())->process($container); - $this->assertCount(1, $container->getDefinition((string) $controllers['c2::setTestCase']->getValues()[0])->getArgument(0)); - $this->assertSame([], $container->getDefinition((string) $controllers['c2::fooAction']->getValues()[0])->getArgument(0)); + $this->assertCount(1, $getLocator($controllers, 'c2::setTestCase')); + $this->assertSame([], $getLocator($controllers, 'c2::fooAction')); (new RemoveEmptyControllerArgumentLocatorsPass())->process($container); $controllers = $container->getDefinition((string) $resolver->getArgument(0))->getArgument(0); $this->assertSame(['c1::fooAction', 'c1:fooAction'], array_keys($controllers)); - $this->assertSame(['bar'], array_keys($container->getDefinition((string) $controllers['c1::fooAction']->getValues()[0])->getArgument(0))); + $this->assertSame(['bar'], array_keys($getLocator($controllers, 'c1::fooAction'))); $expectedLog = [ 'Symfony\Component\HttpKernel\DependencyInjection\RemoveEmptyControllerArgumentLocatorsPass: Removing service-argument resolver for controller "c2::fooAction": no corresponding services exist for the referenced types.', 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