Laravel validation a field in an array - laravel

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.

Related

WinterCMS/ OctoberCMS builder - sorting product in categories

I built the plugin (with builder) where I have products and categories. Product and categories are "conected" by relation table. I have list of products and another list of categories. On page where I list my all categories everthing works fine. The problem is with the single category view. Now products in a single category are listed by add order. But I want to have my own order of products or some sort of reordering. Builder delivers sorting in record list (categories page) but not in record details (single category page).
Thanks for answers.
In your model, you have already added relationship like category <-> products
Here you can add an order option to set the order.
// inside category model
public $hasMany = [
'products' => [
\Acme\Shop\Models\Product::class,
'order' => 'title desc', // <- here although its hardcoded
]
];
Or if you prefer dynamic order then inside your page code section you can add the onStart function and fetch the product manually.
function onStart() {
$id = 1; // just for example you can get category based on slug
$sortedProducts = Category::find($id)
->products()->orderBy('title')->get();
// dynamic as per your need ^
$this['products'] = $sortedProducts;
}
// now in the code section you can use `products` and they will be sorted
if any doubt please comment

How to validate a relation property in Laravel?

What I have
I have a Room model with a has many relation to Reservation. Room has a property is_open indicating that the given room is available.
Goal
I would like to be able to validate in the rules function of a FormRequest that a room is available when I store a new reservation based on the is_open property.
What I tried
The validated data is in an array so the rule key needs to be this: data.room_id. I know that exists can validate something in the database but I don't know how its syntax works. Also how can I validate a property of a relation?
class ReservationStoreRequest extends FormRequest {
public function authorize() {
return true;
}
public function rules() {
return [
'data.room_id' => 'exists:' // what goes here?
];
}
}
tl;dr
public function rules() {
return [
'data.room_id' => [
Rule::exists('rooms', 'id')->where(function ($query) {
$query->where('is_open', 1);
})
]
];
}
Validating with exists
There is documentation on the exists rule but it is really concise. Lets see what the commands in the docs actually mean. The point of this rule is that it can validate the existence of a field in a table in your db given by some criteria. For example, when you save a reservation you can validate that the room you are reserving is exists in the database.
Basic usage
'size' => 'exists:rooms'
This will check the rooms table if there is an entry where the size column contains the value that is the value under validation (in this case the corresponding value of key size which is large). This results in the following SQL query:
select count(*) as aggregate from `rooms` where `size` = "large";
Specifying a custom column name
In the previous section the validated column's name comes from the key of the validated property ('size'). You might want to give a different column name because you are validating a column that is named differently from your data's key (e.g. 'size' is stored in the 'category' column) another case is that your rooms table's primary key is just id instead of room_id like in the validated data. In your case the validated data's key is data.room_id but you would like to check the id column of rooms. You can specify the column's name separated by a comma:
'data.room_id' => 'exists:rooms,id'
This will check the rooms table if there is an entry where the id column contains the value under validation resulting in the following SQL query:
select count(*) as aggregate from `rooms` where `id` = 4;
Specify a custom column name with extra clauses
If you need to further filter the results you can use the Illuminate\Validation\Rule class. You pass the table and column name (the second being optional just like in the basic usage section) and in an anonymous function add the extra clauses to the query. You can use every input data in the request, just retrieve it with $this->input('propertyName') or $this->all() then processing it by hand:
'data.room_id' => [
Rule::exists('rooms', 'id')->where(function ($query) {
$query
->where('is_open', 1);
->where('size', $this->input('data.size'));
})
]
The resulting SQL query is this:
select count(*) as aggregate from `rooms` where `id` = 4 and (`is_open` = 1 and `size` = large);
The available clauses in the $query variable are where, whereNot, whereNull, whereNotNull, whereIn and whereNotIn. Check out the Illuminate/Validation/Rules/Exists class docs.

Validate if another field is empty laravel validation

I want to validate field if present and will fail if another field not present
How to do it ? I have create custom validation to get record based id on storage
I have field product_id and amount and the amount field validate between max and min based on product_id, if i present the amount = 100000 and not fill the product_id field the validating for amount field is passed and display error because i am not fill the product_id i want check before the custom validation execute will check if product_id empty or not..
I have read all validation method on laravel docs still didnt find the solution.
How to do it ?
Finally i found a solution...
on Form Request class i just add sometimes rule on validate instance so when the product_id field not present on amount field will not validate and when present it will validate using validation rules like this:
class StoreRequest extend FormRequest
{
protected function getValidatorInstance()
{
$validator = parent::getValidatorInstance();
$validator->sometimes('amount', 'required|numeric|max_amount:product_id', function ($input) {
return Arr::has($input, 'product_id');
});
return $validator;
}
}

