diff --git a/README.md b/README.md index fd901b1c..e9778c50 100644 --- a/README.md +++ b/README.md @@ -41,22 +41,61 @@ Define dictionaries in your config.yml file: ```yaml knp_dictionary: dictionaries: - my_dictionary: # your dictionary name - - Foo # your dictionary content - - Bar - - Baz + civility: # your dictionary name + - Mr # your dictionary content + - Ms +``` + +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( + #[Target('civility.dictionary')] + private Dictionary $dictionary, + ) {} +} ``` -You will be able to retrieve it by injecting the Collection service and accessing the dictionary by its key +You can also select a dictionary by naming the argument +`Dictionary`. Names are normalized to camel case, so `civility` +maps to `$civilityDictionary`: ```php +use Knp\DictionaryBundle\Dictionary; - private Dictionary $myDictionary; +final class UserManager +{ public function __construct( - \Knp\DictionaryBundle\Dictionary\Collection $dictionaries) + private Dictionary $civilityDictionary, + ) {} +} +``` + +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 +not receive an automatic autowiring alias. Inject the collection instead for +these names or when selecting a dictionary dynamically: + +```php +use Knp\DictionaryBundle\Dictionary; + +final class UserManager +{ + private Dictionary $dictionary; + + public function __construct(Dictionary\Collection $dictionaries) { - $this->myDictionary = $dictionaries['my_dictionary']; + $this->dictionary = $dictionaries['civility']; } +} ``` ## Dictionary form type @@ -70,7 +109,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('civility', DictionaryType::class, array( - 'name' => 'my_dictionary' + 'name' => 'civility' )) ; } @@ -88,7 +127,7 @@ use Knp\DictionaryBundle\Validator\Constraints\Dictionary; class User { #[ORM\Column] - #[Dictionary(name: 'my_dictionary')] + #[Dictionary(name: 'civility')] private $civility; } ``` diff --git a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php index 87b7f700..2b017a63 100644 --- a/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php +++ b/spec/Knp/DictionaryBundle/DependencyInjection/Compiler/DictionaryBuildingPassSpec.php @@ -6,10 +6,16 @@ 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; +use Symfony\Component\DependencyInjection\Attribute\Target; use Symfony\Component\DependencyInjection\ContainerBuilder; use Webmozart\Assert\Assert; @@ -62,6 +68,7 @@ function it_builds_a_value_as_key_dictionary_from_the_config(ContainerBuilder $c return true; }) )->shouldBeCalled(); + $this->expectDico1Alias($container); $this->process($container); } @@ -105,6 +112,7 @@ function it_builds_a_value_dictionary_from_the_config(ContainerBuilder $containe return true; }) )->shouldBeCalled(); + $this->expectDico1Alias($container); $this->process($container); } @@ -148,7 +156,132 @@ function it_builds_a_key_value_dictionary_from_the_config(ContainerBuilder $cont return true; }) )->shouldBeCalled(); + $this->expectDico1Alias($container); $this->process($container); } + + function it_autowires_configured_dictionaries_by_name() + { + $container = new ContainerBuilder(); + (new KnpDictionaryExtension())->load([[ + 'dictionaries' => [ + 'vote' => ['yes', 'no'], + 'entity_class_icons' => ['user', 'group'], + 'foo' => ['base'], + 'foo.autowiring' => ['nested'], + ], + ]], $container); + (new KnpDictionaryBundle())->build($container); + $container + ->register(DictionaryConsumer::class, DictionaryConsumer::class) + ->setAutowired(true) + ->setPublic(true) + ; + $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'); + + $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() + { + $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')); + 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_keeps_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' + ); + Assert::false($container->hasDefinition('knp_dictionary.dictionary_autowiring.vote')); + } + + private function expectDico1Alias(ContainerBuilder $container): void + { + $container->hasAlias(Dictionary::class.' $dico1Dictionary')->willReturn(false); + $container->setDefinition( + 'knp_dictionary.dictionary_autowiring.dico1', + 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_autowiring.dico1', + Dictionary::class, + 'dico1.dictionary' + ) + ->shouldBeCalled() + ->willReturn(new Alias('knp_dictionary.dictionary_autowiring.dico1')) + ; + } +} + +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..64355f25 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; @@ -21,12 +22,52 @@ public function process(ContainerBuilder $containerBuilder): void throw new \Exception('The configuration "knp_dictionary.dictionaries" should be an array.'); } + $aliases = []; + foreach ($configuration['dictionaries'] as $name => $config) { $containerBuilder->setDefinition( \sprintf('knp_dictionary.dictionary.%s', $name), $this->createDefinition($name, $config) ); + + if (null !== $argumentName = $this->normalizeArgumentName($name)) { + $aliases[$argumentName] = \array_key_exists($argumentName, $aliases) ? null : $name; + } } + + foreach ($aliases as $argumentName => $name) { + if (null === $name) { + continue; + } + + if ($containerBuilder->hasAlias(Dictionary::class.' $'.$argumentName)) { + continue; + } + + $serviceId = \sprintf('knp_dictionary.dictionary_autowiring.%s', $name); + $containerBuilder->setDefinition( + $serviceId, + $this->createCollectionReferenceDefinition($name) + ); + $containerBuilder->registerAliasForArgument($serviceId, Dictionary::class, $name.'.dictionary'); + } + } + + 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))); + + return 1 === preg_match('/^[a-zA-Z_\x7f-\xff]/', $argumentName) ? $argumentName : null; + } + + private function createCollectionReferenceDefinition(string $name): Definition + { + return (new Definition(Dictionary::class, [$name])) + ->setFactory([new Reference(Collection::class), 'offsetGet']) + ; } /**