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 @@
-
+
+
+
+
+