diff --git a/README.md b/README.md index 84749d3e..d9c02ed3 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,30 @@ Get a registry for this category later: $dictionaries = $registry->filterByCategory('product.toy')->all(); ``` +### Extended dictionary + +You can create an extended dictionary: +```yaml +knp_dictionary: + dictionaries: + europe: + type: 'key_value' + content: + fr: France + de: Germany + + world: + type: 'key_value' + extends: europe + content: + us: USA + ca: Canada +``` +The dictionary `world` will now contains its own values in addition +to the `europe` values. + +*Note*: You must define the initial dictionary *BEFORE* the extended one. + ## Transformers For now, this bundle is only able to resolve your **class constants**: diff --git a/spec/Knp/DictionaryBundle/Dictionary/CombinedDictionarySpec.php b/spec/Knp/DictionaryBundle/Dictionary/CombinedDictionarySpec.php new file mode 100644 index 00000000..d543e60f --- /dev/null +++ b/spec/Knp/DictionaryBundle/Dictionary/CombinedDictionarySpec.php @@ -0,0 +1,90 @@ +beConstructedWith('combined_dictionary', [ + $dictionary1, + $dictionary2, + $dictionary3 + ]); + } + + function it_is_initializable() + { + $this->shouldHaveType(CombinedDictionary::class); + } + + function it_is_a_dictionary() + { + $this->shouldImplement(Dictionary::class); + } + + function it_access_to_value_like_an_array( + $dictionary1, + $dictionary2, + $dictionary3 + ) { + $dictionary1->offsetExists('foo1')->willReturn(true); + $dictionary1->offsetGet('foo1')->willReturn('foo10'); + + $dictionary1->offsetExists('bar1')->willReturn(false); + $dictionary2->offsetExists('bar1')->willReturn(true); + $dictionary2->offsetGet('bar1')->willReturn('bar10'); + + $dictionary1->offsetExists('baz1')->willReturn(false); + $dictionary2->offsetExists('baz1')->willReturn(false); + $dictionary3->offsetExists('baz1')->willReturn(true); + $dictionary3->offsetGet('baz1')->willReturn('baz10'); + + Assert::eq($this['foo1']->getWrappedObject(), 'foo10'); + Assert::eq($this['bar1']->getWrappedObject(), 'bar10'); + Assert::eq($this['baz1']->getWrappedObject(), 'baz10'); + } + + function it_getvalues_should_return_dictionaries_values( + $dictionary1, + $dictionary2, + $dictionary3 + ) { + $dictionary1->getValues()->willReturn([ + 'foo1' => 'foo10', + 'foo2' => 'foo20', + ]); + + $dictionary2->getValues()->willReturn([ + 'bar1' => 'bar10', + 'bar2' => 'bar20', + ]); + + $dictionary3->getValues()->willReturn([ + 'foo2' => 'baz20', + 'bar2' => 'baz20', + ]); + + $this->getValues()->shouldReturn([ + 'foo1' => 'foo10', + 'foo2' => 'baz20', + 'bar1' => 'bar10', + 'bar2' => 'baz20', + ]); + } + + function its_getname_should_return_dictionary_name() + { + $this->getName()->shouldReturn('combined_dictionary'); + } +} diff --git a/spec/Knp/DictionaryBundle/Dictionary/ExtendedDictionarySpec.php b/spec/Knp/DictionaryBundle/Dictionary/ExtendedDictionarySpec.php new file mode 100644 index 00000000..6cafad67 --- /dev/null +++ b/spec/Knp/DictionaryBundle/Dictionary/ExtendedDictionarySpec.php @@ -0,0 +1,51 @@ +getValues()->willReturn(['foo1' => 'foo1', 'foo2' => 'foo2']); + $extendedDictionary->getValues()->willReturn(['bar1' => 'bar1', 'bar2' => 'bar2']); + + $this->beConstructedWith('foo', $initialDictionary, $extendedDictionary); + } + + function it_is_initializable() + { + $this->shouldHaveType(ExtendedDictionary::class); + } + + function it_is_a_dictionary() + { + $this->shouldImplement(Dictionary::class); + } + + function it_access_to_value_like_an_array() + { + Assert::eq($this['foo1']->getWrappedObject(), 'foo1'); + Assert::eq($this['foo2']->getWrappedObject(), 'foo2'); + Assert::eq($this['bar1']->getWrappedObject(), 'bar1'); + Assert::eq($this['bar2']->getWrappedObject(), 'bar2'); + } + + function its_getvalues_should_return_dictionary_values() + { + $this->getValues()->shouldReturn([ + 'foo1' => 'foo1', + 'foo2' => 'foo2', + 'bar1' => 'bar1', + 'bar2' => 'bar2', + ]); + } + + function its_getname_should_return_dictionary_name() + { + $this->getName()->shouldReturn('foo'); + } +} diff --git a/spec/Knp/DictionaryBundle/Dictionary/Factory/CombinedSpec.php b/spec/Knp/DictionaryBundle/Dictionary/Factory/CombinedSpec.php new file mode 100644 index 00000000..b68c143e --- /dev/null +++ b/spec/Knp/DictionaryBundle/Dictionary/Factory/CombinedSpec.php @@ -0,0 +1,76 @@ +beConstructedWith($registry); + } + + function it_is_initializable() + { + $this->shouldHaveType(Combined::class); + } + + function it_is_a_factory() + { + $this->shouldHaveType(Factory::class); + } + + function it_supports_specific_config() + { + $this->supports(['type' => 'combined'])->shouldReturn(true); + } + + function it_creates_a_dictionary( + $registry, + Dictionary $dictionary1, + Dictionary $dictionary2, + Dictionary $dictionary3 + ) { + $dictionary1->getValues()->willReturn([ + 'foo1' => 'foo10', + 'foo2' => 'foo20', + ]); + + $dictionary2->getValues()->willReturn([ + 'bar1' => 'bar10', + 'bar2' => 'bar20', + ]); + + $dictionary3->getValues()->willReturn([ + 'foo2' => 'baz20', + 'bar2' => 'baz20', + ]); + + $registry->get('dictionary1')->willReturn($dictionary1); + $registry->get('dictionary2')->willReturn($dictionary2); + $registry->get('dictionary3')->willReturn($dictionary3); + + $config = [ + 'type' => 'combined', + 'dictionaries' => [ + 'dictionary1', + 'dictionary2', + 'dictionary3', + ] + ]; + + $dictionary = $this->create('combined_dictionary', $config); + + $dictionary->getValues()->shouldReturn([ + 'foo1' => 'foo10', + 'foo2' => 'baz20', + 'bar1' => 'bar10', + 'bar2' => 'baz20', + ]); + } +} diff --git a/spec/Knp/DictionaryBundle/Dictionary/Factory/ExtendedSpec.php b/spec/Knp/DictionaryBundle/Dictionary/Factory/ExtendedSpec.php new file mode 100644 index 00000000..e849b021 --- /dev/null +++ b/spec/Knp/DictionaryBundle/Dictionary/Factory/ExtendedSpec.php @@ -0,0 +1,59 @@ +beConstructedWith($factoryAggregate, $registry); + } + + function it_is_initializable() + { + $this->shouldHaveType(Factory\Extended::class); + } + + function it_is_a_factory() + { + $this->shouldHaveType(Factory::class); + } + + function it_supports_specific_config() + { + $this->supports(['extends' => 'my_dictionary'])->shouldReturn(true); + } + + function it_creates_a_dictionary( + $factoryAggregate, + $registry, + Dictionary $initialDictionary, + Dictionary $extendsDictionary + ) { + $config = [ + 'content' => ['bar1', 'bar2', 'bar3'], + ]; + + $initialDictionary->getValues()->willReturn(['foo1', 'foo2']); + $extendsDictionary->getValues()->willReturn(['bar1', 'bar2', 'bar3']); + + $factoryAggregate->create('yolo', $config)->willReturn($extendsDictionary); + $registry->get('initial_dictionary')->willReturn($initialDictionary); + + $config = array_merge($config, ['extends' => 'initial_dictionary']); + $factoryAggregate->supports($config)->willReturn(true); + + $dictionary = $this->create('yolo', $config); + + $dictionary->getName()->shouldBe('yolo'); + $dictionary->getValues()->shouldBe(['foo1', 'foo2', 'bar1', 'bar2', 'bar3']); + } +} diff --git a/src/Knp/DictionaryBundle/DependencyInjection/Configuration.php b/src/Knp/DictionaryBundle/DependencyInjection/Configuration.php index 9814f518..fc28ee4e 100644 --- a/src/Knp/DictionaryBundle/DependencyInjection/Configuration.php +++ b/src/Knp/DictionaryBundle/DependencyInjection/Configuration.php @@ -1,5 +1,7 @@ end() ->children() ->scalarNode('type')->defaultValue('value')->end() + ->scalarNode('extends')->end() + ->arrayNode('dictionaries') + ->normalizeKeys(false)->prototype('scalar')->end() + ->end() ->scalarNode('category')->defaultNull()->end() ->arrayNode('content') ->prototype('scalar')->end() diff --git a/src/Knp/DictionaryBundle/Dictionary/CombinedDictionary.php b/src/Knp/DictionaryBundle/Dictionary/CombinedDictionary.php new file mode 100644 index 00000000..1c8f0ff1 --- /dev/null +++ b/src/Knp/DictionaryBundle/Dictionary/CombinedDictionary.php @@ -0,0 +1,130 @@ +name = $name; + $this->dictionaries = $dictionaries; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getValues(): array + { + $values = array_map(function ($dictionary) { + return $dictionary->getValues(); + }, $this->dictionaries); + + return array_merge(...$values); + } + + /** + * {@inheritdoc} + */ + public function getKeys(): array + { + $keys = array_map(function ($dictionary) { + return $dictionary->getKeys(); + }, $this->dictionaries); + + return array_merge(...$keys); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return in_array($offset, $this->getKeys()); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + foreach ($this->dictionaries as $dictionary) { + if ($dictionary->offsetExists($offset)) { + return $dictionary->offsetGet($offset); + } + } + + throw new InvalidArgumentException( + sprintf('Undefined offset "%s"', $offset) + ); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + foreach ($this->dictionaries as $dictionary) { + if ($dictionary->offsetExists($offset)) { + $dictionary->offsetSet($offset, $value); + + return; + } + } + + throw new InvalidArgumentException( + sprintf('Undefined offset "%s"', $offset) + ); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + foreach ($this->dictionaries as $dictionary) { + if ($dictionary->offsetExists($offset)) { + $dictionary->offsetUnset($offset); + } + } + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $iterator = new AppendIterator(); + + foreach ($this->dictionaries as $dictionary) { + $iterator->append($dictionary->getIterator()); + } + + return $iterator; + } +} diff --git a/src/Knp/DictionaryBundle/Dictionary/ExtendedDictionary.php b/src/Knp/DictionaryBundle/Dictionary/ExtendedDictionary.php new file mode 100644 index 00000000..c297442f --- /dev/null +++ b/src/Knp/DictionaryBundle/Dictionary/ExtendedDictionary.php @@ -0,0 +1,157 @@ +name = $name; + $this->initialDictionary = $initialDictionary; + $this->extendedDictionary = $extendedDictionary; + } + + /** + * {@inheritdoc} + */ + public function getName(): string + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getValues(): array + { + $this->ensureComputedValues(); + + return $this->computedValues; + } + + /** + * {@inheritdoc} + */ + public function getKeys(): array + { + $this->ensureComputedValues(); + + return array_keys($this->computedValues); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($offset) + { + return + $this->extendedDictionary->offsetExists($offset) + || $this->initialDictionary->offsetExists($offset) + ; + } + + /** + * {@inheritdoc} + */ + public function offsetGet($offset) + { + $this->ensureComputedValues(); + + return $this->computedValues[$offset]; + } + + /** + * {@inheritdoc} + */ + public function offsetSet($offset, $value) + { + $this->clearComputedValues(); + + if ($this->extendedDictionary->offsetExists($offset)) { + return $this->extendedDictionary->offsetSet($offset, $value); + } + + if ($this->initialDictionary->offsetExists($offset)) { + return $this->extendedDictionary->offsetSet($offset, $value); + } + + return $this->extendedDictionary->offsetSet($offset, $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($offset) + { + $this->clearComputedValues(); + + if ($this->extendedDictionary->offsetExists($offset)) { + $this->extendedDictionary->offsetUnset($offset); + } + + if ($this->initialDictionary->offsetExists($offset)) { + $this->extendedDictionary->offsetUnset($offset); + } + } + + /** + * {@inheritdoc} + */ + public function getIterator() + { + $this->ensureComputedValues(); + + return new ArrayIterator($this->computedValues); + } + + private function computeValues() + { + $this->computedValues = array_merge( + $this->initialDictionary->getValues(), + $this->extendedDictionary->getValues() + ); + } + + private function ensureComputedValues() + { + if (null === $this->computedValues) { + $this->computeValues(); + } + } + + private function clearComputedValues() + { + $this->computedValues = null; + } +} diff --git a/src/Knp/DictionaryBundle/Dictionary/Factory/Combined.php b/src/Knp/DictionaryBundle/Dictionary/Factory/Combined.php new file mode 100644 index 00000000..ac60217a --- /dev/null +++ b/src/Knp/DictionaryBundle/Dictionary/Factory/Combined.php @@ -0,0 +1,54 @@ +registry = $registry; + } + + /** + * {@inheritdoc} + */ + public function create(string $name, array $config): Dictionary + { + if (!isset($config['dictionaries'])) { + throw new InvalidArgumentException(sprintf( + 'Dictionary of type %s must contains a key "dictionaries"', + self::TYPE + )); + } + + $dictionaries = array_map(function ($name) { + return $this->registry->get($name); + }, $config['dictionaries']); + + return new CombinedDictionary($name, $dictionaries); + } + + /** + * {@inheritdoc} + */ + public function supports(array $config): bool + { + return isset($config['type']) ? $config['type'] === self::TYPE : false; + } +} diff --git a/src/Knp/DictionaryBundle/Dictionary/Factory/Extended.php b/src/Knp/DictionaryBundle/Dictionary/Factory/Extended.php new file mode 100644 index 00000000..e12e975b --- /dev/null +++ b/src/Knp/DictionaryBundle/Dictionary/Factory/Extended.php @@ -0,0 +1,64 @@ +factoryAggregate = $factoryAggregate; + $this->registry = $registry; + } + + /** + * {@inheritdoc} + */ + public function create(string $name, array $config): Dictionary + { + if (false === $this->factoryAggregate->supports($config)) { + throw new InvalidArgumentException(sprintf( + 'The dictionary with named "%s" cannot be created.', + $name + )); + } + + $extends = $config['extends']; + unset($config['extends']); + + $extendedDictionary = $this->factoryAggregate->create($name, $config); + $initialDictionary = $this->registry->get($extends); + + return new ExtendedDictionary($name, $initialDictionary, $extendedDictionary); + } + + /** + * {@inheritdoc} + */ + public function supports(array $config): bool + { + return isset($config['extends']); + } +} diff --git a/src/Knp/DictionaryBundle/Resources/config/dictionary.xml b/src/Knp/DictionaryBundle/Resources/config/dictionary.xml index b0d3e53b..ae1a7fb9 100644 --- a/src/Knp/DictionaryBundle/Resources/config/dictionary.xml +++ b/src/Knp/DictionaryBundle/Resources/config/dictionary.xml @@ -7,6 +7,12 @@ + + + + + + @@ -27,9 +33,13 @@ - + + + + +