add Symfony Assert in a Callback - validation

I, i have to add an Assert to an atribute when other atribute is equal than something. Like this:
/**
* #Assert\Callback(methods={"isChildMinor",)
*/
class PatientData
{
/**
* #Assert\Date()
*/
public $birthday;
public $role;
public function isChildMinor(ExecutionContext $context)
{
if ($this->role == 3 && check #assert\isMinor() to $birtday) {
=>add violation
}
}
so, i want check if the patient is minor (with assert or somethings else) if the role is equal than 3. How do this?

There are several ways to do, what you want.
1) You could make it right in the form. Like that:
use Symfony\Component\Validator\Constraints as Assert;
public function buildForm(FormBuilderInterface $builder, array $options)
{
$yourEntity = $builder->getData();
//here you start the field, you want to validate
$fieldOptions = [
'label' => 'Field Name',
'required' => true,
];
if ($yourEntity->getYourProperty != 'bla-bla-bla') {
$fieldOptions[] = 'constraints' => [
new Assert\NotBlank([
'message' => 'This is unforgivable! Fill the field with "bla-bla-bla" right now!',
]),
],
}
$builder->add('myField', TextType::class, $fieldOptions);
2) Other way - is to make your custom validation callback in your Entity and play with direct asserts there. It's possible, I think.
3) But the optimal way, from my point of view - is to use several asserts with validation groups. You need to specify Assert\isMinor(groups={"myCustomGroup"}) on birthday field. And then, in your form:
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'validation_groups' => function (FormInterface $form) {
$yourEntity = $form->getData();
if ($yourEntity->role !== 3) {
return ['Default', 'myCustomGroup'];
}
return ['Default'];
},
Hope this'll be helpful for you.

Related

Merge FormRequest rules

