From 1c209e7d167bc6778b2256028d99c4c648242e83 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Sat, 18 Jul 2026 16:47:58 -0400 Subject: [PATCH 1/8] feat(dictionary): add named autowiring aliases Closes #154 --- README.md | 65 +++++++++-- .../Compiler/DictionaryBuildingPassSpec.php | 101 ++++++++++++++++++ .../Compiler/DictionaryBuildingPass.php | 36 ++++++- 3 files changed, 192 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index fd901b1c..8920bb4a 100644 --- a/README.md +++ b/README.md @@ -41,22 +41,69 @@ Define dictionaries in your config.yml file: ```yaml knp_dictionary: dictionaries: - my_dictionary: # your dictionary name - - Foo # your dictionary content - - Bar - - Baz + civility: + - Mr + - Ms +``` + +Configured dictionaries can be injected by type-hinting `Dictionary` and naming +the argument after the dictionary followed by `Dictionary`. Dictionary names are +normalized to camel case, so `entity_class_icons` maps to +`$entityClassIconsDictionary`. + +```php +use Knp\DictionaryBundle\Dictionary; + +final class UserManager +{ + public function __construct( + private Dictionary $civilityDictionary, + ) {} +} ``` -You will be able to retrieve it by injecting the Collection service and accessing the dictionary by its key +Use Symfony's `Target` attribute when the argument needs a different name: ```php +use Knp\DictionaryBundle\Dictionary; +use Symfony\Component\DependencyInjection\Attribute\Target; - private Dictionary $myDictionary; +final class UserManager +{ public function __construct( - \Knp\DictionaryBundle\Dictionary\Collection $dictionaries) + #[Target('civility.dictionary')] + private Dictionary $dictionary, + ) {} +} +``` + +Names that cannot become valid PHP argument names, or names that become +ambiguous after normalization (for example, `foo-bar` and `foo_bar`), do not +receive an automatic autowiring alias. They can be wired explicitly: + +```yaml +services: + App\Service\MyService: + arguments: + $dictionary: '@knp_dictionary.dictionary.foo-bar' +``` + +You can also inject the collection when the dictionary must be selected +dynamically or cannot be autowired by name: + +```php +use Knp\DictionaryBundle\Dictionary; +use Knp\DictionaryBundle\Dictionary\Collection; + +final class UserManager +{ + private Dictionary $dictionary; + + public function __construct(Collection $dictionaries) { - $this->myDictionary = $dictionaries['my_dictionary']; + $this->dictionary = $dictionaries['civility']; } +} ``` ## Dictionary form type @@ -70,7 +117,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('civility', DictionaryType::class, array( - 'name' => 'my_dictionary' + 'name' => 'civility' )) ; } diff --git a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php index 87b7f700..62fdf624 100644 --- a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php +++ b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php @@ -8,8 +8,11 @@ use Knp\DictionaryBundle\DependencyInjection\Compiler\DictionaryRegistrationPass; use Knp\DictionaryBundle\Dictionary; use Knp\DictionaryBundle\Dictionary\Factory\Aggregate; +use Knp\DictionaryBundle\Dictionary\Simple; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ContainerBuilder; use Webmozart\Assert\Assert; @@ -62,6 +65,7 @@ function it_builds_a_value_as_key_dictionary_from_the_config(ContainerBuilder $c return true; }) )->shouldBeCalled(); + $this->expectDico1AliasRegistration($container); $this->process($container); } @@ -105,6 +109,7 @@ function it_builds_a_value_dictionary_from_the_config(ContainerBuilder $containe return true; }) )->shouldBeCalled(); + $this->expectDico1AliasRegistration($container); $this->process($container); } @@ -148,7 +153,103 @@ function it_builds_a_key_value_dictionary_from_the_config(ContainerBuilder $cont return true; }) )->shouldBeCalled(); + $this->expectDico1AliasRegistration($container); $this->process($container); } + + function it_autowires_configured_dictionaries_by_name() + { + $container = new ContainerBuilder(); + $container->setParameter('knp_dictionary.configuration', [ + 'dictionaries' => [ + 'vote' => [ + 'type' => Dictionary::VALUE, + 'content' => ['yes', 'no'], + ], + 'entity_class_icons' => [ + 'type' => Dictionary::VALUE, + 'content' => ['user', 'group'], + ], + ], + ]); + $container->register(Aggregate::class, DictionaryFactoryStub::class); + $container + ->register(DictionaryConsumer::class, DictionaryConsumer::class) + ->setAutowired(true) + ->setPublic(true) + ; + $this->process($container); + $container->compile(); + + $consumer = $container->get(DictionaryConsumer::class); + Assert::isInstanceOf($consumer, DictionaryConsumer::class); + Assert::same($consumer->voteDictionary->getName(), 'vote'); + Assert::same($consumer->entityClassIconsDictionary->getName(), 'entity_class_icons'); + Assert::same($consumer->dictionary->getName(), 'vote'); + } + + function it_keeps_invalid_and_ambiguous_dictionary_names_out_of_named_autowiring() + { + $container = new ContainerBuilder(); + $container->setParameter('knp_dictionary.configuration', [ + 'dictionaries' => [ + '123_status' => [ + 'type' => Dictionary::VALUE, + 'content' => ['draft'], + ], + 'foo-bar' => [ + 'type' => Dictionary::VALUE, + 'content' => ['foo'], + ], + 'foo_bar' => [ + 'type' => Dictionary::VALUE, + 'content' => ['bar'], + ], + ], + ]); + + $this->process($container); + + Assert::true($container->hasDefinition('knp_dictionary.dictionary.123_status')); + Assert::true($container->hasDefinition('knp_dictionary.dictionary.foo-bar')); + Assert::true($container->hasDefinition('knp_dictionary.dictionary.foo_bar')); + Assert::false($container->hasAlias(Dictionary::class.' $123StatusDictionary')); + Assert::false($container->hasAlias(Dictionary::class.' $fooBarDictionary')); + } + + private function expectDico1AliasRegistration(ContainerBuilder $container): void + { + $container->hasAlias(Dictionary::class.' $dico1Dictionary')->willReturn(false); + $container + ->registerAliasForArgument( + 'knp_dictionary.dictionary.dico1', + Dictionary::class, + 'dico1.dictionary' + ) + ->shouldBeCalled() + ->willReturn(new Alias('knp_dictionary.dictionary.dico1')) + ; + } +} + +final class DictionaryFactoryStub +{ + /** + * @param mixed[] $config + */ + public function create(string $name, array $config): Dictionary + { + return new Simple($name, $config['content']); + } +} + +final class DictionaryConsumer +{ + public function __construct( + public readonly Dictionary $voteDictionary, + public readonly Dictionary $entityClassIconsDictionary, + #[Target('vote.dictionary')] + public readonly Dictionary $dictionary, + ) {} } diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php index 527812de..938614c8 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php @@ -21,14 +21,48 @@ public function process(ContainerBuilder $containerBuilder): void throw new \Exception('The configuration "knp_dictionary.dictionaries" should be an array.'); } + /** @var array> $aliases */ + $aliases = []; + foreach ($configuration['dictionaries'] as $name => $config) { + $serviceId = \sprintf('knp_dictionary.dictionary.%s', $name); + $containerBuilder->setDefinition( - \sprintf('knp_dictionary.dictionary.%s', $name), + $serviceId, $this->createDefinition($name, $config) ); + + if (null !== $argumentName = $this->normalizeArgumentName($name)) { + $aliases[$argumentName][] = [$serviceId, $name.'.dictionary']; + } + } + + foreach ($aliases as $argumentName => $candidates) { + if (1 !== \count($candidates)) { + continue; + } + + if ($containerBuilder->hasAlias(Dictionary::class.' $'.$argumentName)) { + continue; + } + + $containerBuilder->registerAliasForArgument($candidates[0][0], Dictionary::class, $candidates[0][1]); } } + private function normalizeArgumentName(string $name): ?string + { + $words = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name.'.dictionary'); + + if (null === $words) { + return null; + } + + $argumentName = lcfirst(str_replace(' ', '', ucwords($words))); + + return 1 === preg_match('/^[a-zA-Z_\x7f-\xff]/', $argumentName) ? $argumentName : null; + } + /** * @param mixed[] $config */ From 93141673a1f9c03d7a524748d123fa4298d2cf61 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Sat, 18 Jul 2026 17:03:56 -0400 Subject: [PATCH 2/8] refactor(dictionary): simplify named autowiring --- README.md | 21 ++++++++++--------- .../Compiler/DictionaryBuildingPass.php | 6 +----- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 8920bb4a..2a581578 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,9 @@ knp_dictionary: - Ms ``` -Configured dictionaries can be injected by type-hinting `Dictionary` and naming -the argument after the dictionary followed by `Dictionary`. Dictionary names are -normalized to camel case, so `entity_class_icons` maps to +To inject a configured dictionary directly, type-hint `Dictionary` and name the +argument `Dictionary`. Names are normalized to camel case, so +`entity_class_icons` maps to `$entityClassIconsDictionary`. ```php @@ -62,7 +62,8 @@ final class UserManager } ``` -Use Symfony's `Target` attribute when the argument needs a different name: +To use a different argument name, select the dictionary with Symfony's `Target` +attribute: ```php use Knp\DictionaryBundle\Dictionary; @@ -77,9 +78,9 @@ final class UserManager } ``` -Names that cannot become valid PHP argument names, or names that become -ambiguous after normalization (for example, `foo-bar` and `foo_bar`), do not -receive an automatic autowiring alias. They can be wired explicitly: +Names that are invalid PHP argument names after normalization, or names that +normalize to the same argument name (for example, `foo-bar` and `foo_bar`), do +not receive an automatic autowiring alias. Wire them explicitly: ```yaml services: @@ -88,8 +89,8 @@ services: $dictionary: '@knp_dictionary.dictionary.foo-bar' ``` -You can also inject the collection when the dictionary must be selected -dynamically or cannot be autowired by name: +Inject the collection instead when selecting a dictionary dynamically or when +named autowiring is unavailable: ```php use Knp\DictionaryBundle\Dictionary; @@ -135,7 +136,7 @@ use Knp\DictionaryBundle\Validator\Constraints\Dictionary; class User { #[ORM\Column] - #[Dictionary(name: 'my_dictionary')] + #[Dictionary(name: 'civility')] private $civility; } ``` diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php index 938614c8..89c7c315 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php @@ -52,11 +52,7 @@ public function process(ContainerBuilder $containerBuilder): void private function normalizeArgumentName(string $name): ?string { - $words = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name.'.dictionary'); - - if (null === $words) { - return null; - } + $words = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name.'.dictionary') ?? ''; $argumentName = lcfirst(str_replace(' ', '', ucwords($words))); From d4850cfa8b50059d2faab3a0e7ab34a6d978df05 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Sat, 18 Jul 2026 17:12:36 -0400 Subject: [PATCH 3/8] test(dictionary): cover existing alias precedence --- README.md | 3 +++ .../Compiler/DictionaryBuildingPassSpec.php | 25 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/README.md b/README.md index 2a581578..3b42ec70 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,9 @@ final class UserManager } ``` +An existing named `Dictionary` autowiring alias takes precedence and is not +replaced by a configured dictionary. + To use a different argument name, select the dictionary with Symfony's `Target` attribute: diff --git a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php index 62fdf624..7a63ea34 100644 --- a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php +++ b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php @@ -218,6 +218,31 @@ function it_keeps_invalid_and_ambiguous_dictionary_names_out_of_named_autowiring Assert::false($container->hasAlias(Dictionary::class.' $fooBarDictionary')); } + function it_preserves_existing_named_autowiring_aliases() + { + $container = new ContainerBuilder(); + $container->setParameter('knp_dictionary.configuration', [ + 'dictionaries' => [ + 'vote' => [ + 'type' => Dictionary::VALUE, + 'content' => ['yes', 'no'], + ], + ], + ]); + $container + ->register('app.vote_dictionary', Simple::class) + ->setArguments(['custom_vote', ['custom']]) + ; + $container->setAlias(Dictionary::class.' $voteDictionary', 'app.vote_dictionary'); + + $this->process($container); + + Assert::same( + (string) $container->getAlias(Dictionary::class.' $voteDictionary'), + 'app.vote_dictionary' + ); + } + private function expectDico1AliasRegistration(ContainerBuilder $container): void { $container->hasAlias(Dictionary::class.' $dico1Dictionary')->willReturn(false); From 5f5fd8bdf1ec44b3a6115f8f758261547a9d54d0 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Sat, 18 Jul 2026 17:47:25 -0400 Subject: [PATCH 4/8] fix(dictionary): resolve named aliases through collection --- .../Compiler/DictionaryBuildingPassSpec.php | 50 +++++++++---------- .../Compiler/DictionaryBuildingPass.php | 18 ++++++- 2 files changed, 41 insertions(+), 27 deletions(-) diff --git a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php index 7a63ea34..ac4b2546 100644 --- a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php +++ b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php @@ -6,9 +6,12 @@ use Knp\DictionaryBundle\DependencyInjection\Compiler\DictionaryBuildingPass; use Knp\DictionaryBundle\DependencyInjection\Compiler\DictionaryRegistrationPass; +use Knp\DictionaryBundle\DependencyInjection\KnpDictionaryExtension; use Knp\DictionaryBundle\Dictionary; +use Knp\DictionaryBundle\Dictionary\Collection; use Knp\DictionaryBundle\Dictionary\Factory\Aggregate; use Knp\DictionaryBundle\Dictionary\Simple; +use Knp\DictionaryBundle\KnpDictionaryBundle; use PhpSpec\ObjectBehavior; use Prophecy\Argument; use Symfony\Component\DependencyInjection\Alias; @@ -161,25 +164,18 @@ function it_builds_a_key_value_dictionary_from_the_config(ContainerBuilder $cont function it_autowires_configured_dictionaries_by_name() { $container = new ContainerBuilder(); - $container->setParameter('knp_dictionary.configuration', [ + (new KnpDictionaryExtension())->load([[ 'dictionaries' => [ - 'vote' => [ - 'type' => Dictionary::VALUE, - 'content' => ['yes', 'no'], - ], - 'entity_class_icons' => [ - 'type' => Dictionary::VALUE, - 'content' => ['user', 'group'], - ], + 'vote' => ['yes', 'no'], + 'entity_class_icons' => ['user', 'group'], ], - ]); - $container->register(Aggregate::class, DictionaryFactoryStub::class); + ]], $container); + (new KnpDictionaryBundle())->build($container); $container ->register(DictionaryConsumer::class, DictionaryConsumer::class) ->setAutowired(true) ->setPublic(true) ; - $this->process($container); $container->compile(); $consumer = $container->get(DictionaryConsumer::class); @@ -216,6 +212,9 @@ function it_keeps_invalid_and_ambiguous_dictionary_names_out_of_named_autowiring Assert::true($container->hasDefinition('knp_dictionary.dictionary.foo_bar')); Assert::false($container->hasAlias(Dictionary::class.' $123StatusDictionary')); Assert::false($container->hasAlias(Dictionary::class.' $fooBarDictionary')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary.123_status.autowiring')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary.foo-bar.autowiring')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary.foo_bar.autowiring')); } function it_preserves_existing_named_autowiring_aliases() @@ -241,34 +240,35 @@ function it_preserves_existing_named_autowiring_aliases() (string) $container->getAlias(Dictionary::class.' $voteDictionary'), 'app.vote_dictionary' ); + Assert::false($container->hasDefinition('knp_dictionary.dictionary.vote.autowiring')); } private function expectDico1AliasRegistration(ContainerBuilder $container): void { $container->hasAlias(Dictionary::class.' $dico1Dictionary')->willReturn(false); + $container->setDefinition( + 'knp_dictionary.dictionary.dico1.autowiring', + Argument::that(function ($definition): bool { + Assert::eq($definition->getClass(), Dictionary::class); + Assert::eq((string) $definition->getFactory()[0], Collection::class); + Assert::eq($definition->getFactory()[1], 'offsetGet'); + Assert::eq($definition->getArguments(), ['dico1']); + + return true; + }) + )->shouldBeCalled(); $container ->registerAliasForArgument( - 'knp_dictionary.dictionary.dico1', + 'knp_dictionary.dictionary.dico1.autowiring', Dictionary::class, 'dico1.dictionary' ) ->shouldBeCalled() - ->willReturn(new Alias('knp_dictionary.dictionary.dico1')) + ->willReturn(new Alias('knp_dictionary.dictionary.dico1.autowiring')) ; } } -final class DictionaryFactoryStub -{ - /** - * @param mixed[] $config - */ - public function create(string $name, array $config): Dictionary - { - return new Simple($name, $config['content']); - } -} - final class DictionaryConsumer { public function __construct( diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php index 89c7c315..fff0d484 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php @@ -5,6 +5,7 @@ namespace Knp\DictionaryBundle\DependencyInjection\Compiler; use Knp\DictionaryBundle\Dictionary; +use Knp\DictionaryBundle\Dictionary\Collection; use Knp\DictionaryBundle\Dictionary\Factory\Aggregate; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -33,7 +34,7 @@ public function process(ContainerBuilder $containerBuilder): void ); if (null !== $argumentName = $this->normalizeArgumentName($name)) { - $aliases[$argumentName][] = [$serviceId, $name.'.dictionary']; + $aliases[$argumentName][] = [$serviceId, $name]; } } @@ -46,7 +47,12 @@ public function process(ContainerBuilder $containerBuilder): void continue; } - $containerBuilder->registerAliasForArgument($candidates[0][0], Dictionary::class, $candidates[0][1]); + $serviceId = $candidates[0][0].'.autowiring'; + $containerBuilder->setDefinition( + $serviceId, + $this->createCollectionReferenceDefinition($candidates[0][1]) + ); + $containerBuilder->registerAliasForArgument($serviceId, Dictionary::class, $candidates[0][1].'.dictionary'); } } @@ -59,6 +65,14 @@ private function normalizeArgumentName(string $name): ?string return 1 === preg_match('/^[a-zA-Z_\x7f-\xff]/', $argumentName) ? $argumentName : null; } + private function createCollectionReferenceDefinition(string $name): Definition + { + return (new Definition(Dictionary::class)) + ->setFactory([new Reference(Collection::class), 'offsetGet']) + ->addArgument($name) + ; + } + /** * @param mixed[] $config */ From 01137eaeda1347d51ddc4a0579d8e59a487cdbca Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Sat, 18 Jul 2026 17:54:26 -0400 Subject: [PATCH 5/8] fix(dictionary): isolate autowiring service ids --- README.md | 26 ++++++------------- .../Compiler/DictionaryBuildingPassSpec.php | 21 ++++++++++----- .../Compiler/DictionaryBuildingPass.php | 11 ++++---- 3 files changed, 28 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 3b42ec70..3ab76fa4 100644 --- a/README.md +++ b/README.md @@ -41,15 +41,15 @@ Define dictionaries in your config.yml file: ```yaml knp_dictionary: dictionaries: - civility: - - Mr + civility: # your dictionary name + - Mr # your dictionary content - Ms ``` To inject a configured dictionary directly, type-hint `Dictionary` and name the argument `Dictionary`. Names are normalized to camel case, so -`entity_class_icons` maps to -`$entityClassIconsDictionary`. +`civility` maps to +`$civilityDictionary`. ```php use Knp\DictionaryBundle\Dictionary; @@ -65,7 +65,7 @@ final class UserManager An existing named `Dictionary` autowiring alias takes precedence and is not replaced by a configured dictionary. -To use a different argument name, select the dictionary with Symfony's `Target` +To use a different argument name, select the dictionary with Symfony's `#[Target]` attribute: ```php @@ -83,27 +83,17 @@ final class UserManager Names that are invalid PHP argument names after normalization, or names that normalize to the same argument name (for example, `foo-bar` and `foo_bar`), do -not receive an automatic autowiring alias. Wire them explicitly: - -```yaml -services: - App\Service\MyService: - arguments: - $dictionary: '@knp_dictionary.dictionary.foo-bar' -``` - -Inject the collection instead when selecting a dictionary dynamically or when -named autowiring is unavailable: +not receive an automatic autowiring alias. Inject the collection instead for +these names or when selecting a dictionary dynamically: ```php use Knp\DictionaryBundle\Dictionary; -use Knp\DictionaryBundle\Dictionary\Collection; final class UserManager { private Dictionary $dictionary; - public function __construct(Collection $dictionaries) + public function __construct(Dictionary\Collection $dictionaries) { $this->dictionary = $dictionaries['civility']; } diff --git a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php index ac4b2546..c138b9c4 100644 --- a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php +++ b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php @@ -168,6 +168,8 @@ function it_autowires_configured_dictionaries_by_name() 'dictionaries' => [ 'vote' => ['yes', 'no'], 'entity_class_icons' => ['user', 'group'], + 'foo' => ['base'], + 'foo.autowiring' => ['nested'], ], ]], $container); (new KnpDictionaryBundle())->build($container); @@ -183,6 +185,11 @@ function it_autowires_configured_dictionaries_by_name() Assert::same($consumer->voteDictionary->getName(), 'vote'); Assert::same($consumer->entityClassIconsDictionary->getName(), 'entity_class_icons'); Assert::same($consumer->dictionary->getName(), 'vote'); + + $dictionaries = $container->get(Collection::class); + Assert::isInstanceOf($dictionaries, Collection::class); + Assert::same($dictionaries['foo']->getName(), 'foo'); + Assert::same($dictionaries['foo.autowiring']->getName(), 'foo.autowiring'); } function it_keeps_invalid_and_ambiguous_dictionary_names_out_of_named_autowiring() @@ -212,9 +219,9 @@ function it_keeps_invalid_and_ambiguous_dictionary_names_out_of_named_autowiring Assert::true($container->hasDefinition('knp_dictionary.dictionary.foo_bar')); Assert::false($container->hasAlias(Dictionary::class.' $123StatusDictionary')); Assert::false($container->hasAlias(Dictionary::class.' $fooBarDictionary')); - Assert::false($container->hasDefinition('knp_dictionary.dictionary.123_status.autowiring')); - Assert::false($container->hasDefinition('knp_dictionary.dictionary.foo-bar.autowiring')); - Assert::false($container->hasDefinition('knp_dictionary.dictionary.foo_bar.autowiring')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.123_status')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.foo-bar')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.foo_bar')); } function it_preserves_existing_named_autowiring_aliases() @@ -240,14 +247,14 @@ function it_preserves_existing_named_autowiring_aliases() (string) $container->getAlias(Dictionary::class.' $voteDictionary'), 'app.vote_dictionary' ); - Assert::false($container->hasDefinition('knp_dictionary.dictionary.vote.autowiring')); + Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.vote')); } private function expectDico1AliasRegistration(ContainerBuilder $container): void { $container->hasAlias(Dictionary::class.' $dico1Dictionary')->willReturn(false); $container->setDefinition( - 'knp_dictionary.dictionary.dico1.autowiring', + 'knp_dictionary.dictionary_autowiring.dico1', Argument::that(function ($definition): bool { Assert::eq($definition->getClass(), Dictionary::class); Assert::eq((string) $definition->getFactory()[0], Collection::class); @@ -259,12 +266,12 @@ private function expectDico1AliasRegistration(ContainerBuilder $container): void )->shouldBeCalled(); $container ->registerAliasForArgument( - 'knp_dictionary.dictionary.dico1.autowiring', + 'knp_dictionary.dictionary_autowiring.dico1', Dictionary::class, 'dico1.dictionary' ) ->shouldBeCalled() - ->willReturn(new Alias('knp_dictionary.dictionary.dico1.autowiring')) + ->willReturn(new Alias('knp_dictionary.dictionary_autowiring.dico1')) ; } } diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php index fff0d484..fa81dacc 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php @@ -22,7 +22,7 @@ public function process(ContainerBuilder $containerBuilder): void throw new \Exception('The configuration "knp_dictionary.dictionaries" should be an array.'); } - /** @var array> $aliases */ + /** @var array> $aliases */ $aliases = []; foreach ($configuration['dictionaries'] as $name => $config) { @@ -34,7 +34,7 @@ public function process(ContainerBuilder $containerBuilder): void ); if (null !== $argumentName = $this->normalizeArgumentName($name)) { - $aliases[$argumentName][] = [$serviceId, $name]; + $aliases[$argumentName][] = $name; } } @@ -47,12 +47,13 @@ public function process(ContainerBuilder $containerBuilder): void continue; } - $serviceId = $candidates[0][0].'.autowiring'; + $name = $candidates[0]; + $serviceId = \sprintf('knp_dictionary.dictionary_autowiring.%s', $name); $containerBuilder->setDefinition( $serviceId, - $this->createCollectionReferenceDefinition($candidates[0][1]) + $this->createCollectionReferenceDefinition($name) ); - $containerBuilder->registerAliasForArgument($serviceId, Dictionary::class, $candidates[0][1].'.dictionary'); + $containerBuilder->registerAliasForArgument($serviceId, Dictionary::class, $name.'.dictionary'); } } From f347b95254ba621ed69ee20573b693f3fe815c64 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Sat, 18 Jul 2026 18:35:40 -0400 Subject: [PATCH 6/8] refactor(dictionary): infer alias candidates --- .../Compiler/DictionaryBuildingPass.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php index fa81dacc..22181691 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php @@ -22,7 +22,6 @@ public function process(ContainerBuilder $containerBuilder): void throw new \Exception('The configuration "knp_dictionary.dictionaries" should be an array.'); } - /** @var array> $aliases */ $aliases = []; foreach ($configuration['dictionaries'] as $name => $config) { @@ -34,12 +33,12 @@ public function process(ContainerBuilder $containerBuilder): void ); if (null !== $argumentName = $this->normalizeArgumentName($name)) { - $aliases[$argumentName][] = $name; + $aliases[$argumentName] = \array_key_exists($argumentName, $aliases) ? null : $name; } } - foreach ($aliases as $argumentName => $candidates) { - if (1 !== \count($candidates)) { + foreach ($aliases as $argumentName => $name) { + if (null === $name) { continue; } @@ -47,7 +46,6 @@ public function process(ContainerBuilder $containerBuilder): void continue; } - $name = $candidates[0]; $serviceId = \sprintf('knp_dictionary.dictionary_autowiring.%s', $name); $containerBuilder->setDefinition( $serviceId, From 4e61453193f83f41d3022e5938333fe3f7a64bec Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Mon, 3 Aug 2026 14:35:32 -0400 Subject: [PATCH 7/8] docs: clarify dictionary autowiring selection --- README.md | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3ab76fa4..bfc62981 100644 --- a/README.md +++ b/README.md @@ -46,41 +46,40 @@ knp_dictionary: - Ms ``` -To inject a configured dictionary directly, type-hint `Dictionary` and name the -argument `Dictionary`. Names are normalized to camel case, so -`civility` maps to -`$civilityDictionary`. +To inject a configured dictionary directly, type-hint `Dictionary` and select +its name with Symfony's `#[Target]` attribute: ```php use Knp\DictionaryBundle\Dictionary; +use Symfony\Component\DependencyInjection\Attribute\Target; final class UserManager { public function __construct( - private Dictionary $civilityDictionary, + #[Target('civility.dictionary')] + private Dictionary $dictionary, ) {} } ``` -An existing named `Dictionary` autowiring alias takes precedence and is not -replaced by a configured dictionary. - -To use a different argument name, select the dictionary with Symfony's `#[Target]` -attribute: +Selecting by argument name remains supported for compatibility. Use +`Dictionary`; names are normalized to camel case, so `civility` +maps to `$civilityDictionary`: ```php use Knp\DictionaryBundle\Dictionary; -use Symfony\Component\DependencyInjection\Attribute\Target; final class UserManager { public function __construct( - #[Target('civility.dictionary')] - private Dictionary $dictionary, + private Dictionary $civilityDictionary, ) {} } ``` +An existing named `Dictionary` autowiring alias takes precedence and is not +replaced by a configured dictionary. + Names that are invalid PHP argument names after normalization, or names that normalize to the same argument name (for example, `foo-bar` and `foo_bar`), do not receive an automatic autowiring alias. Inject the collection instead for From 6b2f9e63587355e8d7b67eceef0774213e13e1c1 Mon Sep 17 00:00:00 2001 From: ErwannRousseau Date: Mon, 3 Aug 2026 15:43:58 -0400 Subject: [PATCH 8/8] refactor(dictionary): simplify named autowiring --- README.md | 7 +++---- .../Compiler/DictionaryBuildingPassSpec.php | 10 +++++----- .../Compiler/DictionaryBuildingPass.php | 8 +++----- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index bfc62981..e9778c50 100644 --- a/README.md +++ b/README.md @@ -62,8 +62,8 @@ final class UserManager } ``` -Selecting by argument name remains supported for compatibility. Use -`Dictionary`; names are normalized to camel case, so `civility` +You can also select a dictionary by naming the argument +`Dictionary`. Names are normalized to camel case, so `civility` maps to `$civilityDictionary`: ```php @@ -77,8 +77,7 @@ final class UserManager } ``` -An existing named `Dictionary` autowiring alias takes precedence and is not -replaced by a configured dictionary. +An existing named `Dictionary` autowiring alias takes precedence. Names that are invalid PHP argument names after normalization, or names that normalize to the same argument name (for example, `foo-bar` and `foo_bar`), do diff --git a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php index c138b9c4..2b017a63 100644 --- a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php +++ b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php @@ -68,7 +68,7 @@ function it_builds_a_value_as_key_dictionary_from_the_config(ContainerBuilder $c return true; }) )->shouldBeCalled(); - $this->expectDico1AliasRegistration($container); + $this->expectDico1Alias($container); $this->process($container); } @@ -112,7 +112,7 @@ function it_builds_a_value_dictionary_from_the_config(ContainerBuilder $containe return true; }) )->shouldBeCalled(); - $this->expectDico1AliasRegistration($container); + $this->expectDico1Alias($container); $this->process($container); } @@ -156,7 +156,7 @@ function it_builds_a_key_value_dictionary_from_the_config(ContainerBuilder $cont return true; }) )->shouldBeCalled(); - $this->expectDico1AliasRegistration($container); + $this->expectDico1Alias($container); $this->process($container); } @@ -224,7 +224,7 @@ function it_keeps_invalid_and_ambiguous_dictionary_names_out_of_named_autowiring Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.foo_bar')); } - function it_preserves_existing_named_autowiring_aliases() + function it_keeps_existing_named_autowiring_aliases() { $container = new ContainerBuilder(); $container->setParameter('knp_dictionary.configuration', [ @@ -250,7 +250,7 @@ function it_preserves_existing_named_autowiring_aliases() Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.vote')); } - private function expectDico1AliasRegistration(ContainerBuilder $container): void + private function expectDico1Alias(ContainerBuilder $container): void { $container->hasAlias(Dictionary::class.' $dico1Dictionary')->willReturn(false); $container->setDefinition( diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php index 22181691..64355f25 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPass.php @@ -25,10 +25,8 @@ public function process(ContainerBuilder $containerBuilder): void $aliases = []; foreach ($configuration['dictionaries'] as $name => $config) { - $serviceId = \sprintf('knp_dictionary.dictionary.%s', $name); - $containerBuilder->setDefinition( - $serviceId, + \sprintf('knp_dictionary.dictionary.%s', $name), $this->createDefinition($name, $config) ); @@ -57,6 +55,7 @@ public function process(ContainerBuilder $containerBuilder): void private function normalizeArgumentName(string $name): ?string { + // Match Symfony's #[Target] parser, whose API differs in 5.4. $words = preg_replace('/[^a-zA-Z0-9\x7f-\xff]++/', ' ', $name.'.dictionary') ?? ''; $argumentName = lcfirst(str_replace(' ', '', ucwords($words))); @@ -66,9 +65,8 @@ private function normalizeArgumentName(string $name): ?string private function createCollectionReferenceDefinition(string $name): Definition { - return (new Definition(Dictionary::class)) + return (new Definition(Dictionary::class, [$name])) ->setFactory([new Reference(Collection::class), 'offsetGet']) - ->addArgument($name) ; }