Разверните «Точечная нотация». ключи вложенного массива в дочерние массивы

Я начинаю с вложенного массива произвольной глубины. В этом массиве некоторые ключи представляют собой серию токенов, разделенных точками. Например, «billingAddress.street» или «foo.bar.baz». Я хотел бы расширить эти ключевые элементы до массивов, поэтому в результате получается вложенный массив со всеми этими ключами.

Например:

[
'billingAddress.street' => 'My Street',
'foo.bar.baz' => 'biz',
]

следует расширить до:

[
'billingAddress' => [
'street' => 'My Street',
],
'foo' => [
'bar' => [
'baz' => 'biz',
]
]
]

Исходный файл «billingAddress.street» можно оставить рядом с новым массивом «billingAddress», но в этом нет необходимости (поэтому решение может работать с исходным массивом или создать новый массив). Другие элементы, такие как «billingAddress.city», возможно, должны быть добавлены к той же расширенной части массива.

Некоторые ключи могут иметь более двух токенов, разделенных точками, поэтому их нужно будет расширить глубже.

Я смотрел на array_walk_recursive() но это действует только на элементы. Для каждого соответствующего ключа элемента я на самом деле хочу изменить родительский массив, в котором находятся эти элементы.

Я смотрел на array_map, но это не обеспечивает доступ к ключам, и, насколько я знаю, не является рекурсивным.

Пример массива для расширения:

[
'name' => 'Name',
'address.city' => 'City',
'address.street' => 'Street',
'card' => [
'type' => 'visa',
'details.last4' => '1234',
],
]

Это должно быть расширено до:

[
'name' => 'Name',
'address.city' => 'City', // Optional
'address' => [
'city' => 'City',
'street' => 'Street',
],
'address.street' => 'Street', // Optional
'card' => [
'type' => 'visa',
'details.last4' => '1234', // Optional
'details' => [
'last4' => '1234',
],
],
]

То, что мне нужно, это то, что подходит каждому array во вложенном массиве и может применить к нему пользовательскую функцию. Но я подозреваю, что упускаю что-то очевидное. Платежный шлюз, с которым я работаю, отправляет мне это сочетание массивов и «притворных массивов» с использованием точечной нотации, и моя цель состоит в том, чтобы преобразовать его в массив для извлечения порций.

Я полагаю, что проблема отличается от аналогичных вопросов по SO из-за этого сочетания массивов и не массивов для расширения. Концептуально это вложенный массив, где звук группы элементов в любой уровень должен быть заменен новыми массивами, поэтому здесь происходит два уровня рекурсии: прогулка по дереву и расширение, а затем прогулка по расширенным деревьям, чтобы увидеть, нужно ли дополнительное расширение.

3

Решение

Может оказаться полезным изменить порядок ключей, которые вы получите, взорвав комбинированный (пунктирный) ключ. В этом обратном порядке легче постепенно перенести предыдущий результат в новый массив, создавая тем самым вложенный результат для одной пары ключ / значение с точками.

Наконец, этот частичный результат может быть объединен с накопленным «великим» результатом с помощью встроенного array_merge_recursive функция:

function expandKeys($arr) {
$result = [];
foreach($arr as $key => $value) {
if (is_array($value)) $value = expandKeys($value);
foreach(array_reverse(explode(".", $key)) as $key) $value = [$key => $value];
$result = array_merge_recursive($result, $value);
}
return $result;
}

Смотрите, это работает на repl.it

4

Другие решения

Вот рекурсивная попытка. Обратите внимание, что это не удаляет старые ключи, не поддерживает порядок ключей и игнорирует ключи типа foo.bar.baz,

function expand(&$data) {
if (is_array($data)) {
foreach ($data as $k => $v) {
$e = explode(".", $k);

if (count($e) == 2) {
[$a, $b] = $e;
$data[$a][$b]= $v;
}

expand($data[$k]);
}
}
}

Результат:

Array
(
[name] => Name
[address.city] => City
[address.street] => Street
[card] => Array
(
[type] => visa
[details.last4] => 1234
[details] => Array
(
[last4] => 1234
)

)

[address] => Array
(
[city] => City
[street] => Street
)

)

Объяснение:

При любом вызове функции, если параметр является массивом, перебирайте ключи и значения, ища ключи с . в них. Для любых таких ключей раскройте их. Рекурсивно вызывайте эту функцию для всех ключей в массиве.

Полная версия:

Вот полная версия, которая поддерживает несколько .s и очищает ключи потом:

function expand(&$data) {
if (is_array($data)) {
foreach ($data as $k => $v) {
$e = explode(".", $k);
$a = array_shift($e);

if (count($e) == 1) {
$data[$a][$e[0]] = $v;
}
else if (count($e) > 1) {
$data[$a][implode(".", $e)] = $v;
}
}

foreach ($data as $k => $v) {
expand($data[$k]);

if (preg_match('/\./', $k)) {
unset($data[$k]);
}
}
}
}

Вот РЕПЛ.

4

Другое решение @trincot было признано более элегантным, и это решение, которое я использую сейчас.

Вот мое решение, которое расширяет возможности и советы, данные @ggorlen

Подход, который я выбрал:

  • Создайте новый массив, а не оперируйте с начальным массивом.
  • Не нужно сохранять старые предварительно расширенные элементы. Они могут быть легко добавлены при необходимости.
  • Расширение ключей выполняется по одному уровню за раз из корневого массива, а остальные расширения возвращаются обратно рекурсивно.

Метод класса:

protected function expandKeys($arr)
{
$result = [];

while (count($arr)) {
// Shift the first element off the array - both key and value.
// We are treating this like a stack of elements to work through,
// and some new elements may be added to the stack as we go.

$value = reset($arr);
$key = key($arr);
unset($arr[$key]);

if (strpos($key, '.') !== false) {
list($base, $ext) = explode('.', $key, 2);

if (! array_key_exists($base, $arr)) {
// This will be another array element on the end of the
// arr stack, to recurse into.

$arr[$base] = [];
}

// Add the value nested one level in.
// Value at $arr['bar.baz.biz'] is now at $arr['bar']['baz.biz']
// We may also add to this element before we get to processing it,
// for example $arr['bar.baz.bam']


$arr[$base][$ext] = $value;
} elseif (is_array($value)) {
// We already have an array value, so give the value
// the same treatment in case any keys need expanding further.

$result[$key] = $this->expandKeys($value);
} else {
// A scalar value with no expandable key.

$result[$key] = $value;
}
}

return $result;
}

$result = $this->expandKeys($sourceArray)
2