PHP - backtrack through multi-dimensional array to check for recursion issues - php

I have a PHP array that outlines a parent-child relationships between objects, based on their ID. The array could potentially be infinitely deep, but the only rule is that "you may not add a child ID to a parent, where the child ID is a parent or grandparent (or great-grandparent etc etc) of said parent", in order to rule out recursive loops.
For example:
<?php
// Good relationship: 6 and 4 are children of 7, with 5 a child of 6 and so on
$good_relationship = [
'id' => 7,
'children' => [
[
'id' => 6,
'children' => [
[
'id' => 5,
'children' => [.. etc etc..]
]
]
],
[
'id' => 4,
'children' => []
]
]
];
// Badly-formed relationship: 6 is a child of 7, but someone added 7 as a child of 6.
$bad_relationship = [
'id' => 7,
'children' => [
[
'id' => 6,
'children' => [
[
'id' => 7,
'children' => [ ... 6, then 7 then 6 - feedback loop = bad ... ]
]
]
],
[
'id' => 4,
'children' => []
]
]
];
?>
I'm trying to write a function that checks for recursion issues when an ID is potentially added as a child to another ID. It would take in an ID ($candidate_id) and tries to add it as a child of another ID ($parent_id), and checks the existing array ($relationship) all the way back up the chain, and returns true if the candidate does not show up as a parent,grandparent,etc of $parent, and false if the candidate addition will cause a recursion issue by being added.
From the above $good_relationship, it would return true is I added ID 3 to ID 5, but false if I added ID 7 to ID 5.
Here's what I have so far, but I know it's way off - it's only checking for immediate grandparent of the candidate ID.
<?php
public function check_candidate($array, $candidate_id, $parent_id, &$grandparent_id = 0)
{
$reply_array = [];
foreach($array as $action)
{
// If $action['id'] is the same as the grandparent,
if($grandparent_id == $action['id'])
{
return false;
}
$grandparent_id = $action['id'];
if(isset($action['children']) && count($action['children']) >= 1)
{
$this->check_candidate($action['children'], $candidate_id, $parent_id, $grandparent_id);
}
}
return true;
}
?>
I've had a look at array_walk_recursive() in this case, but if $good_relationship has more than 1 element to it (which it always will), the callback will not know how 'deep' it is within the function, and it all becomes a bit of a nightmare.
Can anyone help me here?

Related

Get total number of occurrences of a data item from nested array

I have an array with a structure like:
$arr = [
'data1' => [ /* some data */],
'data2' => [
'sub-data1' => [
[
'id' => 1
'status' => 'active'
],
[
'id' => 2
'status' => 'not-active'
]
],
'sub-data2' => [
[
'id' => 3
'status' => 'active'
],
[
'id' => 4
'status' => 'active'
]
]
]
]
Is there a simple way in which I can count how many sub-dataxxx have any item with a status is active?
I have managed to do this with nested foreach loops, but I'm wondering if there is a more simple method?
The above example should show the number of active entries as 2, as there are 2 sub-data elements with active statuses. This ignores any non-active statuses.
Edit: To clarify my expected result
I am not wanting to count the number of status = active occurrences. I'm wanting to count the number of sub-dataxxx elements that contain an element with status = active.
So in this instance, both of sub-data1 and sub-data2 contain sub-elements that contain status = active, therefore my count should be 2.
you can do it quite easily with a function like this
function countActiveSubData(array $data): int
{
return array_reduce($data, function ($res, $sub) {
foreach ($sub as $d) {
if ('active' === $d['status']) {
return $res + 1;
}
}
return $res;
}, 0);
}
you can call it on a single data or if you want to get the result for entire $arr you can call it like this
$result = array_map('countActiveSubData', $arr);
// the result will be [
'data1' => 0,
'data2'=> 2
....
]

Laravel unique identifiers for latest foreign key in collection

I have a Laravel collection with record IDs and foreign keys:
{id=1, foreign_id=1},
{id=2, foreign_id=1},
{id=3, foreign_id=2},
{id=4, foreign_id=3},
{id=5, foreign_id=2}
I expect:
{id=2, foreign_id=1},
{id=5, foreign_id=2},
{id=4, foreign_id=3}
I want to search Laravel query builder for unique values ​​for foreign_id if id in collection occurs more than 1 time.
I want then to give latest foreign_id.
Try $collection->unique('foreign_id');
Here I'm giving an example, You can check by yours,
$a = collect([
[
'id' => 1,
'foreign_id' => 2
],
[
'id' => 2,
'foreign_id' => 1
],
[
'id' => 3,
'foreign_id' => 2
],
[
'id' => 4,
'foreign_id' => 3
],
[
'id' => 5,
'foreign_id' => 2
],
]);
$a->unique('foreign_id');
The easiest way to do it is to sort collection by "id" in descending order and than use unique method by "foreign_id"
$myCollection->sortByDesc('id')->unique('foreign_id')

Filter multi-dimensional array (recursively) inwards to outwards using custom functions

