Laravel: how to validate that required subarray items are present? - php

In my laravel POST endpoint, Im expecting an "items" parameter in the following form:
$items => [
['item' => 'a', 'item_slot' => 1],
['item' => 'b', 'item_slot' => 2],
['item' => 'c', 'item_slot' => 3],
]
I want to validate that all the required item slots were provided.
I have an array of required slots $requiredItemSlots = [1, 2]
What validation rules should I use to make sure that item slots in $requiredItemSlots were present? Note, i don't want to limit the provided slots to the required ones, i just need to make sure that the required slots were filled.
For now I have something like:
'items.*.item' => 'required',
'items.*.item_slot' => 'required|distinct'
To ensure that no duplicate slots were passed in.
Originally I tried doing
'items.*.item_slot' => Rule::in($requiredItemSlots)
But that's not correct because not all the slots are necessarily required.

You'll need to make a custom rule, something like that:
'items' => [
'required',
'array',
function ($attribute, $value, $fail) {
$required = [1, 2];
// Cast to collection for easier checks
$items = collect($value);
foreach ($required as $r) {
if (! $items->firstWhere('item_slot', '=', $r)) {
$fail("$r has to be present in items.");
}
}
},
], // ..other validation rules

You may use a combination of distinct, in:1,2,3 and size:3 to validate the input:
'items' => 'required|array|size:3',
'items.*.item_slot' => [
'required',
'distinct',
Rule::in($requiredSlots),
]
With size:3 you force the array to have exactly 3 elements. With distinct you make sure there are no duplicates in the item_slot element field. And with Rule::in($requiredSlots) you ensure that there are no unknown item_slots given.

Related

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

How can I manage some irregular array to regular array?

I write code with some array that have different structure, but I must extract the data to do something else. How can I manager these array?
The array's structure are as follow:
$a = [
'pos1' => 'somedata',
'pos2' => ['data2', 'data3'],
'pos3' => '';
];
$b = [
[
'pos1' => ['data1', 'data2', ['nest1', 'nest2']],
'pos2' => ['data1', 'data2', 'data3'],
],
['data1', 'data2'],
'data4',
];
The array's Index can be a key or a position, and the value of the corresponding index may be a array with the same structure. More tough problem is that the subarray can be nesting, and the time of the nesting has different length.
Fortunately, every array has it's owe fixed structure.
I want to convert the these array to the format as follow. When the index is a value, change it to the keyword; and if the index is a keyword, nothing changed.
$a = [
'pos1' => 'somedata',
'pos2' => [
'pos2_1' => 'data2',
'pos2_2' => 'data3'
],
'pos3' => '';
];
$b = [
'pos1' => [
'pos1_1' => [
'pos1_1_1' => 'data1',
'pos1_1_2' => 'data2',
'pos1_1_3' => [
'pos1_1_3_1' => 'nest1',
'pos1_1_3_2' => 'nest2',
],
],
'pos1_2' => [
'pos1_2_1' => 'data1',
'pos1_2_2' => 'data2',
'pos1_2_3' => 'data3',
],
],
'pos2' => ['data1', 'data2'],
'pos3' => 'data4',
];
My first solution is for every array, write the function to convert the format(the keyword will specify in function). But it is a huge task and diffcult to manage.
The second solution is write a common function, with two argument: the source array and the configuration that specify the keyword to correspondent value index. For example:
$a = [0, ['pos10' => 1]];
$conf = [
// It means that when the value index is 0, it will change it into 'pos1'
'pos1' => 0,
'pos2' => 1,
];
The common funciton will generate the result of:
$result = [
'pos1' => 0,
'pos2' => ['pos10' => 1],
]
But this solution will lead to a problem: the config is diffcult to understand and design, and other people will spend a lot of time to understand the format after conversion.
Is there are some better solution to manage these array that other people can easy to use these array?
Thanks.

How to get duplicate array out of an array

