Skip to content
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
59 changes: 49 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
`<dictionaryName>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
Expand All @@ -70,7 +109,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('civility', DictionaryType::class, array(
'name' => 'my_dictionary'
'name' => 'civility'
))
;
}
Expand All @@ -88,7 +127,7 @@ use Knp\DictionaryBundle\Validator\Constraints\Dictionary;
class User
{
#[ORM\Column]
#[Dictionary(name: 'my_dictionary')]
#[Dictionary(name: 'civility')]
private $civility;
}
```
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -105,6 +112,7 @@ function it_builds_a_value_dictionary_from_the_config(ContainerBuilder $containe
return true;
})
)->shouldBeCalled();
$this->expectDico1Alias($container);

$this->process($container);
}
Expand Down Expand Up @@ -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,
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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'])
;
}

/**
Expand Down
Loading