Laravel saving ordered list of eloquent models

I'm creating a food menu which the administrator can order/sort by dragging and dropping. This menu consists of multiple categories (ProductCategory) and products (Product).
I'm using HTML5Sortable on the client-side to allow nested d&d. The markup is pretty simple:
<div class="categories">
#foreach($categories as $category)
<div class="category">
#foreach($category->products as $product)
<div class="products">
<div class=""product" data=id="{{ $product->id }}">
{{ $product->name }}
</div>
</div><!-- /products !-->
#endforeach
</div><!-- /category !-->
#endforeach
</div>
And the corresponding javascript:
$('.categories').sortable({
items: '.category'
});
$('.products').sortable({
items: '.product'
});
// Will be called when the user is done repositioning the products and categories
function getOrderedList() {
var data = {};
$('.categories').find('.category').map(function(i) {
var category = $(this);
data[i] = {};
data[i].id = category.data('id');
data[i].products = category.find('.product').map(function() {
return $(this).data('id');
}).get();
});
data = JSON.stringify(data); // Send data to server
}
The function getOrderedList will send a JSON string back to Laravel, which contains the sorted category id's and product id's:
{"0":{"id":1,"products":[2,3,1,4,5,6,7,8,9,10]},"1":{"id":2,"products":[11,12,13,14]},"2":{"id":3,"products":[15,16,17,18]}}
How would I make this work on the back-end? I guess I must store this array somewhere in the database and later find and order the models by the id's?
In short: What is a clean and flexible solution for sorting (nested) models (within Laravel)?
A common convention is Weight, add a field called (Int)Weight on the products table, which is used to define the order of the items.
Once a change in the order occurs you only update the weight field.
When you retrieve the items, you sort them by Weight.
it becomes similar to an Array
Id Name Weight
01 'product 1' 2
02 'product 2' 0
03 'product 3' 1
when you order it by weight you get
product 2
product 3
product 1
it's similar to an array because
$products[0] = 'product 2'
$products[1] = 'product 3'
$products[2] = 'product 1'
Note that if you want to make it even more dynamic, you can create a polymorphic model that can satisfy multiple models.
Please refer to https://laravel.com/docs/5.1/eloquent-relationships#many-to-many-polymorphic-relations
Polymorphic Relations example
Create table Weights (migration example)
$table->increments('id');
$table->integer('value');
$table->integer('weightable_id')->unsigned();
$table->string('weightable_type');
Create model Weight
class Weight extends Eloquent
{
public function weightable()
{
return $this->morphTo();
}
}
now with any other model
class Products extends Eloquent
{
...
public function weight()
{
return $this->morphOne(Weight::class);
}
}
this way you can just add that method to any model you want then you can sort your model with it.
P.S. make sure any model that uses it, creates that relation immediately after creating the model
i do not recommend this method, it's much better if you explicitly define the weight field in the Products table, i understand how much you want your code to be dynamic, but everything comes at a cost
Performance goes down, it's not easy to visualize your code once you establish polymorphic relations, its more like starting to use Jumps instead of Functions
First, the JSON that you are producing shouldn't be an object where the keys are just array indices. Instead it should be an array of objects that looks like this:
[{"id":1,"products":[2,3,1,4,5,6,7,8,9,10]},{"id":2,"products":[11,12,13,14]},{"id":3,"products":[15,16,17,18]}]
Since the products table to product_categories table has an obvious many to one relationship, you'd just use the product_categories_id foreign key on the products table to represent the relationships laid out in your JSON.
In the nested objects of your JSON, every value in the products key array will have a foreign key that corresponds to the id key value in the same nested object (this is the product_category_id column on your products table).
Your API endpoint function would then look something like this:
public function myApiEndpoint(){
$input = Input::get('input');
$input = json_decode($input, true);
foreach($input as $category){
Product::whereIn('id', $category['products'])->update(array(
'product_category_id' => $category['id']
));
}
}
I am updating the model directly in the API controller here, but you should really do any model changes through a repository that's also implementing an interface.
The above will work if you only ever have one menu (with it's categories and products). If you want multiple menus, then you'll need a menus table along with a three way pivot table (with columns menu_id, product_id, and product_category_id).
I just implement this behavior using this library:
https://github.com/spatie/eloquent-sortable
It is very simple to implement, basically you need an extra column to keep the order and the library will do the rest, here is a part of the documentation:
Implement the Spatie\EloquentSortable\Sortable interface.
Use the trait Spatie\EloquentSortable\SortableTrait.
Optionally specify which column will be used as the order column. The default is order_column.
use Spatie\EloquentSortable\Sortable;
use Spatie\EloquentSortable\SortableTrait;
class MyModel extends Eloquent implements Sortable
{
use SortableTrait;
public $sortable = [
'order_column_name' => 'order_column',
'sort_when_creating' => true,
];
...
}

Display collection of Shopping cart rules and products categories associated to each rule

I want to check if there is a sales promotion on the product then stick the promotion label on that product on category list page. But I don't know how to loop through all the shopping cart rules and retrieve the products/categories associated to each rule.
EDITED
Thanks seanbreeden, but I can't pull the skus from $conditions. var_dump($conditions); shows this:
{s:4:"type";s:32:"salesrule/rule_condition_combine";s:9:"attribute";N;s:8:"operator";N;s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";}a:7:{s:4:"type";s:32:"salesrule/rule_condition_combine";s:9:"attribute";N;s:8:"operator";N;s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";s:10:"conditions";a:1:{i:0;a:7:{s:4:"type";s:42:"salesrule/rule_condition_product_subselect";s:9:"attribute";s:3:"qty";s:8:"operator";s:2:">=";s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";s:10:"conditions";a:1:{i:0;a:5:{s:4:"type";s:32:"salesrule/rule_condition_product";s:9:"attribute";s:12:"category_ids";s:8:"operator";s:2:"==";s:5:"value";s:2:"23";s:18:"is_value_processed";b:0;}}}}}a:7:{s:4:"type";s:32:"salesrule/rule_condition_combine";s:9:"attribute";N;s:8:"operator";N;s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";s:10:"conditions";a:2:{i:0;a:5:{s:4:"type";s:32:"salesrule/rule_condition_address";s:9:"attribute";s:13:"base_subtotal";s:8:"operator";s:2:">=";s:5:"value";s:2:"45";s:18:"is_value_processed";b:0;}i:1;a:7:{s:4:"type";s:42:"salesrule/rule_condition_product_subselect";s:9:"attribute";s:3:"qty";s:8:"operator";s:2:">=";s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";s:10:"conditions";a:1:{i:0;a:5:{s:4:"type";s:32:"salesrule/rule_condition_product";s:9:"attribute";s:3:"sku";s:8:"operator";s:2:"==";s:5:"value";s:46:"test-config, BLFA0968C-BK001, BLFA0968C-CR033X";s:18:"is_value_processed";b:0;}}}}}a:6:{s:4:"type";s:32:"salesrule/rule_condition_combine";s:9:"attribute";N;s:8:"operator";N;s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";}a:6:{s:4:"type";s:32:"salesrule/rule_condition_combine";s:9:"attribute";N;s:8:"operator";N;s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";}a:7:{s:4:"type";s:32:"salesrule/rule_condition_combine";s:9:"attribute";N;s:8:"operator";N;s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";s:10:"conditions";a:1:{i:0;a:7:{s:4:"type";s:42:"salesrule/rule_condition_product_subselect";s:9:"attribute";s:3:"qty";s:8:"operator";s:2:">=";s:5:"value";s:1:"1";s:18:"is_value_processed";N;s:10:"aggregator";s:3:"all";s:10:"conditions";a:1:{i:0;a:5:{s:4:"type";s:32:"salesrule/rule_condition_product";s:9:"attribute";s:3:"sku";s:8:"operator";s:2:"==";s:5:"value";s:16:"BLFA0968C-CR033X";s:18:"is_value_processed";b:0;}}}}}
but when I loop through $conditions i.e.
$rules = Mage::getResourceModel('salesrule/rule_collection')->load();
foreach ($rules as $rule) {
$conditions = $rule->getConditionsSerialized();
foreach ($conditions as $condition) {
var_dump($condition);
}
}
it doesn't show anything so don't really know how to pull skus here.
EDIT2
As Alaxandre suggested, I'm not using unserialized approach. I'm doing it like this now:
$rules = Mage::getResourceModel('salesrule/rule_collection')->load();
foreach ($rules as $rule) {
if ($rule->getIsActive()) {
//print_r($rule->getData());
$rule = Mage::getModel('salesrule/rule')->load($rule->getId());
$conditions = $rule->getConditions();
$conditions = $rule->getConditions()->asArray();
foreach( $conditions['conditions'] as $_conditions ):
foreach( $_conditions['conditions'] as $_condition ):
$string = explode(',', $_condition['value']);
for ($i=0; $i<count($string); $i++) {
$skus[] = trim($string[$i]);
}
endforeach;
endforeach;
}
}
return $skus;
And then checking in list page if sku matches within $skus array then show the label. But again there are limitation with this approach as well. I'm think of another approach (I'm not sure if thats is possible).
Thinking of creating a new table (to save the sales rules products).Everytime save the sales rule, catch the save rule event and update the table with Rule name and all the associated products. Then on the list page check that table, if products exist in the table, show the appropriate label. Now I think the event is adminhtml_controller_salesrule_prepare_save (not 100% sure) but I don't know how to get the sku from the rule condition in the observer to save in the new table.
I would suggest you to do it like this. When you had a product to cart, each rules are checked to calculate the final price and reduction. You can know which rules are applied to each item of your cart. In the table sales_flat_quote_item you have the column applied_rule_ids. I think you can access to this in php, by a function getAllItemsInCart or something like this (you have to find out). After you do $item->getAppliedRuleIds() and finally you can get the name of the rule apply to an item (product).
Good luck :)
Edit:
I read again your request and I think my answer doesn't fit with your request.
Your case is even more complicated. For each product on your catalog page you have to apply all the rules of your website. But Mage_SalesRule_Model_Validator process expect item and not product...
If you have lot of rules this task will slow down your catalog and this is really not good! The best would be to cache this result of the rules label in the database, may be in the table catalog_category_product or... (and even better to generate this cache automatically).
Edit2:
Other possibility would be to have a new field in rule creation where you set manually the related products (sku). You save this data in the table salesrule or in a new table salesrule_related_sku.
Then when you display the catalog you check for the sku and if the rule still active.
This solution would be the easiest one :-)
You could pull the getMatchingProductsIds from /app/code/core/Mage/CatalogRule/Model/Rule.php and compare them with the skus displayed on the category list page.
$catalog_rule = Mage::getModel('catalogrule/rule')->load(1); // ID of your catalog rule here, or you could leave off ->load(1) and iterate through ->getCollection() instead
$catalog_rule_skus = $catalog_rule->getMatchingProductIds();
hth
EDIT
Here's a way to get the serialized conditions:
$rules = Mage::getResourceModel('salesrule/rule_collection')->load();
foreach ($rules as $rule) {
$conditions = $rule->getConditionsSerialized();
var_dump($conditions);
}
EDIT 2
There would have to be a better way to do this. The only way I could pull that data was to unserialize then iterate with foreach through each layer. Anyone have any better ideas for this? This works but is very sloppy.
$rules = Mage::getResourceModel('salesrule/rule_collection')->load();
foreach ($rules as $rule) {
if ($rule->getIsActive()) {
$conditions = $rule->getConditionsSerialized();
$unserialized_conditions = unserialize($conditions);
$unserialized_conditions_compact = array();
foreach($unserialized_conditions as $key => $value) {
$unserialized_conditions_compact[] = compact('key', 'value');
}
for ($i=0;$i<count($unserialized_conditions_compact);$i++) {
if (in_array("conditions",$unserialized_conditions_compact[$i])) {
foreach($unserialized_conditions_compact[$i] as $key => $value) {
foreach($value as $key1 => $value1) {
foreach($value1 as $key2 => $value2) {
foreach($value2 as $key3 => $value3) {
$skus[] = explode(",",$value3['value']);
}
}
}
}
}
}
}
}
var_dump($skus);
The rules are associated to all product for a website. There is no rules set for a specific products/categories from the database point of view. For each product in the cart, Magento will validate all the rules you have for a website. This operation is done in the class Mage_SalesRule_Model_Validator. The only way to solve your request is to extend the function process from this class (at least I think so :p).
I wanted the same thing as you want. I wanted to get associated SKUS, Category Ids and any other conditions value to generate Google feeds to be used in Google merchant promotions.
I have used the recursive function to reach to last children of the condition and fetch its value.
I am checking based on the attribute value of the condition. If an attribute value is blank then go one step down and check if attribute value present and if so then fetch the value of it otherwise continue to go down.
Here is the code that I used to fetch values. Which will also work for the case, when two conditions are on the same level.
public function get_value_recursively($value){
foreach($value as $key => $new_value) {
if(strlen($new_value[attribute]) == 0){
$value = $new_value[conditions];
return $this->get_value_recursively($value);
}else{
$resultSet = array();
if (count($value) > 1){
for ($i=0;$i<count($value);$i++) {
$resultSet[] = array('attribute' => $value[$i][attribute], 'value' => $value[$i][value]);
}
$result = $resultSet;
}else{
$result = array('attribute' => $new_value[attribute], 'value' => $new_value[value]);
}
return json_encode($result, JSON_FORCE_OBJECT);
}
}
}
according to #seanbreeden answer you can call this function from first foreach
It will return the result like this :
{"0":{"attribute":"category_ids","value":"5, 15"},"1":{"attribute":"sku","value":"msj000, msj001, msj002"}}
P.S. I am not PHP dev. So, Ignore layman style code. :)

Resources