I have an array with orders. Example:
$orders = [
'0' => [
'ordernumber' => 1,
'customer' => [
'phone' => '0123456789',
'mobile' => '0612345678'
],
],
'1' => [
'ordernumber' => 2,
'customer' => [
'phone' => '0123456789',
'mobile' => '0612345678'
],
],
'2' => [
'ordernumber' => 3,
'customer' => [
'phone' => '0987654321',
'mobile' => '0687654321'
],
],
'3' => [
'ordernumber' => 3,
'customer' => [
'phone' => '0123456789',
'mobile' => '0612345678'
],
]
];
I want to sort these orders. As you can see there can be orders where the same customer (customer with same phone number, this can be either same phone number or same mobile number) has multiple orders. I want to put all the orders that have the same phone number (doesn't matter if the phone number matches or the mobile number) in an array $duplicateOrders and all the "single" orders (orders that dont match a phone number) in an array $singleOrders. At the end the orders array must be empty. But no order can be lost or be in both arrays.
I have tried to loop through the orders with a foreach loop where I put every order in the $singleOrders array and unset it from the $orders array. I than try to match that order with another foreach loop to all the remaining orders in $orders. If I get a match i put that order (this is done once) in the $duplicateOrders array and every match of it also (I unset every match also from the $orders array). If the orders array is empty I stop, otherwise the first foreach loops kicks in and takes the next order and the proces repeats. This is my code:
protected function splitDuplicateOrders()
{
$singleOrderKey = 0;
if ($this->orders) {
foreach ($this->orders as $key => $order) {
if (count($this->orders) == 0) {
break;
}
array_push($this->singleOrders, $order);
unset($this->orders[$key]);
$orderPushed = false;
foreach ($this->orders as $otherKey => $value) {
if ($order->customer->phone == $value->customer->phone || $order->customer->mobile == $value->customer->mobile) {
if (!$orderPushed) {
array_push($this->duplicateOrders, $order);
}
array_push($this->duplicateOrders, $value);
unset($this->orders[$otherKey]);
unset($this->singleOrders[$singleOrderKey]);
$orderPushed = true;
}
}
$singleOrderKey++;
}
}
}
I expected to have an $duplicateOrders array with all the duplicates and a $singleOrders array with all the singles. I tested this with an $orders array of a total of 4 orders where 2 of them were duplicates and 2 were singles. The function sorted it nicely (but only if the orders aren't right after each other, if they are it still sorts the duplicates right but leaves one also in the $singleOrders and than I have 5 orders). Than I tested it where there were 3 duplicates and 1 single order. The $duplicateOrders array was correct but in the $singleOrders the single order was placed but also one duplicate order from the $orders array. It somehow removed the 2 duplicates correct but left one duplicate in the $singleOrders array.
Can someone help me to debug this or provide a different approach? I have been trying to solve this for 2 days but no success.
You could make use of Laravel Collections, in this case I'm gonna use the partition() method. From the documentation:
partition()
The partition method may be combined with the list PHP function to
separate elements that pass a given truth test from those that do not:
$collection = collect([1, 2, 3, 4, 5, 6]);
list($underThree, $equalOrAboveThree) = $collection->partition(function ($i) {
return $i < 3;
});
$underThree->all();
// [1, 2]
$equalOrAboveThree->all();
// [3, 4, 5, 6]
So in your case:
$orders = /** query or API to get orders as array */;
list($repeated, $single) = collect($orders)
->partition(function ($order) use ($orders) {
return $orders->where('customer.phone', $order['customer']['phone'])->count() > 1
OR $orders->where('customer.mobile', $order['customer']['mobile'])->count() > 1;
});
// now you can use them:
$repeated->all();
$single->all();
Notice that this two newly created objects ($repeated and $single) are in fact also instances of the Collection class (as you can see where I used the all() method on each), so you can keep constraining/sorting/customizing them with the help of the Collection's methods.

Laravel request validation required field if other field values all equal 0

I have an array in my request:
['progress_group'] = [0, 0];
['fields'] = [];
If all values in progress_group have the value '0', then the field: fields, should be required. How do I implement this?
I've tried:
$rules = [
'progress_group.*' => 'required',
//'fields' => 'present',
'fields.*' => 'required_if:progress_group.*,0'
];
So:
['progress_group'] = [0, 0];
means fields is required, but
['progress_group'] = [0, 1];
means it is not required to fill in..
required_if compares each element from one array with the one you are comparing with, so it will be progress_group[0] == fields[0] and so on for each item.
What you need is I guess a sum of all the values to be either 0 than it is required, and if the sum is bigger than 0 then it is not required.
So you can make a custom rule, or update your validation as such:
$total = array_sum(request()->input('progress_group'));
if($total == 0) {
$rules['fields.*'] = 'required';
}
I think you had the right idea to use requiredIf, but I think youmay have got the use and syntax wrong, I think this is correct, or will help you on the way to solving your problem:
Validator::make($request->all(), [
'progress_group.*' => 'required',
'fields' => 'present',
'fields.*' => Rule:requiredIf:(progress_group.*, 0),
]);

PHP re-group array by each column to multiple array without loop

PHP re-group array by each column to multiple array without loop
In Laravel DB return, i.e. ->get()->toArray() result is as such:
[
['ts' => 1234, 'a' => 3, 'b' => 2],
['ts' => 1244, 'a' => 2, 'b' => 6],
['ts' => 1254, 'a' => 8, 'b' => 3],
]
Is there any way that I am able to transform the data to as such:
[
['column' => 'a', 'values' => [[1234, 3], [1244, 2], [1254, 8]],
['column' => 'b', 'values' => [[1234, 2], [1244, 6], [1254, 3]],
]
I just wish to know if there's any best / efficient way to render the transformation as described above. Avoid re looping again as data is already available, it can be thought as a data formatting question. Also, I do not wish to use additional array if possible.
Things that I've already looked includes ->get()->map(function()), however this is sequential and I am not able to get values in 2D array as a result.
You'll want to map over your items and return the ts value, as well as the value for each column, respectively:
$items = Model::all();
$result = [
[
'column' => 'a',
'values' => $items->map(function ($item) {
return [$item->ts, $item->a];
})
],
[
'column' => 'b',
'values' => $items->map(function ($item) {
return [$item->ts, $item->b];
})
],
];
If you want to combine the logic for both columns, create a collection of column names, and map over those:
$items = Model::all();
$result = collect(['a', 'b'])->map(function ($column) use ($items) {
$values = $items->map(function ($item) use ($column) {
return [$item->ts, $item->{$column}];
});
return compact('column', 'values');
});

Categories