CakePHP 3.x - Validation with double association - validation

My database has Products, Shops and Branches. A Branch belongsTo a Shop, a Product belongsTo a Shop, but also optionally belongsTo a Branch.
Question: how do I (elegantly) validate from a Product whether the selected Branch is actually of the same Shop as the Shop selected for a Product?
I am already using buildRules like $rules->existsIn to verify whether the selected Shops and Branches exist in the first place. I feel like I should be able to extrapolate these functions so these are cross-checked as well.
Note: I am specifically asking for a validator. I realise I can add a constraint to the Branches->find('list')->... call so the user can only select the correct ones, however, having a validator as well seems safe.

Okay, this I solution I applied. I am not entirely satisfied because it doesn't seem elegant, but it certainly works:
public function buildRules(RulesChecker $rules)
{
// ...
$rules->add(function ($entity, $options) {
$branch = $this->Branches->find('all')->where(['Branch.id' => $entity->branch_id])->first();
if (is_null($branch))
return true;
return $branch->shop_id == $entity->shop_id;
}, 'branchCheck', ['errorField' => 'branch_id', 'message' => 'Branch does not belong to the specified shop']);
// ...
}

Related

MorphTo with conditions

I am working with field MorphTo and I try to make conditions for the resources.
For example I have 4 resources:
Accounts
PaymentMethodCreditCard
PaymentMethodBankAccount
Transactions
Every Account can add as many Payment Methods as he wants.
And in the transaction I work with MorphTo to select the Payment Method that the account selected.
My problem starts when I try to create a transaction from Nova and get a list of all the Payment Methods in the db without any relation to the account.
My ideal idea was like that:
MorphTo::make('Transactions')
->withoutTrashed()
->dependsOn('Account', function (MorphTo $field, NovaRequest $request, FormData $formData) {
if(isset($formData->Account)){
$field->types([
PaymentMethodCreditCard::class => fn($q)=>$q->where('account_id', $formData->Account),
PaymentMethodBankAccount::class => fn($q)=>$q->where('account_id', $formData->Account),
]);
}
}),
But of course it will not work, Someone has any idea how I can add conditions to the resource?
I'll give you two answers. Since the question does not exactly clarify whether the Account value is changable during the edit process or it's predefined.
Account value does not change.
If the account value does not change after the resource is loaded, then the solution you are looking for is Relatable Filtering
public static function relatablePaymentMethods(NovaRequest $request, $query)
{
$resource = $request->findResourceOrFail();
return $query->where('account_id', $resource->Account);
}
Account value can change
If the account value can change, then you'll need to create your own "Morph To" behavior using Select and Hidden fields.
With Select field, you can utilize the dependsOn and options functions, to change the query results when the Account changes.

Laravel resource return array element check against different model

The subject of this question was a bit hard to formulate.
So i'll try to describe as detailed as possible my problem/question.
1 have 2 models.
Category
Product
On my product edit page i render a tree (jstree) with categories, for each category the product belongsTo the node of the tree needs to be checked.
My current approach is in the categoryResource i check if the category id belongs to the product, but this (of course) returns in a lot of queries as per category a query is fired to determine if the category belongs to the product.
public function toArray($request)
{
// got a product id?
if (request('product_id')) {
$categories = Product::findOrFail(request('product_id'))->categories->pluck('id');
}
return [
'checked' => !is_null($categories) && $categories->contains($this->id)
];
}
Other solution would be to load product categories in the controller and loop rebuild the 'resource' but this just doesn't feel right and also i need to be build recursive stuff (for child categegories) which Resource model can already handle.
Anyone got a more proper and solid solution?
If more information is required i'm happy to oblige :)

How to specify a default value for a field Laravel Nova

