I would like to apply validation rules a request containing an array but handle each exception separately, and continue with the elements that passed the validation.
Lets say I have
{ students: [ {name: 'foo'}, {name: 'barbaz' ] }
and my validator looks
like this:
$validatedStudents = request()->validate([
'students.*.name' => 'required|string|max:3'
]);
I still want to continue with student 'foo' and handle student 'barbaz' separately. Laravel will throw an exception for the entire request and don't continue with any students.
One solution to this would be to loop through students and validate each student, but this is not possible since
foreach (request()->all() as $student) { ... }
will give $student as array and not request. Is it possible to run validate on array/collection like a request?
Thanks
Related
I'm building a farming app with Laravel and Vue. There are several activities that happen everyday in the farm and an activity might need to use products (fertilizers). In the Create.vue Activity page I'm generating multiple html inputs. A select to select a product an input to enter the quantity so I end up having a selected_products array in my request. Here's a data sample when I dd($request->all)
What is the problem?
The quantity entered in the selected_products array must be less than or equal to the quantity in stock. (can't have a product with id 9 with quantity 10 (liters or kilograms) when I only have 5 (liters or kilograms) in stock for that product)
So Product and Stock are one-to-one relationship
Product.php
public function stock () {
return $this->morphOne(Stock::class, 'stockable');
}
(I'm using morphOne here 'cause stock can also include the fuel quantity for tractors just in case you're curious)
Anyways I just want the validation to fail and return an Out of stock if the quantity in stock for that very product is less than the quantity entered
By the way after the validation I'm doing this sort of thing in my ActivityController store method so I can attach date in the activity_product table
attach_product_data = [];
foreach ($request->input('selected_products') as $product) {
$attach_product_data[$product['id']] = ['quantity' => $product['quantity']];
}
$activity->products()->attach($attach_product_data);
And this is what I'm trying to accomplish here but I have no idea how
StoreActivityRequest.php
public function rules()
{
return [
'selected_products.*.id' => ['nullable'],
'selected_products.*.quantity' => ['required_with:selected_products.*.id',
function () {
/* first we should find the product with the
`selected_products.*.id` id and check if the quantity in stock is
greater than or equal to the quanity in
`selected_products.*.quantity` */
}],
];
}
After taking a look at some data sample, you might access the ID of each field in the selected_products array in a slightly different manner than your current approach.
The idea here is to no longer validate each selected_products.*.id and selected_products.*.quantity separately but rather use a validation callback on selected_products.* key in the rules array.
Here's a code sample to explain more:
public function rules()
{
return [
'selected_products' => ['array'], // Tell Laravel that we're expecting an array
'selected_products.*' => [function ($attr, $val, $fail) {
// $val is an array that contains the "id" and "quantity" keys
// Only proceed validating when "id" is present (as i saw you're using "nullable" rule on "selected_products.*.id")
if (isset($val['id'])) {
// If no quantity is selected the raise a validation exception (based on your usage of "required_with" rule)
!isset($val['quantity']) && $fail('the quantity is required when id is present.');
// At this stage we're sure that both "id" and "quantity" are present and we might check the DB using the Product modal for example.
// If no product is found using the current "id" then raise a validation failure exception
!($prd = Product::whereId($val['id'])->first()) && fail('Unknown product with ID of ' . $val['id']);
// If the quantity in the DB is less than the selected quantity then a validation failure exception is raised as well
$prd->quantity < $val['quantity'] && fail('The selected quantity exceeds the product quantity in stock.');
}
// If no "id" is found on "selected_products.*" then nothing happens (based on your usage of "nullable" and "required_with" rules
}],
];
}
Because i have spoke of the usage of the explode function above in the comments, here's a code sample that demonstrates it's usage which cannot be applied to your current data structure by the way so I'll improvise my own structure just to demonstrate.
To use the explode function, the fields should be indexed by the IDs (9, 8 and 1 as your data sample), something like this:
With that structure we have greatly reduced the size of the data being sent in the form as we have ditched the id key entirely along with quantity key and instead we simply have Product ID => Selected Quantity.
Based on that structure, we might validate the data as follows:
public function rules()
{
return [
'selected_products' => ['array'],
'selected_products.*' => [function ($attr, $val, $fail) {
// $val contains the selected quantity and $attr can help us get the respective "id"
// We can use "explode" to get the "id"
// Based on my above data sample, $attr can be "selected_products.9", "selected_products.8" or "selected_products.1"
!($id = explode('.', $attr)[1] ?? null) && $fail('Unknown product id.');
// Fetch the DB
!($prd = Product::whereId($id) && fail('Unknown product with ID of ' . $val['id']);
// Validate the selected quantity against what we have in the DB
$prd->quantity < $val && fail('The selected quantity exceeds the product quantity in stock.');
}],
];
}
In the end, I hope i have managed to land some help. Feel free to ask for more details/explanations at any time and meanwhile here are some useful links:
explode function documentation on php.net
Laravel docs about using closures (callback functions) as custom validation rules.
I have laravel livewire collection as below.
[
{
"date":"2021.09.01-0:00",
"open":110.177,
"close":110.175,
"low":110.172,
"high":110.18
},
{
"date":"2021.09.01-0:01",
"open":110.175,
"close":110.171,
"low":110.169,
"high":110.175
},
{
"date":"2021.09.01-0:02",
"open":110.171,
"close":110.173,
"low":110.17,
"high":110.176
}
]
I would like to convert them into form of square bracket collection without key name as below .
$data = [
['2021.09.01-0:00',110.177,110.175,110.172,110.18],
['2021.09.01-0:01',110.175,110.171,110.169,110.175],
['2021.09.01-0:02',110.171,110.173,110.17,110.176]
];
Any advice or guidance on this would be greatly appreciated, Thanks.
You can use collection map method:
The map method iterates through the collection and passes each value to the given callback. The callback is free to modify the item and return it
https://laravel.com/docs/8.x/collections#method-map
$expectData = $collection->map(fn($item) => [$item-> date, $item->open,...])
I am not sure what the original data is but you can map over the collection and pull the values; if Eloquent Models:
$data->map(fn ($v) => array_values($v->toArray()))
Would depend upon how you got that original Collection for how it would end up in this scenario.
If you need to restrict what attributes and the order of them returned you can use only on the model:
fn ($v) => array_values($v->only(['date', ...]))
Using laravel validation, I would like to ensure that a field is unique, but in an array context. (I have seen this and this but they don't address the array context.)
Let's suppose I have this html:
<input name="sites[1][id]"><input name="sites[1][site_mrn]">
<input name="sites[2][id]"><input name="sites[2][site_mrn]">
<input name="sites[3][id]"><input name="sites[3][site_mrn]">
In my validation rule, I want to ensure that each site's id is valid, and that the site_mrn is not blank, so I have:
public function rules()
{
return [
'sites.*.site_mrn' => 'required|min:1',
'sites.*.id' => 'exists:sites,id'
];
}
So that part works. My problem is that I want to ensure that each pair of site site_id and site_mrn are unique in the mpi_sites table, but I don't know how to access each id/site_mrn pair in the input. I want to do something like this:
'sites.*' => Rule::unique('mpi_sites')->where(function ($q) {
$q->where('site_id', $xxxxx)->where('site_mrn', $yyyyy);
})
I have a situation with a subscription form, which must have different validation rules depending on user selection.
I almost complete this, but I'm stuck in a point which need a combination of rules that I think I can't get with predefined laravel rules.
As shown in the following chart, the point is when a user select invoicing preferences, with options Digital and Printed, if user option is Printed I need at least one physical address, so street address field group OR district address fields group must be mandatory.
Mandatory field unless other field is filled can be achieved by required_without_allrule, so I've trying with no success, a combination of required_if and required_without_allrules, like the following example:
public function rules()
{
return [
...
'invoicing_preferences' => 'required',
'invoicing_email' => 'email|required_if:invoicing_preferences,digital',
'invoicing_street_name' => 'string|required_if:invoicing_preferences,printed|required_without_all:invoicing_district,invoicing_parcel',
'invoicing_street_number' => 'number|required_if:invoicing_preferences,printed|required_without_all:invoicing_district,invoicing_parcel',
'invoicing_street_flat' => 'number|required_if:invoicing_preferences,printed|required_without_all:invoicing_district,invoicing_parcel',
'invoicing_street_dep' => 'alpha_num|required_if:invoicing_preferences,printed|required_without_all:invoicing_district,invoicing_parcel',
'invoicing_district' => 'alpha_num|required_if:invoicing_preferences,printed|required_without_all:invoicing_street_name, invoicing_street_number; invoicing_street_flat,invoicing_street_dep',
'invoicing_parcel' => 'alpha_num|required_if:invoicing_preferences,printed|required_without_all:invoicing_street_name, invoicing_street_number; invoicing_street_flat,invoicing_street_dep',
...
];
}
This combination doesn't work because always results in the required_with_allrule no matter if I've checked digital at the first point.
The rules() method is a method that is expected to return array of rules. Why would I write about such an obvious thing? Well, insert any kind of validation logic inside it, which means that it can also do some evaluation of posted data and gradually build up the returning array.
public function rules()
{
$this; // holds information about request itself with all the data POST-ed
if (something) {
return []; // something is true...
}
return []; // default behaviour (ehm, something is not true)
}
Another similar approach is to use multiple arrays and in the end merge them together (build them up). Which may result in nicer code. Also do not be afraid of using one or two private methods to clean up the code.
Lets say I have a form that is a invoice. It has line items like $product[$key], $quantity[$key]. So when the form is submitted the input looks like
{
customer_id : "214"
product_id: [ "1","5", "6" ],
quantity: ["34", "1", "54"]
}
I have a model for that details table. What I have been doing is iterating over it and creating a details object then saving it like this
foreach($product as $key=>$p)
{
if($p)
{
$t = new Details;
$t->product = $p;
$t->quantity = $quantity[$key];
$t->save();
}
}
I'm guessing there is a way to be much more efficient about this. Like creating a collection of details straight from the input but I have no idea how I would accomplish that
You can instantiate models through mass assignment.
$details = new Details(['product_id'=>'1','quantity'=>'34']);
You can also specify columns of the table that you do not want to be mass assigned using the $guarded variable in the model.
Check out mass assignment in Laravel's docs: http://laravel.com/docs/eloquent#mass-assignment
For your particular issue it looks like you would need to build your input array out of the elements of the other arrays.
Seems it isn't possible. Here is Taylor responding to a issue request. It seems the problem is it wouldn't be possible to fire events then. I just ended up doing
$d = array();
foreach ($details as $detail) {
$d[] = new OrderDetail($detail);
}
$order->details()->saveMany($d);