Note
This question is NOT a duplicate of
Filtering multi-dimensional array
Filter out empty array elements of multidimensional array
or several other related questions
I have a very peculiar use case where I have to filter a multi-dimensional array 'inwards to outwards', implying
"filter the innermost level elements, then it's preceding level
elements and so on until coming to filtering the topmost level"
As an (dummy, mock) example, consider this
suppose we have a nested of restaurant-ids (res_ids) grouped by countries (1, 2, & 3) & cities (11, 12, 21, 22, 23, 31)
[
1 => [
11 => [
111 => ['res_id' => 111, 'city_id' => 11, 'country_id' => 1],
112 => ['res_id' => 112, 'city_id' => 11, 'country_id' => 1],
113 => ['res_id' => 113, 'city_id' => 11, 'country_id' => 1],
],
12 => [
121 => ['res_id' => 121, 'city_id' => 12, 'country_id' => 1],
],
],
2 => [
21 => [
212 => ['res_id' => 212, 'city_id' => 21, 'country_id' => 2],
214 => ['res_id' => 214, 'city_id' => 21, 'country_id' => 2],
],
22 => [
221 => ['res_id' => 221, 'city_id' => 22, 'country_id' => 2],
222 => ['res_id' => 222, 'city_id' => 22, 'country_id' => 2],
223 => ['res_id' => 223, 'city_id' => 22, 'country_id' => 2],
],
],
3 => [
31 => [
312 => ['res_id' => 312, 'city_id' => 21, 'country_id' => 2],
314 => ['res_id' => 314, 'city_id' => 21, 'country_id' => 2],
],
]
]
and we want to remove all restaurants (plus the parent sub-array structure) having even res_ids (keep odd ones)
so that resulting output nested array is as follows
note that not only individual 'leaf' items depicting res have been filtered, but also higher level city and country items have been filtered if they contained only even res_ids (which we intended to remove)
[
1 => [
11 => [
111 => ['res_id' => 111, 'city_id' => 11, 'country_id' => 1],
113 => ['res_id' => 113, 'city_id' => 11, 'country_id' => 1],
],
12 => [
121 => ['res_id' => 121, 'city_id' => 12, 'country_id' => 1],
],
],
2 => [
22 => [
221 => ['res_id' => 221, 'city_id' => 22, 'country_id' => 2],
223 => ['res_id' => 223, 'city_id' => 22, 'country_id' => 2],
],
],
]
actually i myself created above array from a flat array by recursive grouping; but now I have to filter them in groups (which can't be done before grouping)
While i can certainly do this using nested loops, I was wondering if we can create a generic function for it (i have other such multi-dimensional filtering use-cases at different depths across my project)
Another important thing to note here that given the generic filtering criteria requirement, we would ideally like to be able to have a different filtering criteria per level: custom functions per se.
Any ideas?
You can do this lika a array_filter callback.
$currentKey is not required, but may be handy.
Working example.
function array_filter_clean(array $array, array $callbacks, $currentDepth = 0, $currentKey = '') {
if (array_key_exists($currentDepth, $callbacks)) { // identify node to apply callback to
$callback = $callbacks[$currentDepth];
if (!$callback($currentKey, $array)) { // empty node when callback returns false (or falsy)
return [];
}
}
foreach ($array as $key => &$value) { // &value to modify $array
if (is_array($value)) {
$value = array_filter_clean($value, $callbacks, $currentDepth+1, $key); // recurse if array
}
}
return array_filter($array); // remove empty nodes (you may want to add "afterCallbacks" here)
}
$callbacksByDepth = [
/* 2 => function ($key, $value) {
return $key > 20;
}, */ // test
3 => function ($key, $value) {
return $value['res_id']%2;
},
];
$output = array_filter_clean($input, $callbacksByDepth);
print_r($output);
I've added comments - in case i forgot to explain something please let me know.
Worth mentioning
This can be done with an extension of RecursiveFilterIterator within a RecursiveIteratorIterator - but the readability of the provided solution is far superior.
Note
Given the case you want to keep every node which contains at least 3 items after you've applied your callback, you will have to extend this funtion (at the last line). You could do exactly the same like above array_key_exists($currentDepth, $callbacksXXX) for another $callbacksAfter array with the same structure. (Or build everything in one array and key your callbacks with before and after - up to you)
I came up with following function that accepts a list of callables, each one of which is used for filtering the array at a single level
in-line with the original example, filtering is done 'inwards to outwards': first we filter the innermost level, then the one above that and so on (so essentially the nth filter acts on residual output obtained by applying n-1 filters before it)
have a look at the unit-tests to understand the behaviour
/**
* Filters a multi-dimensional array recursively by applying series of filtering function callables, each at a
* different level. Filtering is done starting from innermost depth and moving outwards.
* It is assumed that structure / depth of array is consistent throughout (each key grows upto same max depth)
*
* Regarding $filter_callables
* - this is a series of filtering functions (callables) applied at each level (1st callable is for first /
* top-most or outer-most level, next callable is for next level at depth 2 and so on)
* - each filter callable function should accept exactly 2 arguments: (1) the value or item and (2) the key of item
* as mandated by 'ARRAY_FILTER_USE_BOTH' flag of PHP's array_filter function
* - to skip applying filtering at a level, we can pass null (instead of callable) for that position
* - no of callables should be less than or equal to depth of array (or else exception will be thrown)
*
* see test-cases to understand further (plus detailed explaination)
* #param array $nested_array Nested array to be filtered resursively
* #param array $filter_callables List of callables to be used as 'filter' functions at each 'depth' level
* #return array Recursively filtered array
*/
public static function filterByFnRecursive(array $nested_array, array $filter_callables): array {
if (empty($nested_array) || empty($filter_callables)) {
// base case: if array is empty (empty array was passed) or no more callables left to be applied, return
return $nested_array;
} else {
// retrieve first callable (meant for this level)
$filterer = array_shift($filter_callables);
if (!empty($filter_callables)) {
// if there are more callables, recursively apply them on items of current array
$modified_nested_array = array_map(static function (array $item) use ($filter_callables): array {
return self::filterByFnRecursive($item, $filter_callables);
}, $nested_array);
} else {
// otherwise keep the current array intact
$modified_nested_array = $nested_array;
}
if (empty($filterer)) {
// if callable is NULL, return array (at current level) unmodified
// this is provided to allow skipping filtering at any level (by passing null callable)
return $modified_nested_array;
} else {
// otherwise filter the items at current level
return array_filter($modified_nested_array, $filterer, ARRAY_FILTER_USE_BOTH);
}
}
}
Do checkout this gist for bigger collection of array utility functions along with unit-tests

Validate array keys at Laravel Request

I have a Laravel Request where I need to validate the keys from an array.
The keys are the productId and I am checking if the product belongs to the user.
Here is an example of products at the POST request:
[
8 => [
'quantity' => 10,
'discount' => 10
],
9 => [
'quantity' => 10,
'discount' => 10
]
]
And bellow is the Request rules. Is it possible to check on the keys?
public function rules()
{
return [
'product.*' => 'required|exists:recipes,id,user_id,' . $this->user()->id,
'product.*.quantity' => 'required|numeric|min:0',
'product.*.discount' => 'required|numeric|min:0'
];
}
I made a temporary solution...I kept the id validation at the request.
'products.*.id' => 'required|exists:recipes,id,user_id,' . $this->user()->id,
'products.*.quantity' => 'required|numeric|min:0',
'products.*.discount' => 'required|numeric|min:0',
But at the controller the data is modified to fit the sync() method where the id is removed from the object that will be modified and setted as a key.
$products = [];
for ($i = 0; $i < count($data['products']); $i++) {
$recipes[$data['products'][$i]['id']] = $data['products'][$i];
unset($products[$data['products'][$i]['id']]['id']);
}
$budget->products()->sync($products);
$budget->products = $data['products'];
I didn't mentioned that this is a manytomany polymorphic relationships.

Eloquent recursive relation

I have an issue where I'm trying to get all descendants of an object and keep only those with a specific property.
I have these relations:
public function getChildren()
{
return $this->hasMany(self::class, 'parent_id', 'id');
}
public function allChildren()
{
return $this->getChildren()->with('allChildren');
}
And I get this type of array for example:
$array = [
0 => ['name' => 'aaa', 'type' => 0, 'parent' => null, 'children' => [
1 => ['name' => 'bbb', 'type' => 1, 'parent' => null, 'children' => []],
2 => ['name' => 'ccc', 'type' => 0, 'parent' => null, 'children' => [
3 => ['name' => 'ddd', 'type' => 1, 'parent' => 2, 'children' => []]
]]
]],
4 => ['name' => 'eee', 'type' => 0, 'parent' => null, 'children' => []]
];
For this example, I would like to remove all objects that are of type 1 and get a clean array without those only.
I don't really understand why it is possible to get all descendats of an object but not be able to pass conditions.
Thanks in advance.
A collection only solution would be something like this (place the custom macro in a Service Provider of your application):
Collection::macro('whereDeep', function ($column, $operator, $value, $nested) {
return $this->where($column, $operator, $value)->map(function ($x) use ($column, $operator, $value, $nested) {
return $x->put($nested, $x->get($nested)->whereDeep($column, $operator, $value, $nested));
});
});
Then where needed call:
$yourArray->whereDeep('type', '!=', 1, 'children');
On your example, the macro works like this:
Filter all the elements where: type != 1
(the outer array will beuntouched as both items has type => 0)
For each element of the current array:
Retrive the children property and apply the same filtering to this subarray starting with the first point of this instructions.
Replace the children property with the new children property just filtered.
Anyways, you should try to deep dive into why the relation filtering doesn't work. That solution would be more efficient if optimized correctly.
I found a great solution where there is no need of all this recursion or any of these relationship calls so I share it:
Using: "gazsp/baum"
// get your object with roots method
$contents = Content::roots()->get();
// and simply run through the object and get whatever you need
// thanks to getDescendantsAndSelf method
$myArray = [];
foreach($contents as $content) {
$myArray[] = $content->getDescendantsAndSelf()->where('type', '!=', 1)->toHierarchy();
}
return $myArray;
This works for me the same way as the other method above.

Categories