This question is based on this thread: Merge 2 rules FormFequest for validate an update and store action in laravel5.5
Context: Let's suppose I have these 2 requests and I want to merge the SocialMediaFormRequest rules in ReadersFormRequest rules.
ReadersFormRequest
class ReadersFormRequest extends FormRequest
{
public function rules(SocialMediaFormRequest $social)
{
$mediaRules = $social->rules();
$rules = [
'first_name'=>'required',
'last_name'=>'required',
'birthday'=>'required',
'region'=>'required',
'photo_url'=>'required',
'support'=>'required',
'riwayas_id'=>'required',
'description'=>'required',
];
return array_merge($rules,$mediaRules);
}
}
SocialMediaFormRequest
class SocialMediaFormRequest extends FormRequest
{
public function rules()
{
return [
'url'=>'required|url',
'title'=>'required'
];
}
}
Form that I received
first_name: "example"
last_name: "example"
birthday: 2022-06-13
region: somewhere
photo_url: "https:XXX"
support: false
riwayas_id: 1
description: ""
media.url: "https:YYY"
media.title: "stackoverflow"
Question: How can I only pass the argument media.XXX in my form SocialMediaFormRequest?
You can use prepareForValidation() method in the form request to sanitize the inputs : https://laravel.com/docs/9.x/validation#preparing-input-for-validation
So, if in SocialMediaFormRequest you receive the full request you can only get the required fields like that:
public function prepareForValidation()
{
$this->replace([
'url' => $this->url ?? ($this->media['url'] ?? null),
'title' => $this->title ?? ($this->media['title'] ?? null),
]);
}
Also, in ReadersFormRequest when you inject the other request or resolve it from the container it doesn't work correctly, so it is better to get the rules like that:
public function rules()
{
$mediaRules = (new SocialMediaFormRequest())->rules();
and in order to access the media.* attributes in ReadersFormRequest you can again use prepareForValidation:
public function prepareForValidation()
{
$this->merge([
'url' => $this->media['url'] ?? null,
'title' => $this->media['title'] ?? null,
]);
}

Laravel - How to update Input Array without deleting Sales Detail

In my Laravel-8 project, I have this controller for Input Field Array Update.
Controller:
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
SaleDetail::where('sale_id', $sale->id)->delete();
foreach ($data['invoiceItems'] as $item) {
$details = [
'sale_id' => $sale->id,
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$saleDetail = new SaleDetail($details );
$saleDetail->save();
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
In the form, the user can add more Sales Detail or remove.
Some of the SaleDetail fields are being used somewhere else.
Is there a way to update the input field array without deleting the SaleDetail as shown in what I did here:
SaleDetail::where('sale_id', $sale->id)->delete();
Thanks
I've tried to restructure your code so that's easier to edit. I've left some comments. I can really recommend refactoring.guru. There you will find many ways to improve your code so that it is more extensible, maintainable and testable. If you have any questions, please feel free to ask.
class Sale extends Model
{
// Use a relationship instead of building your own query
public function details() {
return $this->hasMany(SaleDetail::class);
}
}
class SaleDetail extends Model
{
// Use a computed property instead of manually calculating total price
// You can access it with $saleDetail->totalPrice
public function getTotalPriceAttribute() {
return $this->price * $this->quantity;
}
}
class UpdateSaleRequest extends Request
{
public function authorize() {
return true;
}
protected function prepareForValidation() {
$this->merge([
// Create a Carbon instance by string
'date' => Carbon::make($this->date)
]);
}
public function rules() {
// Your validation rules
// Please also validate your invoice items!
// See https://laravel.com/docs/8.x/validation#validating-arrays
}
}
// We let Laravel solve the sale by dependency injection
// You have to rename the variable name in ihr web.php
public function update(UpdateSaleRequest $request, Sale $sale)
{
// At this point, all inputs are validated!
// See https://laravel.com/docs/8.x/validation#creating-form-requests
$sale->update($request->validated());
// Please ensure, that all properties have the same name
// In your current implementation you have price = cost, be consistent!
foreach($request->input('invoiceItems') as $invoiceItem) {
// How we can consider that a detail is already created?
// I assume that each item_id will only occur once, otherwise you'll
// place the id of each detail in your update form (e.g. in a hidden input)
$candidate = $sale->details()
->where('item_id', $properties['item_id'])
->first();
if($candidate) {
$candidate->update($properties);
} else {
$sale->details()->create($properties);
}
}
// A JWT-Exception should not be necessary, since your authentication
// will be handled by a middleware.
return response()->json($sale);
}
I have not tested the code, few adjustments may be needed.
Laravel has a method called updateOrCreate as follow
/**
* Create or update a record matching the attributes, and fill it with values.
*
* #param array $attributes
* #param array $values
* #return \Illuminate\Database\Eloquent\Model|static
*/
public function updateOrCreate(array $attributes, array $values = [])
{
return tap($this->firstOrNew($attributes), function ($instance) use ($values) {
$instance->fill($values)->save();
});
}
That means you could do some thing like
public function update(UpdateSaleRequest $request, $id)
{
try {
$sale = Sale::find($id);
$data = $request->all();
$update['date'] = date('Y-m-d', strtotime($data['date']));
$update['company_id'] = $data['company_id'];
$update['name'] = $data['name'];
$update['remarks'] = $data['remarks'];
$sale->update($update);
foreach ($data['invoiceItems'] as $item) {
$details = [
'item_id' => $item['item_id'],
'employee_id' => $item['employee_id'],
'quantity' => $item['qty'],
'price' => $item['cost'],
'total_price' => $item['cost'] * $item['qty'],
'sale_type_id'=>$item['sale_type_id']
];
$sale->saleDetail()->updateOrCreate([
'sale_id' => $sale->id
], $details);
}
} catch (JWTException $e) {
throw new HttpException(500);
}
return response()->json($sale);
}
I would encourage you to refactor and clean up your code.You can also read more about it here https://laravel.com/docs/8.x/eloquent#upserts

Model appends including entire relationship in query

Edit: I was able to see where the relations are being included in my response, but I still don't know why.
On my Customer model, I have:
protected $appends = [
'nps',
'left_feedback',
'full_name',
'url'
];
The accessors are as follows:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews->count() > 0) {
return $this->reviews->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
if ($this->reviews && $this->reviews->count() > 0 && $this->reviews->first()->feedback != null) {
return "Yes";
} else {
return "No";
}
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location;
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
So if I comment out the $appends property, I get the response I originally wanted with customer not returning all the relations in my response.
But I do want those appended fields on my Customer object. I don't understand why it would include all relations it's using in the response. I'm returning specific strings.
So is there a way to keep my $appends and not have all the relations it's using in the accessors from being included?
Original Question:
I am querying reviews which belongsTo a customer. I want to include the customer relation as part of the review, but I do not want to include the customer relations.
$reviews = $reviews->with(['customer' => function($query) {
$query->setEagerLoads([]);
$query->select('id', 'location_id', 'first_name', 'last_name');
}]);
$query->setEagerLoads([]); doesn't work in this case.
I've tried $query->without('location'); too, but it still gets included
And I should note I don't have the $with property on the model populated with anything.
Here is the Review model relation:
public function customer() {
return $this->belongsTo('App\Customer');
}
Here is the Customer model relation:
public function reviews() {
return $this->hasMany('App\Review');
}
// I dont want these to be included
public function location() {
return $this->belongsTo('App\Location');
}
public function reviewRequests() {
return $this->hasMany('App\ReviewRequest');
}
In the response, it will look something like:
'review' => [
'id'=> '1'
'customer => [
'somecol' => 'test',
'somecolagain' => 'test',
'relation' => [
'relation' => [
]
],
'relation' => [
'somecol' => 'sdffdssdf'
]
]
]
So a chain of relations ends up being loaded and I don't want them.
As you said in one comment on the main question, you are getting the relations due to the appended accessors.
Let me show you how it should be done (I am going to copy paste your code and simply edit some parts, but you can still copy paste my code and place it in yours and will work the same way but prevent adding the relations) and then let me explain why is this happening:
/**
* Accessor
*/
public function getNpsAttribute() {
if ($this->reviews()->count() > 0) {
return $this->reviews()->first()->nps;
} else {
return "n/a";
}
}
/**
* Accessor
*/
public function getLeftFeedbackAttribute() {
return $this->reviews()->count() > 0 &&
$this->reviews()->first()->feedback != null
? "Yes"
: "No";
}
/**
* Accessor
*/
public function getFullNameAttribute() {
return ucwords($this->first_name . ' ' . $this->last_name);
}
/**
* Accessor
*/
public function getUrlAttribute() {
$location = $this->location()->first();
$company = $location->company;
$account_id = $company->account->id;
return route('customers.show', ['account_id' => $account_id, 'company' => $company, 'location' => $location, 'customer' => $this]);
}
As you can see, I have changed any $this->relation to $this->relation()->first() or $this->relation->get().
If you access any Model's relation as $this->relation it will add it to the eager load (loaded) so it will really get the relation data and store it in the Model's data so next time you do $this->relation again it does not have to go to the DB and query again.
So, to prevent that, you have to access the relation as $this->relation(), that will return a query builder, then you can do ->count() or ->exists() or ->get() or ->first() or any other valid query builder method, but accessing the relation as query builder will prevent on getting the data and store it the model (I know doing ->get() or ->first() will get the data, but you are not directly getting it through the model, you are getting it through the query builder relation, that is different).
This way you will prevent on storing the data on the model, hence giving you problems.
You can also use API Resources, it is used to map a Model or Collection to a desired output.
One last thing, if you can use $this->relation()->exists() instead of $this->relation()->count() > 0 it will help on doing it faster, mostly any DB is faster on looking if data exists (count >= 1) than really counting all the entries it has, so it is faster + more performant on using exists.
Try :
$review->with(‘customer:id,location_id,first_name,last_name’)->get();
Or :
$review->withOnly(‘customer:id,location_id,first_name,last_name’)->get();

Opposite of required_if in laravel using multiple value evaluation

How to validate must null if another field has specific value or not null
In my case it is the opposite of required_if with multiple values
$rule = array(
'selection' => 'required',
'stext' => 'required_if:selection,2|required_if:selection,3',// stext should be null if selection is 2 or 3
);
And if needed how to perform own validation?
So in your example you can do something like this:
$rule = array(
'selection' => 'required',
'stext' => 'required'
);
// override the rule
if(in_array(request('selection'), [2, 3]))
{
$rule['stext'] = 'nullable';
}
This means if the selection is 2 the field will be required and if the selection field has any other value the stext field will be required.
I am not sure if I understood your question correctly. In any case the opposite of required_if is required_without so you can use that one if you want this field to be required even if the selection is empty.
With custom rule your passes method should look like this:
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
class CustomRule implements Rule
{
protected $selection;
public __construct($selection)
{
$this->selection = $selection;
}
public function passes($attribute, $value)
{
return $value === null && in_array($this->selection, [2, 3]);
}
}
You use it like this:
$rule['stext'] = [ new CustomRule(request('selection') ]
I try to extend the validation rule. put the following in AppServiceProvider:
Validator::extend('null_if', function ($attribute, $value, $parameters, $validator) {
$other = $parameters[0];
$other_value = array_get(request()->toArray(), $other);
if ($parameters[1] == $other_value) {
return empty($value);
}
return true;
});
Tell me if it's work or what error given to you.

yii2 custom validation not working

I need to compare 2 attribute value in the model and only if first value is lower than second value form can validate.I try with below code but it not worked.
controller
public function actionOpanningBalance(){
$model = new Bill();
if ($model->load(Yii::$app->request->post())) {
$model->created_at = \Yii::$app->user->identity->id;
$model->save();
}else{
return $this->render('OpanningBalance', [
'model' => $model,
]);
}
}
Model
public function rules()
{
return [
[['outlet_id', 'sr_id', 'bill_number', 'bill_date', 'created_at', 'created_date','bill_amount','credit_amount'], 'required'],
[['outlet_id', 'sr_id', 'created_at', 'updated_at'], 'integer'],
[['bill_date', 'd_slip_date', 'cheque_date', 'created_date', 'updated_date','status'], 'safe'],
[['bill_amount', 'cash_amount', 'cheque_amount', 'credit_amount'], 'number'],
[['comment'], 'string'],
['credit_amount',function compareValue($attribute,$param){
if($this->$attribute > $this->bill_amount){
$this->addError($attribute, 'Credit amount should less than Bill amount');
}],
[['bill_number', 'd_slip_no', 'bank', 'branch'], 'string', 'max' => 225],
[['cheque_number'], 'string', 'max' => 100],
[['bill_number'], 'unique']
];
}
}
It's going in to the validator function but not add the error like i wanted
$this->addError($attribute, 'Credit amount should less than Bill amount');
anyone can help me with this?
If the validation is not adding any error, it's most likely being skipped. The issue is most likely becasue of default rules behaviour whereby it skips empty or already error given values as per here: https://www.yiiframework.com/doc/guide/2.0/en/input-validation#inline-validators
Specifically:
By default, inline validators will not be applied if their associated attributes receive empty inputs or if they have already failed some validation rules. If you want to make sure a rule is always applied, you may configure the skipOnEmpty and/or skipOnError properties to be false in the rule declarations.
So you would need to set up the skipOnEmpty or skipOnError values depending on what works for you:
[
['country', 'validateCountry', 'skipOnEmpty' => false, 'skipOnError' => false],
]
Try this:
public function actionOpanningBalance(){
$model = new Bill();
if ($model->load(Yii::$app->request->post()) && $model->validate()) {
$model->created_at = \Yii::$app->user->identity->id;
$model->save();
}else{
return $this->render('OpanningBalance', [
'model' => $model,
]);
}
}
For Validation
You can use anonymous function :
['credit_amount',function ($attribute, $params) {
if ($this->$attribute > $this->bill_amount)) {
$this->addError($attribute, 'Credit amount should less than Bill amount.');
return false;
}
}],
you can use like this below answer is also write
public function rules(){
return [
['credit_amount','custom_function_validation', 'on' =>'scenario'];
}
public function custom_function_validation($attribute){
// add custom validation
if ($this->$attribute < $this->cash_amount)
$this->addError($attribute,'Credit amount should less than Bill amount.');
}
I've made custom_function_validation working using 3rd params like this:
public function is18yo($attribute, $params, $validator)
{
$dobDate = new DateTime($this->$attribute);
$now = new DateTime();
if ($now->diff($dobDate)->y < 18) {
$validator->addError($this, $attribute, 'At least 18 years old');
return false;
}
}
This is a back end validation and it will trigger on submit only. So you can try something like this inside your validation function.
if (!$this->hasErrors()) {
// Your validation code goes here.
}
If you check the basic Yii2 app generated you can see that example in file models/LoginForm.php, there is a function named validatePassword.
Validation will trigger only after submitting the form.

Resources