Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ArrayHelper::restrictedMerge() #139

Merged
merged 7 commits into from
Dec 8, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

## 3.0.1 under development

- no changes in this release.
- New #139: Add `ArrayHelper::restrictedMerge()` method that allow to merge two or more arrays recursively with
specified depth (@vjik)

## 3.0.0 January 12, 2023

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Overall the helper has the following method groups.
- filter
- map
- merge
- restrictedMerge
- toArray

### Other
Expand Down
78 changes: 52 additions & 26 deletions src/ArrayHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,39 +124,34 @@
}

/**
* Merges two or more arrays into one recursively.
* If each array has an element with the same string key value, the latter
* will overwrite the former (different from {@see array_merge_recursive()}).
* Recursive merging will be conducted if both arrays have an element of array
* type and are having the same key.
* For integer-keyed elements, the elements from the latter array will
* be appended to the former array.
* Merges two or more arrays into one recursively. If each array has an element with the same string key value,
* the latter will overwrite the former (different from {@see array_merge_recursive()}). Recursive merging will be
* conducted if both arrays have an element of array type and are having the same key. For integer-keyed elements,
* the elements from the latter array will be appended to the former array.
*
* @param array ...$arrays Arrays to be merged.
*
* @return array The merged array (the original arrays are not changed).
*/
public static function merge(...$arrays): array
{
$result = array_shift($arrays) ?: [];
while (!empty($arrays)) {
foreach (array_shift($arrays) as $key => $value) {
if (is_int($key)) {
if (array_key_exists($key, $result)) {
if ($result[$key] !== $value) {
$result[] = $value;
}
} else {
$result[$key] = $value;
}
} elseif (isset($result[$key]) && is_array($value) && is_array($result[$key])) {
$result[$key] = self::merge($result[$key], $value);
} else {
$result[$key] = $value;
}
}
}
return $result;
return self::doMerge($arrays, null);
}

/**
* Merges two or more arrays into one recursively with specified depth. If each array has an element with the same
* string key value, the latter will overwrite the former (different from {@see array_merge_recursive()}).
* Recursive merging will be conducted if both arrays have an element of array type and are having the same key.
* For integer-keyed elements, the elements from the latter array will be appended to the former array.
*
* @param array[] $arrays Arrays to be merged.
* @param int|null $depth The maximum depth that merging is recursively. `Null` means unlimited depth.
*
* @return array The merged array (the original arrays are not changed).
*/
public static function restrictedMerge(array $arrays, ?int $depth): array
vjik marked this conversation as resolved.
Show resolved Hide resolved
Tigrov marked this conversation as resolved.
Show resolved Hide resolved
{
return self::doMerge($arrays, $depth);
}