I want to set the default value of a resource field to the authenticated user's id. I have a model called Note which has a one to many relationship with Game and User.
User hasMany Note
Game hasMany Note
Note belongsTo User
Note belongsTo Game
In Laravel Nova my fields looks like this for the note
ID::make()->sortable(),
Text::make('Note', 'note')->onlyOnIndex(),
Textarea::make('Note', 'note')->alwaysShow(),
BelongsTo::make('Game', 'game')->hideWhenCreating()->hideWhenUpdating(),
BelongsTo::make('Created By', 'user', 'App\Nova\User')->hideWhenCreating()->hideWhenUpdating(),
DateTime::make('Created At', 'created_at')->hideWhenCreating(),
DateTime::make('Updated At', 'updated_at')->hideWhenCreating(),
Because I am referencing the Note on the Game Nova resource, when I create a Note, the game_id column is populated correctly. But, I want the user_id column to be the value of the authenticated user. It does not seem to work like this, how would I accomplish it?
If I understand correctly from the line BelongsTo::make('Created By', 'user', 'App\Nova\User')->hideWhenCreating()->hideWhenUpdating() you're trying to set a default value for the column without showing the field on the form?
I don't think this is possible in this way. As soon as you use the hide functions the fields aren't rendered and will never be passed along with the request. I tried this, and the user_id field was never sent with the request.
I think there are two ways to do this:
Show the field in the form and set the default value using the metadata (and perhaps making the field read-only for good measure).
BelongsTo::make('Created By', 'user', 'App\Nova\User')->withMeta([
"belongsToId" => auth()->user()->id,
])
See this part of the Nova docs
Or use the Eloquent creating event. The following will go in your Note model.
public static function boot()
{
parent::boot();
static::creating(function($note)
{
$note->user_id = auth()->user()->id;
});
}
Granted, the above method is a bit simple. You'd be better off using proper event listeners.
Sidenote: from an architectural point of view, I'd go with option 2. Setting a default value without getting the end-user involved sounds like a job for the Eloquent model, not for a Nova form.
You can use a method resolveUsing(). An example
<?php
//...
Select::make('My Select', 'my_custom_name')
->options(['a' => 'a', 'b' => 'b', 'c' => 'c'])
->resolveUsing(function ($value, $resource, $attribute) {
// $value = model attribute value
// $attribute = 'my_custom_name'
return 'b';
});

How to combine Laravel validation required_if and required_without_all rules?

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.

Storing product attributes with orders in Magento

I am looking for a solution to storing product attributes with each order. Essentially I have a need for storing a unique item lot number for each product that can then be searched on the front end to find out which orders contained products from a specific lot. My initial thought was to do this with product attributes and store the attribute with each product in an order.
Does anybody have a better solution or can point me in the right direction for implementing this solution?
Edit: Still looking for a solution to this
I agree the lot number should be a product attribute but you don't need to store it twice. Just join it with order table when you later need it - That way the information is kept up to date (unless you want to know what it was at the time of ordering, in which case this is all wrong).
Unfortunately the order tables are flattened, not EAV, so don't handle attributes so well. The collection's joinAttribute() method is a stub. You can get around that with this query patterns library (self promotion disclaimer; I'm using it here because I wrote the attribute functions and don't want to redo the work) and then extending it with a class specific to you.
class Knectar_Select_Product_Lot extends Knectar_Select_Product
{
public function __construct()
{
parent::__construct();
$this->joinAttribute('lots', 'catalog_product', 'lot_number',
'products.entity_id', 0, 'lot_number'
);
}
public static function enhance(Varien_Db_Select $select, $tableName, $condition, $columns = null, $type = self::LEFT_JOIN)
{
$select->_join(
$type,
array($tableName => new self()),
$condition,
$columns ? $columns : 'lot_number'
);
}
}
The enhance() function does the hard bit, you only need to call it and filter by it's column.
$orderItems = Mage::getResourceModel('sales/order_item_collection');
Knectar_Select_Product_Lot::enhance($orderItems->getSelect, 'lots', 'lots.product_id = main_table.product_id');
$orderItems->addFieldToFilter('lot_number', 'ABC123');
If you are using the collection in an admin grid then it's column filters will do addFieldToFilter() for you.

Resources