diff --git a/docs/guide-ru/helper-array.md b/docs/guide-ru/helper-array.md index 92dcdf558b8..94a60765c28 100644 --- a/docs/guide-ru/helper-array.md +++ b/docs/guide-ru/helper-array.md @@ -348,3 +348,140 @@ ArrayHelper::isIn('a', new(ArrayObject['a'])); ArrayHelper::isSubset(new(ArrayObject['a', 'c']), new(ArrayObject['a', 'b', 'c']) ``` + +## Преобразование многомерных массивов + +Метод `ArrayHelper::flatten()` позволяет преобразовать многомерный массив в одномерный, объединяя ключи. + +### Основное использование + +Чтобы преобразовать вложенный массив, просто передайте массив в метод `flatten()`: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// [ +// 'a.b.c' => 1, +// 'a.b.d' => 2, +// 'a.e' => 3, +// 'f' => 4, +// ] +``` + +### Пользовательский разделитель + +Вы можете указать пользовательский (т.е. отличный от значения по умолчанию: `.`) разделитель для объединения ключей: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array, '_'); +// Результат: +// [ +// 'a_b_c' => 1, +// 'a_b_d' => 2, +// 'a_e' => 3, +// 'f' => 4, +// ] +``` + +### Обработка специальных символов в ключах + +Метод `flatten()` может обрабатывать ключи со специальными символами: + +```php +$array = [ + 'a.b' => [ + 'c.d' => 1, + ], + 'e.f' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// [ +// 'a.b.c.d' => 1, +// 'e.f' => 2, +// ] +``` + +### Смешанные типы данных + +Метод `flatten()` работает с массивами, содержащими различные типы данных: + +```php +$array = [ + 'a' => [ + 'b' => 'string', + 'c' => 123, + 'd' => true, + 'e' => null, + ], + 'f' => [1, 2, 3], +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// [ +// 'a.b' => 'string', +// 'a.c' => 123, +// 'a.d' => true, +// 'a.e' => null, +// 'f.0' => 1, +// 'f.1' => 2, +// 'f.2' => 3, +// ] +``` + +### Краевые случаи + +Метод `flatten()` обрабатывает различные краевые случаи, такие как пустые массивы и значения, не являющиеся массивами: + +```php +// Пустой массив +$array = []; +$flattenedArray = ArrayHelper::flatten($array); +// Результат: [] + +// Значение, не являющееся массивом +$array = 'string'; +$flattenedArray = ArrayHelper::flatten($array); +// Результат: +// yii\base\InvalidArgumentException: Argument $array must be an array or implement Traversable +``` + +### Коллизии ключей + +Когда ключи совпадают, метод `flatten()` перезапишет предыдущее значение: + +```php +$array = [ + 'a' => [ + 'b' => 1, + ], + 'a.b' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Результат: ['a.b' => 2] +``` diff --git a/docs/guide/helper-array.md b/docs/guide/helper-array.md index 19c3c03ad5f..e7b1641fff8 100644 --- a/docs/guide/helper-array.md +++ b/docs/guide/helper-array.md @@ -483,3 +483,140 @@ ArrayHelper::isIn('a', new ArrayObject(['a'])); // true ArrayHelper::isSubset(new ArrayObject(['a', 'c']), new ArrayObject(['a', 'b', 'c'])); ``` + +## Flattening Arrays + +The `ArrayHelper::flatten()` method allows you to convert a multi-dimensional array into a single-dimensional array by concatenating keys. + +### Basic Usage + +To flatten a nested array, simply pass the array to the `flatten()` method: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// [ +// 'a.b.c' => 1, +// 'a.b.d' => 2, +// 'a.e' => 3, +// 'f' => 4, +// ] +``` + +### Custom Separator + +You can specify a custom separator to use when concatenating keys: + +```php +$array = [ + 'a' => [ + 'b' => [ + 'c' => 1, + 'd' => 2, + ], + 'e' => 3, + ], + 'f' => 4, +]; + +$flattenedArray = ArrayHelper::flatten($array, '_'); +// Result: +// [ +// 'a_b_c' => 1, +// 'a_b_d' => 2, +// 'a_e' => 3, +// 'f' => 4, +// ] +``` + +### Handling Special Characters in Keys + +The `flatten()` method can handle keys with special characters: + +```php +$array = [ + 'a.b' => [ + 'c.d' => 1, + ], + 'e.f' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// [ +// 'a.b.c.d' => 1, +// 'e.f' => 2, +// ] +``` + +### Mixed Data Types + +The `flatten()` method works with arrays containing different data types: + +```php +$array = [ + 'a' => [ + 'b' => 'string', + 'c' => 123, + 'd' => true, + 'e' => null, + ], + 'f' => [1, 2, 3], +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// [ +// 'a.b' => 'string', +// 'a.c' => 123, +// 'a.d' => true, +// 'a.e' => null, +// 'f.0' => 1, +// 'f.1' => 2, +// 'f.2' => 3, +// ] +``` + +### Edge Cases + +The `flatten()` method handles various edge cases, such as empty arrays and non-array values: + +```php +// Empty array +$array = []; +$flattenedArray = ArrayHelper::flatten($array); +// Result: [] + +// Non-array value +$array = 'string'; +$flattenedArray = ArrayHelper::flatten($array); +// Result: +// yii\base\InvalidArgumentException: Argument $array must be an array or implement Traversable +``` + +### Key Collisions + +When keys collide, the `flatten()` method will overwrite the previous value: + +```php +$array = [ + 'a' => [ + 'b' => 1, + ], + 'a.b' => 2, +]; + +$flattenedArray = ArrayHelper::flatten($array); +// Result: ['a.b' => 2] +``` diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 391edd84d92..67c0fc35e4f 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -25,6 +25,7 @@ Yii Framework 2 Change Log - Enh #20295: Add an ability to have wildcards in `yii\log\Target::$maskVars` array (xcopy) - Bug #20296: Fix broken enum test (briedis) - Bug #20300: Clear stat cache in `FileCache::setValue()` (rob006) +- Enh #20306: Add new `yii\helpers\ArrayHelper::flatten()` method (xcopy) 2.0.51 July 18, 2024 -------------------- diff --git a/framework/helpers/BaseArrayHelper.php b/framework/helpers/BaseArrayHelper.php index bc770f96cb7..c24cc99f970 100644 --- a/framework/helpers/BaseArrayHelper.php +++ b/framework/helpers/BaseArrayHelper.php @@ -1043,4 +1043,61 @@ public static function recursiveSort(array &$array, $sorter = null) return $array; } + + /** + * Flattens a multidimensional array into a one-dimensional array. + * + * This method recursively traverses the input array and concatenates the keys + * in a dot format to form a new key in the resulting array. + * + * Example: + * + * ```php + * $array = [ + * 'A' => [1, 2], + * 'B' => [ + * 'C' => 1, + * 'D' => 2, + * ], + * 'E' => 1, + * ]; + * $result = \yii\helpers\ArrayHelper::flatten($array); + * // $result will be: + * // [ + * // 'A.0' => 1 + * // 'A.1' => 2 + * // 'B.C' => 1 + * // 'B.D' => 2 + * // 'E' => 1 + * // ] + * ``` + * + * @param array $array the input array to be flattened in terms of name-value pairs. + * @param string $separator the separator to use between keys. Defaults to '.'. + * + * @return array the flattened array. + * @throws InvalidArgumentException if `$array` is neither traversable nor an array. + */ + public static function flatten($array, $separator = '.'): array + { + if (!static::isTraversable($array)) { + throw new InvalidArgumentException('Argument $array must be an array or implement Traversable'); + } + + $result = []; + + foreach ($array as $key => $value) { + $newKey = $key; + if (is_array($value)) { + $flattenedArray = self::flatten($value, $separator); + foreach ($flattenedArray as $subKey => $subValue) { + $result[$newKey . $separator . $subKey] = $subValue; + } + } else { + $result[$newKey] = $value; + } + } + + return $result; + } } diff --git a/framework/log/Target.php b/framework/log/Target.php index a56d9efbf00..1d9a22f23d9 100644 --- a/framework/log/Target.php +++ b/framework/log/Target.php @@ -167,54 +167,6 @@ public function collect($messages, $final) } } - /** - * Flattens a multidimensional array into a one-dimensional array. - * - * This method recursively traverses the input array and concatenates the keys - * to form a new key in the resulting array. - * - * Example: - * - * ```php - * $array = [ - * 'A' => [1, 2], - * 'B' => [ - * 'C' => 1, - * 'D' => 2, - * ], - * 'E' => 1, - * ]; - * $result = \yii\log\Target::flatten($array); - * // result will be: - * // [ - * // 'A.0' => 1 - * // 'A.1' => 2 - * // 'B.C' => 1 - * // 'B.D' => 2 - * // 'E' => 1 - * // ] - * ``` - * - * @param array $array the input array to be flattened. - * @param string $prefix the prefix to be added to each key in the resulting array. - * - * @return array the flattened array. - */ - private static function flatten($array, $prefix = ''): array - { - $result = []; - - foreach ($array as $key => $value) { - if (is_array($value)) { - $result = array_merge($result, self::flatten($value, $prefix . $key . '.')); - } else { - $result[$prefix . $key] = $value; - } - } - - return $result; - } - /** * Generates the context information to be logged. * The default implementation will dump user information, system variables, etc. @@ -223,7 +175,7 @@ private static function flatten($array, $prefix = ''): array protected function getContextMessage() { $context = ArrayHelper::filter($GLOBALS, $this->logVars); - $items = self::flatten($context); + $items = ArrayHelper::flatten($context); foreach ($this->maskVars as $var) { foreach ($items as $key => $value) { if (StringHelper::matchWildcard($var, $key, ['caseSensitive' => false])) { diff --git a/tests/framework/helpers/ArrayHelperTest.php b/tests/framework/helpers/ArrayHelperTest.php index a086503006b..4e6b790bebc 100644 --- a/tests/framework/helpers/ArrayHelperTest.php +++ b/tests/framework/helpers/ArrayHelperTest.php @@ -1610,6 +1610,125 @@ public function dataProviderRecursiveSort() ], ]; } + + public function testFlatten() + { + // Test with deeply nested arrays + $array = [ + 'a' => [ + 'b' => [ + 'c' => [ + 'd' => 1, + 'e' => 2, + ], + 'f' => 3, + ], + 'g' => 4, + ], + 'h' => 5, + ]; + $expected = [ + 'a.b.c.d' => 1, + 'a.b.c.e' => 2, + 'a.b.f' => 3, + 'a.g' => 4, + 'h' => 5, + ]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Test with arrays containing different data types + $array = [ + 'a' => [ + 'b' => [ + 'c' => 'string', + 'd' => 123, + 'e' => true, + 'f' => null, + ], + 'g' => [1, 2, 3], + ], + ]; + $expected = [ + 'a.b.c' => 'string', + 'a.b.d' => 123, + 'a.b.e' => true, + 'a.b.f' => null, + 'a.g.0' => 1, + 'a.g.1' => 2, + 'a.g.2' => 3, + ]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Test with arrays containing special characters in keys + $array = [ + 'a.b' => [ + 'c.d' => [ + 'e.f' => 1, + ], + ], + 'g.h' => 2, + ]; + $expected = [ + 'a.b.c.d.e.f' => 1, + 'g.h' => 2, + ]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Test with custom separator + $array = [ + 'a' => [ + 'b' => [ + 'c' => [ + 'd' => 1, + 'e' => 2, + ], + 'f' => 3, + ], + 'g' => 4, + ], + 'h' => 5, + ]; + $result = ArrayHelper::flatten($array, '_'); + $expected = [ + 'a_b_c_d' => 1, + 'a_b_c_e' => 2, + 'a_b_f' => 3, + 'a_g' => 4, + 'h' => 5, + ]; + + $this->assertEquals($expected, $result); + } + + public function testFlattenEdgeCases() + { + // Empty array + $array = []; + $expected = []; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Non-array value + $array = 'string'; + $expected = ['string']; + $this->expectException('yii\base\InvalidArgumentException'); + $this->expectExceptionMessage('Argument $array must be an array or implement Traversable'); + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Special characters in keys + $array = ['a.b' => ['c.d' => 1]]; + $expected = ['a.b.c.d' => 1]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Mixed data types + $array = ['a' => ['b' => 'string', 'c' => 123, 'd' => true, 'e' => null]]; + $expected = ['a.b' => 'string', 'a.c' => 123, 'a.d' => true, 'a.e' => null]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + + // Key collisions + $array = ['a' => ['b' => 1], 'a.b' => 2]; + $expected = ['a.b' => 2]; + $this->assertEquals($expected, ArrayHelper::flatten($array)); + } } class Post1