This question already has answers here:
How to access and manipulate multi-dimensional array by key names / path?
(10 answers)
Closed 5 years ago.
I'm trying to remove an element in a multidimensional array.
The element which is needs to be removed is given by a string like globals.connects.setting1. This means I'm intend to modify an array like this:
// Old Array
$old = array(
"globals" => array(
"connects" => array(
"setting1" => "some value",
"setting2" => "some other value",
"logging" => array()),
"someOtherKey" => 1
));
// After modification
$new = array(
"globals" => array(
"connects" => array(
"setting2" => "some other value",
"logging" => array()),
"someOtherKey" => 1
));
Unfortunately I do not know either the "depth" of the given string nor the exact structure of the array. I'm looking for a function like unsetElement($path, $array) which returns the array $new, if the array $old and globals.connects.setting1 is given as argument.
I'd like to avoid the use of the eval() function due to security reasons.
$testArr = [
"globals" => [
"connects" => [
'setting1' => [
'test' => 'testCon1',
],
'setting2' => [
'abc' => 'testCon2',
],
],
],
'someOtherKey' => [],
];
$pathToUnset = "globals.connects.setting2";
function unsetByPath(&$array, $path) {
$path = explode('.', $path);
$temp = & $array;
foreach($path as $key) {
if(isset($temp[$key])){
if(!($key == end($path))) $temp = &$temp[$key];
} else {
return false; //invalid path
}
}
unset($temp[end($path)]);
}
unsetByPath($testArr, $pathToUnset);
Related
To start off with, I have checked all resources I could for examples but was not able to find one that brought me close enough so i can resolve this query (simple as it may seem).
I've also seen there is a question that is the same but never resolved here: Get allDirectories() in Laravel and create a tree
I'll also just use the same sample data cause it's the exact same scenario.
I basically get an output from laravel's AllDirectories() function which output's something like this:
array:20 [▼
0 => "test"
1 => "files"
2 => "files/2"
3 => "files/2/Blocks"
4 => "files/2/Blocks/thumbs"
5 => "files/shares"
]
And I want to convert that into a multidimensional array that looks something like this:
[
["label" => "test", "path" => "test", "children" => []],
["label" => "files", "path" => "files", "children" =>
[
["label" => "2", "path" => "files/2", "children" =>
[
["label" => "Blocks", "path" => "files/2/Blocks", "children" =>
[
[
"label" => "thumbs", "path" => "files/2/Blocks/thumbs", "children" => []
]
]
]
]
],
["label" => "shares", "path" => "files/shares", "children" => []]
]
],
];
How can one go about converting the output from AllDirectories() to a multidimensional array?
Thanks in advance for any tips or tricks :)
You could exploit laravel collections and a bit of recursion to achieve what you need.
I wrote a function which works on a preprocessed output (array instead of plain string) and does the following steps:
Take all given paths and create groups based on the first segment of each path.
For each created group, take its children paths and remove the first segment from each children (filter out empty paths).
Execute convertPathsToTree function on the children paths, and assign its output result to the children key of the resulting tree structure.
Here is the code:
function convertPathsToTree($paths, $separator = '/', $parent = null)
{
return $paths
->groupBy(function ($parts) {
return $parts[0];
})->map(function ($parts, $key) use ($separator, $parent) {
$childrenPaths = $parts->map(function ($parts) {
return array_slice($parts, 1);
})->filter();
return [
'label' => (string) $key,
'path' => $parent . $key,
'children' => $this->convertPathsToTree(
$childrenPaths,
$separator,
$parent . $key . $separator
),
];
})->values();
}
Usage
First of all, let's assume the paths are assigned as a collection to a $data variable:
$data = collect([
'test',
'files',
'files/2',
'files/2/Blocks',
'files/2/Blocks/thumbs',
'files/shares',
]);
You first need to pre-process the array by splitting each path with the directory separator (/ in this example). This can be done with a simple map call:
$processedData = $data->map(function ($item) {
return explode('/', $item);
});
Then, you can use the above function and provide the transformed input to obtain the requested structure:
convertPathsToTree($processedData);
If you would rather obtain an output array instead of an collection, add ->toArray(); after the ->values() call at the end of the function.
STEPS
Convert the paths into arrays.
Find the maximum path depth.
Group paths based on their level of depth.
Merge groupings in a hierarchical format.
Reset the result's array indices/keys.
Print output.
$rawPaths = [
0 => "test",
1 => "files",
2 => "files/2",
3 => "files/2/Blocks",
4 => "files/2/Blocks/thumbs",
5 => "files/karma",
6 => "files/karma/foo",
7 => "files/karma/foo/bar",
8 => "files/shares",
];
// 1. Convert the paths into arrays.
$paths = array_map(function ($path) {
return explode("/", $path);
}, $rawPaths);
// 2. Find the maximum path depth.
$maxDepth = 0;
for ($i = 0; $i < count($rawPaths); $i++) {
if (($count = substr_count($rawPaths[$i], "/")) > $maxDepth) {
$maxDepth = $count;
}
}
// 3. Group paths based on their level of depth.
$groupings = [];
for ($j = 0; $j <= $maxDepth; $j++) {
$groupings[] = array_filter($paths, function ($p) use ($j) {
return count($p) === ($j + 1);
});
}
// 4. Merge groupings in a hierarchical format.
$result = [];
for ($depth = 0; $depth <= $maxDepth; $depth++) {
array_map(function ($grouping) use (&$result, $depth) {
setNode($result, $grouping, $depth);
}, $groupings[$depth]);
}
function setTree(&$grouping, &$depth): array
{
$pathBuilder = $grouping[$depth];
for ($i = 0; $i < $depth; $i++) {
$pathBuilder = $grouping[$depth - ($i + 1)] . "/" . $pathBuilder;
}
return [
"label" => $grouping[$depth],
"path" => $pathBuilder,
"children" => []
];
}
function setNode(&$result, $grouping, $depth)
{
$node = &$result[$grouping[0]];
if ($depth) {
for ($i = ($depth - 1); $i >= 0; $i--) {
$node = &$node["children"][$grouping[$depth - $i]];
}
}
$node = setTree($grouping, $depth);
}
// 5. Reset the result's array indices/keys.
$arrayIterator = new \RecursiveArrayIterator(array_values($result));
$recursiveIterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($recursiveIterator as $key => $value) {
if (is_array($value) && ($key === "children")) {
$value = array_values($value);
// Get the current depth and traverse back up the tree, saving the modifications.
$currentDepth = $recursiveIterator->getDepth();
for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) {
// Get the current level iterator.
$subIterator = $recursiveIterator->getSubIterator($subDepth);
// If we are on the level we want to change, use the replacements ($value), otherwise set the key to the parent iterators value.
$subIterator->offsetSet($subIterator->key(), ($subDepth === $currentDepth ? $value : $recursiveIterator->getSubIterator(($subDepth + 1))->getArrayCopy()));
}
}
}
// 6. Print output.
var_export($recursiveIterator->getArrayCopy());
// Output
array (
0 =>
array (
'label' => 'test',
'path' => 'test',
'children' =>
array (
),
),
1 =>
array (
'label' => 'files',
'path' => 'files',
'children' =>
array (
0 =>
array (
'label' => '2',
'path' => 'files/2',
'children' =>
array (
0 =>
array (
'label' => 'Blocks',
'path' => 'files/2/Blocks',
'children' =>
array (
0 =>
array (
'label' => 'thumbs',
'path' => 'files/2/Blocks/thumbs',
'children' =>
array (
),
),
),
),
),
),
1 =>
array (
'label' => 'karma',
'path' => 'files/karma',
'children' =>
array (
0 =>
array (
'label' => 'foo',
'path' => 'files/karma/foo',
'children' =>
array (
0 =>
array (
'label' => 'bar',
'path' => 'files/karma/foo/bar',
'children' =>
array (
),
),
),
),
),
),
2 =>
array (
'label' => 'shares',
'path' => 'files/shares',
'children' =>
array (
),
),
),
),
)
I would like to merge two arrays to compare old vs new values. For example, $arr1 is old values $arr2 is new values.
In case when the data is deleted $arr2 is an empty array. Example:
$arr1 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
$arr2 = [];
For this my expected output after merge is
$merged = [
"databases" => [
0 => [
"id" => [
'old' => 1,
'new' => null
],
"name" => [
'old' => "DB1",
'new' => null
],
"slug" => [
'old' => "db1",
'new' => null
],
"url" => [
'old' => "https://www.db1.org",
'new' => null
],
]
]
];
if arr2 is different then the values should be present in the new field instead of null.
For example:
$arr1 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
$arr2 = [
"databases" => [
0 => [
"id" => 5
"name" => "DB2"
"slug" => "db2"
"url" => "https://www.db2.com"
]
]
];
expected output:
$merged = [
"databases" => [
0 => [
"id" => [
'old' => 1,
'new' => 5
],
"name" => [
'old' => "DB1",
'new' => "DB2"
],
"slug" => [
'old' => "db1",
'new' => "db2"
],
"url" => [
'old' => "https://www.db1.org",
'new' => "https://www.db2.com"
],
]
]
];
Case 3 is when $arr1 is empty but $arr2 is populated:
$arr1 = [];
$arr2 = [
"databases" => [
0 => [
"id" => 1
"name" => "DB1"
"slug" => "db1"
"url" => "https://www.db1.org"
]
]
];
and the expected output is:
$merged = [
"databases" => [
0 => [
"id" => [
'old' => null,
'new' => 1
],
"name" => [
'old' => null,
'new' => "DB1"
],
"slug" => [
'old' => null,
'new' => "db1"
],
"url" => [
'old' => null,
'new' => "https://www.db1.org"
],
]
]
];
The inbuilt php functions cannot format the data in old vs new format so was wondering how to go about this? Any solutions/suggestions would be appreciated.
Update
Here is what I had tried before:
I had tried simple array_merge_recursive but it does not store the source array. So if you have $arr1 key not there, the final merged array will only have one value.
I tried some more recursive functions late in the night but failed so in essence didn't have anything to show for what I had tried. However, this morning, I came up with the solution and have posted it as an answer in case anyone needs to use it.
Interesting question, as long as a (non-empty) array on one side means to traverse into it and any skalar or null is a terminating node (while if any of old or new being an array would enforce traversing deeper so dropping the other non-array value):
It works by mapping both old and new on one array recursively and when the decision is to make to traverse to offer null values in case a keyed member is not available while iterating over the super set of the keys of both while null would represent no keys:
$keys = array_unique(array_merge(array_keys($old ?? []), array_keys($new ?? [])));
$merged = [];
foreach ($keys as $key) {
$merged['old'] = $old[$key] ?? null;
$merged['new'] = $new[$key] ?? null;
}
This then can be applied recursively, for which I found it is easier to handle both $old and $new as ['old' => $old, 'new' => $new] for that as then the same structure can be recursively merged:
function old_and_new(array $old = null, array $new = null): array
{
$pair = get_defined_vars();
$map =
static fn(callable $map, array $arrays): array => in_array(true, array_map('is_array', $arrays), true)
&& ($parameter = array_combine($k = array_keys($arrays), $k))
&& ($keys = array_keys(array_flip(array_merge(...array_values(array_map('array_keys', array_filter($arrays, 'is_array'))))))
)
? array_map(
static fn($key) => $map($map, array_map(static fn($p) => $arrays[$p][$key] ?? null, $parameter)),
array_combine($keys, $keys)
)
: $arrays;
return $map($map, $pair);
}
print_r(old_and_new(new: $arr2));
Online demo: https://3v4l.org/4KdLs#v8.0.9
The inner technically works with more than two arrays, e.g. three. And it "moves" the array keys upwards, similar to a transpose operation. Which btw. there is a similar question (but only similar, for the interesting part in context of your question it is not answered and my answer here doesn't apply there directly):
Transposing multidimensional arrays in PHP
After reviewing my own code here is the solution I came up with. I am posting it here in case someone else needs a solution for this:
/**
* Function to merge old and new values to create one array with all entries
*
* #param array $old
* #param array $new
* #return void
*/
function recursiveMergeOldNew($old, $new) {
$merged = array();
$array_keys = array_keys($old) + array_keys($new);
if($array_keys) {
foreach($array_keys as $key) {
$oldChildArray = [];
$newChildArray = [];
if(isset($old[$key])) {
if(!is_array($old[$key])) {
$merged[$key]['old'] = $old[$key];
} else {
$oldChildArray = $old[$key];
}
} else {
$merged[$key]['old'] = null;
}
if(isset($new[$key])) {
if( !is_array($new[$key])) {
$merged[$key]['new'] = $new[$key];
} else {
$newChildArray = $new[$key];
}
} else {
$merged[$key]['new'] = null;
}
if($oldChildArray || $newChildArray) {
$merged[$key] = recursiveMergeOldNew($oldChildArray, $newChildArray);
}
}
}
return $merged;
}
Note - this solution needs testing.
I am trying to extract a value from an array by searching by another value.
I have the uri value and I require the playcount value that corresponds with the uri.
What is the best approach to traverse this multi-level array and return the desired data?
My current code:
$decoded = json_decode($response, true);
$trackids = 'spotify:track:'. $trackid .'';
$key = array_search($trackids, array_column($decoded, 'playcount'));
$result = $decoded[$key]['playcount'];
echo "Result: ";
echo $result;
I think it is incomplete and not sure how to proceed from there as it doesn't work.
The $decoded array:
$decoded = [
'success' => 1,
'data' => [
'uri' => 'spotify:album:3T4tUhGYeRNVUGevb0wThu',
'name' => '÷ (Deluxe)',
'cover' => [
'uri' => 'https://i.scdn.co/image/ab67616d00001e02ba5db46f4b838ef6027e6f96'
],
'year' => 2017,
'track_count' => 16,
'discs' => [
[
'number' => 1,
'name' => null,
'tracks' => [
[
'uri' => 'spotify:track:7oolFzHipTMg2nL7shhdz2',
'playcount' => 181540969,
'name' => 'Eraser',
'popularity' => 63,
'number' => 1,
'duration' => 227426,
'explicit' => null,
'playable' => 1,
'artists' => [
[
'name' => 'Ed Sheeran',
'uri' => 'spotify:artist:6eUKZXaKkcviH0Ku9w2n3V',
'image' => [
'uri' => 'https://i.scdn.co/image/ab6761610000517412a2ef08d00dd7451a6dbed6'
]
]
]
],
[
'uri' => 'spotify:track:6PCUP3dWmTjcTtXY02oFdT',
'playcount' => 966197832,
'name' => 'Castle on the Hill',
'popularity' => 79,
'number' => 2,
'duration' => 261153,
'explicit' => null,
'playable' => 1,
'artists' => [
[
'name' => 'Ed Sheeran',
'uri' => 'spotify:artist:6eUKZXaKkcviH0Ku9w2n3V',
'image' => [
'uri' => 'https://i.scdn.co/image/ab6761610000517412a2ef08d00dd7451a6dbed6'
]
]
]
]
]
]
]
]
];
$key = array_search($trackids, array_column($decoded, 'playcount')); in this line, you make two mistake. First, there is no column like playcount in $decode array. Second, you are searching with uri key, not playcount key. There is one more thing, discs key and track inside discs key, both are multidimensional array. So if you want to fetch the exact value, This query will be,
$decoded = array_map(function($x) {
return array_column($x, 'url');
}, array_column($decoded['data']['discs'], 'tracks'));
$decoded = call_user_func_array('array_merge', $decoded);
$key = array_search($trackids, $decoded);
You're on the right track, but you need to dig down deeper into the array structure to search for the tracks within each disc.
$decoded = json_decode($response, true);
$trackid = '7oolFzHipTMg2nL7shhdz2';
$trackidString = 'spotify:track:' . $trackid;
$playcount = null;
// Loop through the discs looking for the track
foreach ($decoded['data']['discs'] as $currDisc)
{
// If we find the track, get the playcount and break out of the loop
$key = array_search($trackidString, array_column($currDisc['tracks'], 'uri'));
if($key !== false)
{
$playcount = $currDisc['tracks'][$key]['playcount'];
break;
}
}
assert($playcount == 181540969, 'We should find the expected playcount value');
echo 'Result: ' . $playcount . PHP_EOL;
I see some inefficient usages of array functions for a task that is cleanest and most performantly performed with classic nested loops. When searching traversable structures for a solitary qualifying value, it is not best practice to use array functions because they do not permit "short-circuiting" or a break/return. In other words, array functions are a poor choice when you do not need to iterate the entire structure.
While traversing your array structure, whenever you encounter indexed keys on a given label, you should implement another foreach(). There are only 2 levels with indexes on your way to the targeted uri and playcount values, so only 2 foreach() loops are needed. When you find your targeted uri, extract the value then break your loop.
Code: (Demo)
$needle = 'spotify:track:7oolFzHipTMg2nL7shhdz2';
foreach ($decoded['data']['discs'] as $disc) {
foreach ($disc['tracks'] as $track) {
if ($track['uri'] === $needle) {
echo $track['playcount'];
break;
}
}
}
An array with the following structure
[
'name' => :string
'size' => :string|int
]
must be recursively converted into an object anytime it encounters this structure in iteration.
So, I wrote these functions
// Converts an array into the instance of stdclass
function to_entity(array $file){
$object = new stdclass;
$object->name = $file['name'];
$object->size = $file['size'];
return $object;
}
// Converts required arrays into objects
function recursive_convert_to_object(array $files){
foreach ($files as $name => $file) {
// Recursive protection
if (!is_array($file)) {
continue;
}
foreach ($file as $key => $value) {
if (is_array($value)) {
// Recursive call
$files[$name] = recursive_convert_to_object($files[$name]);
} else {
$files[$name] = to_entity($file);
}
}
}
return $files;
}
Now, when providing an input like this:
$input = [
'translation' => [
1 => [
'user' => [
'name' => 'Tom',
'size' => 2
]
]
]
];
And calling it like this, it works as expected (i.e an ecountered item user is being converted to stdclass via to_entity() function:
print_r( recursive_convert_to_object($input) );
Now here's the problem:
if an input like this gets provded (i.e one item with the key 4 => [...] is added) it no longer works throwing E_NOTICE about undefined indexes both name and size.
$input = [
'translation' => [
1 => [
'user' => [
'name' => 'Tom',
'size' => 2
]
],
4 => [
'user' => [
'name' => 'Tom',
'size' => 5
]
],
]
];
Let me repeat, that the depth of the target array is unknown. So no matter what depth is, it must find and convert an array into the object.
So where's the logical issue inside that recursive function?
Not sure if this is the best method to solve the problem, but as you already have invested some time in it.
The issue is that once you have replaced some of the content in the array, you put an object back in the array. Then when you test if it's an array in
if (is_array($value)) {
which it isn't so you convert it into an entity, it's trying to convert the object again.
To stop this happening...
if ( !is_object($value)) {
$files[$name] = to_entity($file);
}
This question already has an answer here:
PHP Get value from config array
(1 answer)
Closed 7 years ago.
I have this key "this.key.exists". Key can be: a.b.c.d.e.f etc. this is only example.
And i want to check if exists in array:
$array = [ // depth of array and number of values are variable
'this' => [
'key' => [
'exists' => 'some value' // this is what am i looking for
],
'key2' => [
'exists' => 'some value' // this is not what am i looking for
],
],
'that' => [
'this' => [
'key' => [
'exists' => 'some value' // this is not what am i looking for
],
]
]
];
I need to find this key for update his value.
$array['this']['key']['exists'] = 'need to set new value';
Thanks for help
$str = "this.key.exists";
$p = &$array; // point to array root
$exists = true;
foreach(explode('.', $str) as $step) {
if (isset($p[$step])) $p = &$p[$step]; // if exists go to next level
else { $exists = false; break; } // no such key
}
if($exists) $p = "new value";