问题
I start with a nested array of some arbitrary depth. Within that array, some keys are a series of tokens separated by dots. For example "billingAddress.street" or "foo.bar.baz". I would like to expand those keyed elements to arrays, so the result is a nested array with all those keys expanded.
For example:
[
'billingAddress.street' => 'My Street',
'foo.bar.baz' => 'biz',
]
should be expanded to:
[
'billingAddress' => [
'street' => 'My Street',
],
'foo' => [
'bar' => [
'baz' => 'biz',
]
]
]
The original "billingAddress.street" can be left alongside the new "billingAddress" array, but it does not need to be (so the solution may operate on the original array or create a new array). Other elements such as "billingAddress.city" may need to be added to the same expanded portion of the array.
Some keys may have more than two tokens separated by dots, so will need to be expanded deeper.
I've looked at array_walk_recursive()
but that only operates on elements. For each matching element key, I actually want to modify the parent array those elements are in.
I've looked at array_map
, but that does not provide access to the keys, and as far as I know is not recursive.
An example array to expand:
[
'name' => 'Name',
'address.city' => 'City',
'address.street' => 'Street',
'card' => [
'type' => 'visa',
'details.last4' => '1234',
],
]
This is to be expanded to:
[
'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',
],
],
]
What I think I need, is something that walks to each array
in the nested array and can apply a user function to it. But I do suspect I'm missing something obvious. The payment gateway I am working with sends me this mix of arrays and "pretend arrays" using the dot-notation, and my objective is to normalize it into an array for extracting portions.
I believe the problem differs from similar questions on SO due to this mix of arrays and non-arrays for expanding. Conceptually it is a nested array where sound groups of elements at any level need to be replaced with new arrays, so there are two levels of recursion happening here: the tree walking, and the expansion, and then the walking of the expanded trees to see if there is more expansion needed.
回答1:
You could find it useful to reverse the order of the keys you get from exploding the combined (dotted) key. In that reversed order it is easier to progressively wrap a previous result into a new array, thereby creating the nested result for one dotted key/value pair.
Finally, that partial result can be merged into the accumulated "grand" result with the built-in array_merge_recursive
function:
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;
}
See it run on repl.it
回答2:
Here's a recursive attempt. Note that this doesn't delete old keys, doesn't maintain any key ordering and ignores keys of the type 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]);
}
}
}
Result:
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
)
)
Explanation:
On any call of the function, if the parameter is an array, iterate through the keys and values looking for keys with a .
in them. For any such keys, expand them out. Recursively call this function on all keys in the array.
Full version:
Here's a full version that supports multiple .
s and cleans up keys afterwards:
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]);
}
}
}
}
Here's a repl.
回答3:
Another solution by @trincot has been accepted as being more elegant, and is the solution I am using now.
Here is my solution, which expands on the solution and tips given by @ggorlen
The approach I have taken is:
- Create a new array rather than operate on the initial array.
- No need to keep the old pre-expanded elements. They can be added easily if needed.
- Expanding the keys is done one level at a time, from the root array, with the remaining expansions passed back in recursively.
The class method:
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)
来源:https://stackoverflow.com/questions/51573147/expand-dot-notation-keys-in-a-nested-array-to-child-arrays