laravel validate multiple models - validation

I would like a best practice for this kind of problem
I have items, categories and category_item table for a many to many relationship
I have 2 models with these validations rules
class Category extends Basemodel {
public static $rules = array(
'name' => 'required|min:2|max:255'
);
....
class Item extends BaseModel {
public static $rules = array(
'title' => 'required|min:5|max:255',
'content' => 'required'
);
....
class Basemodel extends Eloquent{
public static function validate($data){
return Validator::make($data, static::$rules);
}
}
I don't know how to validate these 2 sets of rules from only one form with category, title and content fields.
For the moment I just have a validation for the item but I don't know what's the best to do:
create a new set of rules in my controller -> but it seems redundant
sequentially validate Item then category -> but I don't know how to handle validations errors, do I have to merges them? and how?
a 3rd solution I'm unaware of
here is my ItemsController#store method
/**
* Store a newly created item in storage.
*
* #return Redirect
*/
public function store()
{
$validation= Item::validate(Input::all());
if($validation->passes()){
$new_recipe = new Item();
$new_recipe->title = Input::get('title');
$new_recipe->content = Input::get('content');
$new_recipe->creator_id = Auth::user()->id;
$new_recipe->save();
return Redirect::route('home')
->with('message','your item has been added');
}
else{
return Redirect::route('items.create')->withErrors($validation)->withInput();
}
}
I am very interested on some clue about this subject
thanks

One way, as you pointed yourself, is to validate it sequentially:
/**
* Store a newly created item in storage.
*
* #return Redirect
*/
public function store()
{
$itemValidation = Item::validate(Input::all());
$categoryValidation = Category::validate(Input::all());
if($itemValidation->passes() and $categoryValidation->passes()){
$new_recipe = new Item();
$new_recipe->title = Input::get('title');
$new_recipe->content = Input::get('content');
$new_recipe->creator_id = Auth::user()->id;
$new_recipe->save();
return Redirect::route('home')
->with('message','your item has been added');
}
else{
return Redirect::route('items.create')
->with('errors', array_merge_recursive(
$itemValidation->messages()->toArray(),
$categoryValidation->messages()->toArray()
)
)
->withInput();
}
}
The other way would be to create something like an Item Repository (domain) to orchestrate your items and categories (models) and use a Validation Service (that you'll need to create too) to validate your forms.
Chris Fidao book, Implementing Laravel, explains that wonderfully.

You can also use this:
$validationMessages =
array_merge_recursive(
$itemValidation->messages()->toArray(),
$categoryValidation->messages()->toArray());
return Redirect::back()->withErrors($validationMessages)->withInput();
and call it in the same way.

$validateUser = Validator::make(Input::all(), User::$rules);
$validateRole = Validator::make(Input::all(), Role::$rules);
if ($validateUser->fails() OR $validateRole->fails()) :
$validationMessages = array_merge_recursive($validateUser->messages()->toArray(), $validateRole->messages()->toArray());
return Redirect::back()->withErrors($validationMessages)->withInput();
endif;

Related

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();

Put other function on FormRequest in Laravel

I'm building a Laravel 6 application, and I am concerned about "best practices." I have one controller named CustomerController. In my controller, I want to update the Customer model, so I will have a function like the following.
public function update(UpdateCustomer $request, Customer $customer){
//
}
UpdateCustomer is my form request and where I will do the validation. In my update() method, I have classic validation.
public function rules()
{
$validationArray = [];
$validationArray['customer.name'] = 'string|required';
$validationArray['customer.vat'] = 'string|required';
$validationArray['customer.email'] = 'email|required';
return $validationArray;
}
Now I have to do some particular validation other the classic.
Let's assume that I have more data in my model, and I don't want these values to be changed.
For example, I have the following: address, cap, locality. I have a second method on the UpdateCustomer request that I can validate.
public function validateForDataCantChange()
{
$data = $this->input("customer");
$customer = $this->route("customerID");
$validator = Validator::make([], []); // Empty data and rules fields
$arrayDataThatCantChange = [
'address' => $data['address'] ?? NULL,
'cap' => $data['cap'] ?? NULL,
'locality' => $data['locality'] ?? NULL
];
foreach ($arrayDataThatCantChange as $key => $v) {
if ($customer->{$key} !== $v) {
$validator->errors()->add($key, __("messages.the field :field can't be changed", ['field' => $key]));
}
}
if ($validator->errors()->any()) {
throw new ValidationException($validator);
}
}
And then in my controller, I've added the following.
public function update(UpdateCustomer $request, Customer $customer){
$request->validateForDataCantChange();
}
Is this a bad practice? Should I create a new FormRequest? How, in this case (two form requests), can I use two different requests for a single controller?
For the little effort required, I'd personally create a new form request.
If you wish to use the same form request you can do the following:
public function rules()
{
$rules = [
'title' => 'required:unique:posts'
];
// when editing i.e. /posts/2/edit
if ($id = $this->segment(2)) {
$rules['title'] .= ",$id";
}
return $rules;
}
However, I always use a separate class for each action.

Yii2 - Attributes in DynamicModel

I created a yii\base\DynamicModel in controller and I have one form with attributes from this model. I need access these attributes after submitting form in controller.
controller.php
public function actionCreate()
{
$model = new DynamicModel([
'name', 'age', 'city'
]);
if($model->load(Yii::$app->request->post())){
$model->age = $model->age + 5;
/*
* code....
* */
return $this->redirect(['index']);
} else {
return $this->render('create', [
'model' => $model,
]);
}
}
But $model->age, $model->name etc. returns nothing.
I could only access the attribute this way: Yii::$app->request->get('DynamicModel')['age']
What is the correct way to access these attributes?
You need to configure validation rules in order to automatically load attributes by load():
$model = new DynamicModel(['name', 'age', 'city']);
$model->addRule(['name', 'age', 'city'], 'safe');
if ($model->load(Yii::$app->request->post())) {
// ...
Using safe will accept values as is without actual validation, but you may consider adding real validation rules to ensure correct state of your model.

ManyToOne form creating linked object

Imagine these 2 entities:
Article
title
description
category <- ManyToOne
Category
name
Say we need to manage an article with a form and the category attached.
$builder
->add('name')
->add('description')
->add('category')
This one will allow me to select from existing categories.
Now I'd like to be able to create categories if needed.
Title [ ]
Description
[ ]
[ ]
Category [ ]
The category field would be a free text box.
If the text corresponds to no category, a new one would be created and attached.
I tried with some DataTransformer with no luck
I need a reusable solution to manage that because I'll need to embed it especially in another form as a collection.
How can I do it reusable ?
Suggest that you do an "if" on your category.
When you check your form submission is valid and submitted, get the 'category' data, and if not exists persist it as a new category then query for the article after.
The code might be something like this:
if ($form-isSubmitted() && $form->isValid()){
$catName = $form->get('category')->getData(); // Get category.
// Query if it exists.
$qb = $em->createQueryBuilder();
$qb->select('c')
->from('AppBundle:Category', 'c')
->where('c.name = :catName')
->setParameter('catName', $catName);
$cat_results = $qb->getQuery()->setMaxResults(1)->getOneOrNullResult();
if ($cat_results == null){
$newCat = new Category();
$newCat()->setname($catName);
$em->persist($newCat);
$em->flush();
}
else{
...
\\ Render your form etc...
}
Hopefully you get the idea. The above code is simpler, because then you create the category in the same form.
Here is what I ended up with using a DataTransformer
My ArticleType:
class ArticleType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name')
->add('description')
->add('category', CategoryType::class)
//...
A new CategoryType class:
class CategoryType extends AbstractType
{
protected $myService;
public function __construct(MyService $myService)
{
$this->myService = $myService;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
$transformer = new CategoryTransformer($this->myService);
$builder->addModelTransformer($transformer);
}
}
Registered as service
app.form.category_type:
class: AppBundle\Form\CategoryType
arguments: ['#app.my.service']
tags:
- { name: form.type }
And finally the transformer:
class CategoryTransformer implements DataTransformerInterface
{
// ...MyService initialization...
public function transform($category)
{
if (null === $category) {
return '';
}
return $category->getName();
}
public function reverseTransform($categoryName)
{
if (!$categoryName) {
return null;
}
$category = $this->myService->getOrCreateCategoryFromName($categoryName);
if (null === $category) {
throw new TransformationFailedException();
}
return $category;
}
}
MyService is responsible to get or create a category with the given name using the entity manager.
Quite some lines of code but once this is done, wherever I'll use my form, it can be dealt in the easy standard way:
$articleForm = $this->createForm(ArticleType::class, $article);
$articleForm->handleRequest($request);
if ($articleForm->isValid()) {
$em->persist($article);
$em->flush();
//...

Resources