diff --git a/src/Symfony/Component/Config/CHANGELOG.md b/src/Symfony/Component/Config/CHANGELOG.md index 1697989556fd0..ad0aead635476 100644 --- a/src/Symfony/Component/Config/CHANGELOG.md +++ b/src/Symfony/Component/Config/CHANGELOG.md @@ -1,6 +1,11 @@ CHANGELOG ========= +7.2 +--- + + * Allow overriding prototyped array nodes with using `!` + 7.1 --- diff --git a/src/Symfony/Component/Config/Definition/ArrayNode.php b/src/Symfony/Component/Config/Definition/ArrayNode.php index 15ad478623ea6..7336fbcb9c385 100644 --- a/src/Symfony/Component/Config/Definition/ArrayNode.php +++ b/src/Symfony/Component/Config/Definition/ArrayNode.php @@ -22,6 +22,8 @@ */ class ArrayNode extends BaseNode implements PrototypeNodeInterface { + private const OVERRIDE_OPERATOR = '!'; + protected array $xmlRemappings = []; protected array $children = []; protected bool $allowFalse = false; @@ -344,6 +346,13 @@ protected function mergeValues(mixed $leftSide, mixed $rightSide): mixed } foreach ($rightSide as $k => $v) { + if (str_starts_with($k, self::OVERRIDE_OPERATOR)) { + $keyWithoutOverrideOperator = ltrim($k, self::OVERRIDE_OPERATOR); + + $leftSide[$keyWithoutOverrideOperator] = $v; + continue; + } + // no conflict if (!\array_key_exists($k, $leftSide)) { if (!$this->allowNewKeys) { diff --git a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php index a11e726384315..5d1f64464331e 100644 --- a/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php +++ b/src/Symfony/Component/Config/Definition/PrototypedArrayNode.php @@ -23,6 +23,8 @@ */ class PrototypedArrayNode extends ArrayNode { + private const OVERRIDE_OPERATOR = '!'; + protected PrototypeNodeInterface $prototype; protected ?string $keyAttribute = null; protected bool $removeKeyAttribute = false; @@ -259,6 +261,14 @@ protected function mergeValues(mixed $leftSide, mixed $rightSide): mixed $isList = array_is_list($rightSide); foreach ($rightSide as $k => $v) { + // key starts with the '!' operator, so we override the left side + if (str_starts_with($k, self::OVERRIDE_OPERATOR)) { + $keyWithoutOverrideOperator = ltrim($k, self::OVERRIDE_OPERATOR); + + $leftSide[$keyWithoutOverrideOperator] = $v; + continue; + } + // prototype, and key is irrelevant there are no named keys, append the element if (null === $this->keyAttribute && $isList) { $leftSide[] = $v; diff --git a/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php b/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php index abf0de7771627..6f9960f11dad4 100644 --- a/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php +++ b/src/Symfony/Component/Config/Tests/Definition/PrototypedArrayNodeTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Config\Definition\ArrayNode; +use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\PrototypedArrayNode; use Symfony\Component\Config\Definition\ScalarNode; use Symfony\Component\Config\Definition\VariableNode; @@ -354,6 +355,70 @@ public function testPrototypedArrayNodeMerge(array $left, array $right, array $e self::assertSame($result, $expected); } + public function testOverridingPrototypedArrayNodeWithBangOperator(): void + { + $treeBuilder = new TreeBuilder('root'); + $rootNode = $treeBuilder->getRootNode(); + + $rootNode + ->children() + ->arrayNode('workflows') + ->useAttributeAsKey('name') + ->prototype('array') + ->children() + ->arrayNode('from') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ; + + $builtTree = $treeBuilder->buildTree(); + + $this->assertSame( + [ + 'workflows' => [ + 'workflow_a' => [ + 'from' => ['a', 'b'], + ], + 'workflow_b' => [ + 'from' => ['b', 'c'], + ], + 'workflow_c' => [ + 'from' => ['x', 'd'], + ], + ], + ], + $builtTree->merge( + [ + 'workflows' => [ + 'workflow_a' => [ + 'from' => ['a', 'b'], + ], + 'workflow_b' => [ + 'from' => ['c', 'd'], + ], + 'workflow_c' => [ + 'from' => ['d', 'e'], + ], + ], + ], + [ + 'workflows' => [ + '!workflow_b' => [ + 'from' => ['b', 'c'], + ], + 'workflow_c' => [ + '!from' => ['x', 'd'], + ], + ], + ], + ), + ); + } + public static function getPrototypedArrayNodeDataToMerge(): array { return [