From 4a41078a021b8fb27fd4582c8e9248d2de5badc3 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 6 Dec 2023 13:21:08 +0300 Subject: [PATCH 1/6] Add ability to specify recursion depth for recursive modifier --- CHANGELOG.md | 4 +- psalm.xml | 3 + src/DataModifiers.php | 19 +- src/Merger.php | 32 ++- src/Modifier/RecursiveMerge.php | 19 ++ tests/ConfigTest.php | 260 ++++++++++++++++++ .../recursion-depth/config/.merge-plan.php | 17 ++ .../configs/recursion-depth/config/params.php | 18 ++ .../vendor/package/a/params.php | 21 ++ 9 files changed, 370 insertions(+), 23 deletions(-) create mode 100644 tests/TestAsset/configs/recursion-depth/config/.merge-plan.php create mode 100644 tests/TestAsset/configs/recursion-depth/config/params.php create mode 100644 tests/TestAsset/configs/recursion-depth/vendor/package/a/params.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d2f4b3e..0b194f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ # Yii Config Change Log -## 1.4.1 under development +## 1.5.0 under development -- no changes in this release. +- New #155: Add ability to specify recursion depth for recursive modifier (@vjik) ## 1.4.0 November 17, 2023 diff --git a/psalm.xml b/psalm.xml index f4ceced..590dd10 100644 --- a/psalm.xml +++ b/psalm.xml @@ -10,4 +10,7 @@ + + + diff --git a/src/DataModifiers.php b/src/DataModifiers.php index 19c69d5..4a98e91 100644 --- a/src/DataModifiers.php +++ b/src/DataModifiers.php @@ -21,9 +21,9 @@ final class DataModifiers { /** - * @psalm-var array + * @psalm-var array */ - private array $recursiveMergeGroupsIndex; + private array $mergeGroupsRecursionDepth; /** * @psalm-var array @@ -55,7 +55,10 @@ public function __construct(array $modifiers = []) } if ($modifier instanceof RecursiveMerge) { - array_unshift($recursiveMergeGroups, $modifier->getGroups()); + array_unshift( + $recursiveMergeGroups, + array_fill_keys($modifier->getGroups(), $modifier->getDepth()) + ); continue; } @@ -97,12 +100,16 @@ public function __construct(array $modifiers = []) } $this->reverseMergeGroupsIndex = array_flip(array_merge(...$reverseMergeGroups)); - $this->recursiveMergeGroupsIndex = array_flip(array_merge(...$recursiveMergeGroups)); + $this->mergeGroupsRecursionDepth = array_merge(...$recursiveMergeGroups); } - public function isRecursiveMergeGroup(string $group): bool + public function getRecursionDepth(string $group): int|null|false { - return array_key_exists($group, $this->recursiveMergeGroupsIndex); + if (!array_key_exists($group, $this->mergeGroupsRecursionDepth)) { + return false; + } + + return $this->mergeGroupsRecursionDepth[$group]; } public function isReverseMergeGroup(string $group): bool diff --git a/src/Merger.php b/src/Merger.php index a10aacc..77cad9d 100644 --- a/src/Merger.php +++ b/src/Merger.php @@ -55,11 +55,11 @@ public function reset(): void */ public function merge(Context $context, array $arrayA, array $arrayB): array { - $isRecursiveMerge = $this->dataModifiers->isRecursiveMergeGroup($context->group()); + $recursionDepth = $this->dataModifiers->getRecursionDepth($context->group()); $isReverseMerge = $this->dataModifiers->isReverseMergeGroup($context->group()); if ($isReverseMerge) { - $arrayB = $this->prepareArrayForReverse($context, [], $arrayB, $isRecursiveMerge); + $arrayB = $this->prepareArrayForReverse($context, [], $arrayB, $recursionDepth !== false); } return $this->performMerge( @@ -67,7 +67,7 @@ public function merge(Context $context, array $arrayA, array $arrayB): array [], $isReverseMerge ? $arrayB : $arrayA, $isReverseMerge ? $arrayA : $arrayB, - $isRecursiveMerge, + $recursionDepth, $isReverseMerge, ); } @@ -87,19 +87,16 @@ private function performMerge( array $recursiveKeyPath, array $arrayA, array $arrayB, - bool $isRecursiveMerge, - bool $isReverseMerge + int|null|false $recursionDepth, + bool $isReverseMerge, + int $depth = 0, ): array { $result = $arrayA; - - /** @psalm-var mixed $v */ foreach ($arrayB as $k => $v) { if (is_int($k)) { if (array_key_exists($k, $result) && $result[$k] !== $v) { - /** @var mixed */ $result[] = $v; } else { - /** @var mixed */ $result[$k] = $v; } continue; @@ -108,8 +105,9 @@ private function performMerge( $fullKeyPath = array_merge($recursiveKeyPath, [$k]); if ( - $isRecursiveMerge + $recursionDepth !== false && is_array($v) + && ($recursionDepth === null || $depth < $recursionDepth) && ( !array_key_exists($k, $result) || is_array($result[$k]) @@ -122,7 +120,15 @@ private function performMerge( $fullKeyPath, $result, $k, - $this->performMerge($context, $fullKeyPath, $array, $v, $isRecursiveMerge, $isReverseMerge) + $this->performMerge( + $context, + $fullKeyPath, + $array, + $v, + $recursionDepth, + $isReverseMerge, + $depth + 1, + ) ); continue; } @@ -171,10 +177,8 @@ private function prepareArrayForReverse( ): array { $result = []; - /** @var mixed $value */ foreach ($array as $key => $value) { if (is_int($key)) { - /** @var mixed */ $result[$key] = $value; continue; } @@ -194,7 +198,6 @@ private function prepareArrayForReverse( } if ($context->isVariable()) { - /** @var mixed */ $result[$key] = $value; continue; } @@ -211,7 +214,6 @@ private function prepareArrayForReverse( $this->throwDuplicateKeyErrorException($context->group(), $recursiveKeyPath, [$file, $context->file()]); } - /** @var mixed */ $result[$key] = $value; /** @psalm-suppress MixedPropertyTypeCoercion */ diff --git a/src/Modifier/RecursiveMerge.php b/src/Modifier/RecursiveMerge.php index fa3535a..99a3dd3 100644 --- a/src/Modifier/RecursiveMerge.php +++ b/src/Modifier/RecursiveMerge.php @@ -80,9 +80,11 @@ final class RecursiveMerge { /** * @param string[] $groups + * @psalm-param positive-int|null $depth */ private function __construct( private array $groups, + private ?int $depth = null, ) { } @@ -91,6 +93,15 @@ public static function groups(string ...$groups): self return new self($groups); } + /** + * @param string[] $groups + * @psalm-param positive-int|null $depth + */ + public static function groupsWithDepth(array $groups, ?int $depth): self + { + return new self($groups, $depth); + } + /** * @return string[] */ @@ -98,4 +109,12 @@ public function getGroups(): array { return $this->groups; } + + /** + * @psalm-return positive-int|null + */ + public function getDepth(): ?int + { + return $this->depth; + } } diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php index 9523185..7226614 100644 --- a/tests/ConfigTest.php +++ b/tests/ConfigTest.php @@ -571,6 +571,266 @@ public function testDeepRecursive(): void ], $config->get('params')); } + public static function dataRecursionDepth(): array + { + return [ + 'unlimited' => [ + [ + 'top1' => [ + 'nestedA' => [ + 'nestedA-3' => 3, + 'nestedA-4' => 4, + 'nestedZ' => [3, 4, 1, 2], + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'only-app' => 42, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'B' => 2, + 'A' => 1, + ], + 'top3' => ['Z' => 3], + 'top0' => ['X' => 7], + ], + null, + ], + 'level0' => [ + [ + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2], + 'only-app' => 42, + ], + ], + 'top2' => [ + 'A' => 1, + ], + 'top3' => ['Z' => 3], + 'top0' => ['X' => 7], + ], + 0, + ], + 'level1' => [ + [ + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2], + 'only-app' => 42, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'B' => 2, + 'A' => 1, + ], + 'top3' => ['Z' => 3], + 'top0' => ['X' => 7], + ], + 1, + ], + 'level2' => [ + [ + 'top1' => [ + 'nestedA' => [ + 'nestedA-3' => 3, + 'nestedA-4' => 4, + 'nestedZ' => [1, 2], + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'only-app' => 42, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'B' => 2, + 'A' => 1, + ], + 'top3' => ['Z' => 3], + 'top0' => ['X' => 7], + ], + 2, + ], + 'level3' => [ + [ + 'top1' => [ + 'nestedA' => [ + 'nestedA-3' => 3, + 'nestedA-4' => 4, + 'nestedZ' => [3, 4, 1, 2], + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'only-app' => 42, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'B' => 2, + 'A' => 1, + ], + 'top3' => ['Z' => 3], + 'top0' => ['X' => 7], + ], + 3, + ], + 'unlimited-reverse' => [ + [ + 'top0' => ['X' => 7], + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2, 3, 4], + 'only-app' => 42, + 'nestedA-3' => 3, + 'nestedA-4' => 4, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'A' => 1, + 'B' => 2, + ], + 'top3' => ['Z' => 3], + ], + null, + true, + ], + 'level0-reverse' => [ + [ + 'top0' => ['X' => 7], + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2], + 'only-app' => 42, + ], + ], + 'top2' => [ + 'A' => 1, + ], + 'top3' => ['Z' => 3], + ], + 0, + true, + ], + 'level1-reverse' => [ + [ + 'top0' => ['X' => 7], + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2], + 'only-app' => 42, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'A' => 1, + 'B' => 2, + ], + 'top3' => ['Z' => 3], + ], + 1, + true, + ], + 'level2-reverse' => [ + [ + 'top0' => ['X' => 7], + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2], + 'only-app' => 42, + 'nestedA-3' => 3, + 'nestedA-4' => 4, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'A' => 1, + 'B' => 2, + ], + 'top3' => ['Z' => 3], + ], + 2, + true, + ], + 'level3-reverse' => [ + [ + 'top0' => ['X' => 7], + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2, 3, 4], + 'only-app' => 42, + 'nestedA-3' => 3, + 'nestedA-4' => 4, + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'A' => 1, + 'B' => 2, + ], + 'top3' => ['Z' => 3], + ], + 3, + true, + ], + ]; + } + + /** + * @dataProvider dataRecursionDepth + */ + public function testRecursionDepth(array $expected, ?int $depth, bool $reverse = false): void + { + $config = new Config( + new ConfigPaths(__DIR__ . '/TestAsset/configs/recursion-depth', 'config'), + Options::DEFAULT_ENVIRONMENT, + [ + RecursiveMerge::groupsWithDepth(['params'], $depth), + ReverseMerge::groups(...($reverse ? ['params'] : [])), + ] + ); + + $this->assertSame($expected, $config->get('params')); + } + public function testConfigWithReverseMerge(): void { $config = new Config( diff --git a/tests/TestAsset/configs/recursion-depth/config/.merge-plan.php b/tests/TestAsset/configs/recursion-depth/config/.merge-plan.php new file mode 100644 index 0000000..73b0651 --- /dev/null +++ b/tests/TestAsset/configs/recursion-depth/config/.merge-plan.php @@ -0,0 +1,17 @@ + [ + 'params' => [ + 'package/a' => [ + 'params.php', + ], + '/' => [ + 'params.php', + ], + ], + ], +]; diff --git a/tests/TestAsset/configs/recursion-depth/config/params.php b/tests/TestAsset/configs/recursion-depth/config/params.php new file mode 100644 index 0000000..3347601 --- /dev/null +++ b/tests/TestAsset/configs/recursion-depth/config/params.php @@ -0,0 +1,18 @@ + ['X' => 7], + 'top1' => [ + 'nestedA' => [ + 'nestedA-1' => 1, + 'nestedA-2' => 2, + 'nestedZ' => [1, 2], + 'only-app' => 42, + ], + ], + 'top2' => [ + 'A' => 1, + ], +]; diff --git a/tests/TestAsset/configs/recursion-depth/vendor/package/a/params.php b/tests/TestAsset/configs/recursion-depth/vendor/package/a/params.php new file mode 100644 index 0000000..748a4a8 --- /dev/null +++ b/tests/TestAsset/configs/recursion-depth/vendor/package/a/params.php @@ -0,0 +1,21 @@ + [ + 'nestedA' => [ + 'nestedA-3' => 3, + 'nestedA-4' => 4, + 'nestedZ' => [3, 4], + ], + 'nestedB' => [ + 'nestedB-3' => 3, + 'nestedB-4' => 4, + ], + ], + 'top2' => [ + 'B' => 2, + ], + 'top3' => ['Z' => 3], +]; From 53dd7e96f662abcc9729333a4413c391b1aee7e3 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Wed, 6 Dec 2023 13:23:43 +0300 Subject: [PATCH 2/6] readme --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index f950643..679320b 100644 --- a/README.md +++ b/README.md @@ -465,6 +465,12 @@ $config = new Config( $params = $config->get('params'); // merged recursively ``` +If you want to recursively merge arrays to a certain depth, to use the `RecursiveMerge::groupsWithDepth()` method: + +```php +RecursiveMerge::groups(['widgets-themes', 'my-custom-group'], 1) +``` + ### Reverse merge of arrays Result of reverse merge is being ordered descending by data source. It is useful for merging module config with From d81997645648cda4e3c6f8456f9b2c6bca492c53 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 7 Dec 2023 11:27:02 +0300 Subject: [PATCH 3/6] Update README.md Co-authored-by: Alexey Rogachev --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 679320b..46b0941 100644 --- a/README.md +++ b/README.md @@ -465,7 +465,7 @@ $config = new Config( $params = $config->get('params'); // merged recursively ``` -If you want to recursively merge arrays to a certain depth, to use the `RecursiveMerge::groupsWithDepth()` method: +If you want to recursively merge arrays to a certain depth, use the `RecursiveMerge::groupsWithDepth()` method: ```php RecursiveMerge::groups(['widgets-themes', 'my-custom-group'], 1) From 55ead16868900982e4edfc8e2ddb24fe66b5a4d3 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 7 Dec 2023 11:27:34 +0300 Subject: [PATCH 4/6] Update src/DataModifiers.php Co-authored-by: Alexey Rogachev --- src/DataModifiers.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/DataModifiers.php b/src/DataModifiers.php index 4a98e91..ac2a14f 100644 --- a/src/DataModifiers.php +++ b/src/DataModifiers.php @@ -103,6 +103,12 @@ public function __construct(array $modifiers = []) $this->mergeGroupsRecursionDepth = array_merge(...$recursiveMergeGroups); } + /** + * @return int|null|false + * - `int` - depth limited by specified number. + * - `null` - depth is not limited (infinite recursion). + * - `false` - recursion is disabled. + */ public function getRecursionDepth(string $group): int|null|false { if (!array_key_exists($group, $this->mergeGroupsRecursionDepth)) { From 9e2a9970a200338cdea4cf3f622185226d6e7a22 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 7 Dec 2023 08:27:42 +0000 Subject: [PATCH 5/6] Apply fixes from StyleCI --- src/DataModifiers.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DataModifiers.php b/src/DataModifiers.php index ac2a14f..a6ed205 100644 --- a/src/DataModifiers.php +++ b/src/DataModifiers.php @@ -104,7 +104,7 @@ public function __construct(array $modifiers = []) } /** - * @return int|null|false + * @return false|int|null * - `int` - depth limited by specified number. * - `null` - depth is not limited (infinite recursion). * - `false` - recursion is disabled. From 3da856edb0a721e38568a061b535f5005b3fbcad Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Thu, 7 Dec 2023 11:31:46 +0300 Subject: [PATCH 6/6] rename --- src/DataModifiers.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/DataModifiers.php b/src/DataModifiers.php index a6ed205..3d36730 100644 --- a/src/DataModifiers.php +++ b/src/DataModifiers.php @@ -23,7 +23,7 @@ final class DataModifiers /** * @psalm-var array */ - private array $mergeGroupsRecursionDepth; + private array $mergedGroupsRecursionDepthMap; /** * @psalm-var array @@ -100,7 +100,7 @@ public function __construct(array $modifiers = []) } $this->reverseMergeGroupsIndex = array_flip(array_merge(...$reverseMergeGroups)); - $this->mergeGroupsRecursionDepth = array_merge(...$recursiveMergeGroups); + $this->mergedGroupsRecursionDepthMap = array_merge(...$recursiveMergeGroups); } /** @@ -111,11 +111,11 @@ public function __construct(array $modifiers = []) */ public function getRecursionDepth(string $group): int|null|false { - if (!array_key_exists($group, $this->mergeGroupsRecursionDepth)) { + if (!array_key_exists($group, $this->mergedGroupsRecursionDepthMap)) { return false; } - return $this->mergeGroupsRecursionDepth[$group]; + return $this->mergedGroupsRecursionDepthMap[$group]; } public function isReverseMergeGroup(string $group): bool