/**
Expand Down Expand Up @@ -742,7 +737,7 @@
foreach ($array as $element) {
if (!is_array($element) && !is_object($element)) {
throw new InvalidArgumentException(
'index() can not get value from ' . gettype($element) .

Check warning on line 740 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Concat": --- Original +++ New @@ @@ /** @var mixed $element */ foreach ($array as $element) { if (!is_array($element) && !is_object($element)) { - throw new InvalidArgumentException('index() can not get value from ' . gettype($element) . '. The $array should be either multidimensional array or an array of objects.'); + throw new InvalidArgumentException(gettype($element) . 'index() can not get value from ' . '. The $array should be either multidimensional array or an array of objects.'); } $lastArray =& $result; foreach ($groups as $group) {

Check warning on line 740 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": --- Original +++ New @@ @@ /** @var mixed $element */ foreach ($array as $element) { if (!is_array($element) && !is_object($element)) { - throw new InvalidArgumentException('index() can not get value from ' . gettype($element) . '. The $array should be either multidimensional array or an array of objects.'); + throw new InvalidArgumentException(gettype($element) . '. The $array should be either multidimensional array or an array of objects.'); } $lastArray =& $result; foreach ($groups as $group) {

Check warning on line 740 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": --- Original +++ New @@ @@ /** @var mixed $element */ foreach ($array as $element) { if (!is_array($element) && !is_object($element)) { - throw new InvalidArgumentException('index() can not get value from ' . gettype($element) . '. The $array should be either multidimensional array or an array of objects.'); + throw new InvalidArgumentException('index() can not get value from ' . '. The $array should be either multidimensional array or an array of objects.'); } $lastArray =& $result; foreach ($groups as $group) {

Check warning on line 740 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Concat": --- Original +++ New @@ @@ /** @var mixed $element */ foreach ($array as $element) { if (!is_array($element) && !is_object($element)) { - throw new InvalidArgumentException('index() can not get value from ' . gettype($element) . '. The $array should be either multidimensional array or an array of objects.'); + throw new InvalidArgumentException('index() can not get value from ' . '. The $array should be either multidimensional array or an array of objects.' . gettype($element)); } $lastArray =& $result; foreach ($groups as $group) {

Check warning on line 740 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "ConcatOperandRemoval": --- Original +++ New @@ @@ /** @var mixed $element */ foreach ($array as $element) { if (!is_array($element) && !is_object($element)) { - throw new InvalidArgumentException('index() can not get value from ' . gettype($element) . '. The $array should be either multidimensional array or an array of objects.'); + throw new InvalidArgumentException('index() can not get value from ' . gettype($element)); } $lastArray =& $result; foreach ($groups as $group) {
'. The $array should be either multidimensional array or an array of objects.'
);
}
Expand Down Expand Up @@ -926,7 +921,7 @@
*
* @return bool Whether the array contains the specified key.
*/
public static function keyExists(array $array, array|float|int|string $key, bool $caseSensitive = true): bool

Check warning on line 924 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ * * @return bool Whether the array contains the specified key. */ - public static function keyExists(array $array, array|float|int|string $key, bool $caseSensitive = true) : bool + public static function keyExists(array $array, array|float|int|string $key, bool $caseSensitive = false) : bool { if (is_array($key)) { if (count($key) === 1) {
{
if (is_array($key)) {
if (count($key) === 1) {
Expand Down Expand Up @@ -999,7 +994,7 @@
public static function pathExists(
array $array,
array|float|int|string $path,
bool $caseSensitive = true,

Check warning on line 997 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "TrueValue": --- Original +++ New @@ @@ * * @psalm-param ArrayPath $path */ - public static function pathExists(array $array, array|float|int|string $path, bool $caseSensitive = true, string $delimiter = '.') : bool + public static function pathExists(array $array, array|float|int|string $path, bool $caseSensitive = false, string $delimiter = '.') : bool { return self::keyExists($array, self::parseMixedPath($path, $delimiter), $caseSensitive); }
string $delimiter = '.'
): bool {
return self::keyExists($array, self::parseMixedPath($path, $delimiter), $caseSensitive);
Expand Down Expand Up @@ -1300,7 +1295,7 @@
$numNestedKeys = count($keys) - 1;
foreach ($keys as $i => $key) {
if (!is_array($excludeNode) || !array_key_exists($key, $excludeNode)) {
continue 2; // Jump to next filter.

Check warning on line 1298 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "DecrementInteger": --- Original +++ New @@ @@ $numNestedKeys = count($keys) - 1; foreach ($keys as $i => $key) { if (!is_array($excludeNode) || !array_key_exists($key, $excludeNode)) { - continue 2; + continue 1; // Jump to next filter. } if ($i < $numNestedKeys) {

Check warning on line 1298 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Continue_": --- Original +++ New @@ @@ $numNestedKeys = count($keys) - 1; foreach ($keys as $i => $key) { if (!is_array($excludeNode) || !array_key_exists($key, $excludeNode)) { - continue 2; + break; // Jump to next filter. } if ($i < $numNestedKeys) {
}

if ($i < $numNestedKeys) {
Expand All @@ -1308,7 +1303,7 @@
$excludeNode = &$excludeNode[$key];
} else {
unset($excludeNode[$key]);
break;

Check warning on line 1306 in src/ArrayHelper.php

View workflow job for this annotation

GitHub Actions / mutation / PHP 8.2-ubuntu-latest

Escaped Mutant for Mutator "Break_": --- Original +++ New @@ @@ $excludeNode =& $excludeNode[$key]; } else { unset($excludeNode[$key]); - break; + continue; } } }
}
}
}
Expand All @@ -1335,6 +1330,37 @@
return get_object_vars($object);
}

/**
* @param array[] $arrays
*/
private static function doMerge(array $arrays, ?int $depth, int $currentDepth = 0): array
{
$result = array_shift($arrays) ?: [];
while (!empty($arrays)) {
foreach (array_shift($arrays) as $key => $value) {
if (is_int($key)) {
if (array_key_exists($key, $result)) {
if ($result[$key] !== $value) {
$result[] = $value;
}
} else {
$result[$key] = $value;
}
} elseif (
isset($result[$key])
&& ($depth === null || $currentDepth < $depth)
&& is_array($value)
&& is_array($result[$key])
) {
$result[$key] = self::doMerge([$result[$key], $value], $depth, $currentDepth + 1);
} else {
$result[$key] = $value;
}
}
}
return $result;
}

private static function normalizeArrayKey(mixed $key): string
{
return is_float($key) ? NumericHelper::normalize($key) : (string)$key;
Expand Down
145 changes: 145 additions & 0 deletions tests/ArrayHelper/MergeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,149 @@ public function testMergeIntegerKeyedArraysWithSameValue(): void

$this->assertEquals($expected, $result);
}

public static function dataRestrictedMerge(): array
{
return [
'unlimited' => [
[
'top' => 3,
'n1' => [
'n1-1' => 3,
'n1-2' => 'x',
'n1-3' => 'y',
],
'n2' => [
'n2-1' => [1, 2, 3, 4],
'n2-2' => 'a',
'more' => [
'more1' => [6, 7, 8, 9],
'more2' => 'b',
],
],
],
null,
],
'without-recursion' => [
[
'top' => 3,
'n1' => [
'n1-1' => 3,
'n1-3' => 'y',
],
'n2' => [
'n2-1' => [3, 4],
'more' => [
'more1' => [8, 9],
'more2' => 'b',
],
],
],
0,
],
'level1' => [
[
'top' => 3,
'n1' => [
'n1-1' => 3,
'n1-2' => 'x',
'n1-3' => 'y',
],
'n2' => [
'n2-1' => [3, 4],
'n2-2' => 'a',
'more' => [
'more1' => [8, 9],
'more2' => 'b',
],
],
],
1,
],
'level2' => [
[
'top' => 3,
'n1' => [
'n1-1' => 3,
'n1-2' => 'x',
'n1-3' => 'y',
],
'n2' => [
'n2-1' => [1, 2, 3, 4],
'n2-2' => 'a',
'more' => [
'more1' => [8, 9],
'more2' => 'b',
],
],
],
2,
],
'level3' => [
[
'top' => 3,
'n1' => [
'n1-1' => 3,
'n1-2' => 'x',
'n1-3' => 'y',
],
'n2' => [
'n2-1' => [1, 2, 3, 4],
'n2-2' => 'a',
'more' => [
'more1' => [6, 7, 8, 9],
'more2' => 'b',
],
],
],
3,
],
];
}

/**
* @dataProvider dataRestrictedMerge
*/
public function testRestrictedMerge(array $expected, ?int $depth): void
{
$array1 = [
'top' => 1,
'n1' => [
'n1-1' => 1,
],
'n2' => [
'n2-1' => [1, 2],
'n2-2' => 'a',
'more' => [
'more1' => [6, 7],
'more2' => 'a',
],
],
];
$array2 = [
'top' => 2,
'n1' => [
'n1-1' => 2,
'n1-2' => 'x',
],
'n2' => [
'n2-1' => [3, 4],
'more' => [
'more1' => [8, 9],
'more2' => 'b',
],
],
];
$array3 = [
'top' => 3,
'n1' => [
'n1-1' => 3,
'n1-3' => 'y',
],
];

$result = ArrayHelper::restrictedMerge([$array1, $array2, $array3], $depth);

$this->assertSame($expected, $result);
}
}