Laravel save one to many relationship - php

I have the following relationships set up in Laravel:
OrderStatus Model
- hasMany('Order')
Order Model
- 'belongsTo('OrderStatus');
The database is set up with an orders table and an order_statuses table. The orders table has a field for order_status_id.
When I save an Order, I manually set the order_status_id by fetching the appropriate Order Status model, like this:
$status = OrderStatus::where(['name'=>'sample_status'])->firstOrFail();
$order->order_status_id = $status->id;
$order->save();
I'm wondering if there is a built in function to do this rather than setting the order_status_id manually. I've read about "Attaching a related model", and "Associating Models" in the Laravel docs, but I can't figure out if these fit my use case. I think the issue I'm having is that I'm working directly with the child model (the order), and trying to set it's parent. Is there a function for this?

Sure you can do this:
$status = OrderStatus::where(['name'=>'sample_status'])->firstOrFail();
$order = new Order;
$order->status()->associate($status);
$order->save();
(status() is the belongsTo relation. You might need to adjust that name)

The correct way, to save a relationship for a new related model is as follows:
$status = OrderStatus::where(['name'=>'sample_status'])->firstOrFail();
$order = new Order;
$status->order()->save($order);
Documentation link : http://laravel.com/docs/4.2/eloquent#inserting-related-models

You can go with a custom solution.
I am explaining an example, just coded and very much similar to your question, hope it will help.
I have a Question Model and AnswerOption Model as below.
Question Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Question extends Model
{
protected $table = 'questions';
protected $fillable = [
'title',
'created_at',
'updated_at'
];
/**
* Get the answer options for the question.
*/
public function answerOptions()
{
return $this->hasMany('App\Models\AnswerOption');
}
/**
* #param array $answerOptions
*/
public function syncAnswerOptions(array $answerOptions)
{
$children = $this->answerOptions;
$answerOptions = collect($answerOptions);
$deleted_ids = $children->filter(
function ($child) use ($answerOptions) {
return empty(
$answerOptions->where('id', $child->id)->first()
);
}
)->map(function ($child) {
$id = $child->id;
$child->delete();
return $id;
}
);
$attachments = $answerOptions->filter(
function ($answerOption) {
// Old entry (you can add your custom code here)
return empty($answerOption['id']);
}
)->map(function ($answerOption) use ($deleted_ids) {
// New entry (you can add your custom code here)
$answerOption['id'] = $deleted_ids->pop();
return new AnswerOption($answerOption);
});
$this->answerOptions()->saveMany($attachments);
}
}
AnswerOption Model
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class AnswerOption extends Model
{
protected $table = 'answer_options';
protected $fillable = [
'question_id',
'title',
'ord',
'created_at',
'updated_at'
];
/**
* Get the question that owns the answer.
*/
public function question()
{
return $this->belongsTo('App\Models\Question');
}
}
Here you can see a single question hasMany answer options, you can see I have used BelongsTo , hasMany relationsip in models.
Now in QuestionController during Question save and update, you can also save the answer options.
For this I have written syncAnswerOptions method in Question Model.
You just need to pass the array of options with id, if id is present already in the database then it will update, if id is blank it will add a new record, If Id was there but not in your new array, then that record will get deleted.
/**
* If you are attaching AnswerOption(s) for the first time, then pass
* in just the array of attributes:
* [
* [
* // answer option attributes...
* ],
* [
* // answer option attributes...
* ],
* ]
*//**
* If you are attaching new AnswerOption(s) along with existing
* options, then you need to pass the `id` attribute as well.
* [
* [
* 'id' => 24
* ],
* [
* // new answer option attributes...
* ],
* ]
*/
In Question controller's store and update method call this method just after question add and update call.
$question->syncAnswerOptions($data['answerOptions']);
$data['answerOptions'] is Array of answer options just like described in the comments.

Related

Laravel nested resource retrieve model by slug instead of id

I'm working on a Laravel 8 project, it's being used as an API for a frontend. My API contains Brands and Forms, a brand is created by a user and a brand can have a form.
My brand schema contains a slug column, it's this column that's present in my front-end URLs, the slug column is unique, e.g:
/account/brands/my-brand/forms/
/account/brands/my-brand/forms/create/
A form has a brand_id column, this is later used as part of Laralve's hasOne and hasMany relationship for automatic joining since it's easier this way and means I don't have to have an ugly URL.
The trouble I'm having is when I want to show the user a list of their forms for the brand they're on I don't have access to the brand_id in the request, only the slug as this is part of the URL, whereas to retrieve my forms I need the brand_id.
How could I easily (without having another function) obtain this whilst still ensuring my relationships in my model work correctly?
Retrieving forms for a brand that belong to a user
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index($brand)
{
// $brand -> this is the slug, unsure how to utilise this on the joined brand
$forms = Form::where('user_id', Auth::id())->with('brand')->get();
return response()->json([
'forms' => $forms
], 200);
}
My Brand model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Brand extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'brands';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'uuid',
'brand',
'url',
'telephone',
'link_terms',
'link_privacy',
'seo_description',
'base64_logo',
'base64_favicon',
'text_marketing',
'text_promos',
'text_broker',
'text_footer_1',
'text_footer_2',
'text_credit_disclaimer',
'analytics_internal_affiliate'
];
/**
* The relationships that should always be loaded.
*
* #var array
*/
protected $with = [
'form'
];
/**
* Get the form associated with the user.
*/
public function form()
{
return $this->hasOne(Form::class);
}
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(User::class);
}
}
My Form model:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class Form extends Model
{
use HasFactory, SoftDeletes;
/**
* The table associated with the model.
*
* #var string
*/
protected $table = 'forms';
/**
* The attributes that are mass assignable.
*
* #var string[]
*/
protected $fillable = [
'name',
'loan_amount',
'loan_min_amount',
'loan_max_amount',
'loan_term',
'loan_min_term',
'loan_max_term'
];
/**
* Get the brand that owns the comment.
*/
public function brand()
{
return $this->belongsTo(Brand::class);
}
}
The path of least resistance would be to harness the whereRelationship() method on your query.
I believe that would look like this:
$forms = Form::where('user_id', Auth::id())
->whereRelationship('brand', '=', $brand)
->with('brand')
->get();
https://laravel.com/docs/master/eloquent-relationships
An alternative which would be more work to set up but would likely make your life easier long term would be to use route model binding. https://laravel.com/docs/master/routing#route-model-binding
You can ask Laravel's IoC container to resolve the full record for you by typehinting it and naming the variable correctly in the method signature.
Since you are using a column other than id to identify it, you need to inform Laravel of this. There are a couple of different ways to do this, discussed at https://laravel.com/docs/master/routing#customizing-the-default-key-name
Then, you can ask Laravel's IoC container to build an instance of the model by searching in the database for you right in the method signature. You simply need to typehint the parameter and ensure it's name matches that in the route definition.

Sorting of paginator object through a model relation column

I have three tables: products, product_inventories and product_inventory_details. The ORM of each model is shown below,
Product Model
class Product extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
...,
'title',
'selected_inventory_id',
...
];
/**
* Get the inventories those belongs to this model.
*/
public function inventory(){
return $this->hasMany('App\Models\ProductInventory');
}
/**
* Get the selected product_inventory_detail that owns this model.
*/
public function selected(){
return $this->hasOne('App\Models\ProductInventoryDetail', 'id', 'selected_inventory_id');
}
...
}
ProductInventory Model
class ProductInventory extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_id',
...
];
/**
* Get the inventory_details those belongs to this model.
*/
public function items(){
return $this->hasMany('App\Models\ProductInventoryDetail');
}
...
}
ProductInventoryDetail Model
class ProductInventoryDetail extends Model{
...
/**
* The attributes that are mass assignable.
*
* #var array
*/
protected $fillable = [
'product_inventory_id',
'price',
...
];
}
I'm sorting and limiting the results through user input of Sort by dropdown and Show per page dropdown. When sorting by Alphabetical: High to Low option I'm running the query builder method to order the results:
$products = $products->orderBy($sort['column'], $sort['order'])->paginate($limit);
Now with sorting by Price, I can't run the query builder method orderBy() since I'm not using joins and getting the data through relationship properties. Instead I'm using the Laravel's collection method to sort it out:
$products = $products->paginate($limit);
$products = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
The above block is working fine if I don't use pagination methods. But, I need to use the pagination as well since the user can also limit the results per page. I'm also using a Paginator object's method to append some parameters to each page URL:
$products->appends($paramsArr);
Since running the sortBy() method returns a collection instead of Paginator object, it's giving me undefined method exception.
My Question
How can I sort the result set by price in my current scenario without having to implement the joins? Is there a way to achieve that??
I would use QueryBuilder package of Spatie. It will make your life easier for creating sortable and filterable grid table. You use that package this way:
$query = Product::with(['inventory', 'selected']);
$products = \Spatie\QueryBuilder\QueryBuilder::for($query)
->allowedFilters([
'name' => 'name', // name column in the products DB table.
'selected.price' => 'product_inventory_details.column_price', // price column in the product_inventory_details DB table.
])
->defaultSort('name')
->allowedSorts([
'name',
\Spatie\QueryBuilder\AllowedSort::custom('selected.price', new SortSelectedPrice())
])
->paginate(20)
->appends(request()->query());
External custom sort class looks like this:
class SortSelectedPrice implements \Spatie\QueryBuilder\Sorts\Sort
{
public function __invoke(Builder $query, bool $descending, string $property)
{
$direction = $descending ? 'DESC' : 'ASC';
$query->leftJoin('product_inventory_details', 'products.id', '=', 'product_inventory_details.product_id');
$query->orderBy('product_inventory_details.column_price', direction);
}
}
Make sure your URL containing the query string like this for sorting name from A to Z and sorting price from 1xxxxxxxx to 0:
domain.com/products?sort=name,-selected.price
I installed the Spatie package using composer. Don't forget to do that.
I found a way to handle it without having to implement the joins. I added a new variable and stored the Paginator object's items() method result set into it.
$products = $products->paginate($limit);
$productItems = $products->items(); // returns the items array from paginator
And sort that particular variable instead of sorting the whole paginator object. That way my links and URLs are untouched and unmodified in the $products variable and the data of the products are in a separate variable.
if($sort['column'] == 'price'){
if($sort['order'] == 'DESC'){
$productItems = $products->sortByDesc(function($prod, $key){
return $prod->selected->price;
});
} else{
$productItems = $products->sortBy(function($prod, $key){
return $prod->selected->price;
});
}
}
I also had to change my rendering variable from $products to $productItems and accessed the pagination links from the old $products variable.
#forelse ($productItems as $product)
#include('site.components.product-grid')
#empty
<div class="col text-center">...</div>
#endforelse
...
{{ $products->links() }}
I'm posting it here for the community to benefit/discuss/criticize if there is a better way.

How to delete single (many-) rows from one-to-many relations in Laravel 5.5

I got two models in Laravel: Label
class Label extends \Eloquent
{
protected $fillable = [
'channel_id',
'name',
'color',
];
public function channel()
{
return $this->belongsTo('App\Channel');
}
}
And Channel
class Channel extends \Eloquent
{
protected $fillable = [
'name',
];
public function labels()
{
return $this->hasMany('App\Label');
}
}
Now when a label is deleted, I want to make sure, the label belongs to the channel.
This works pretty good, as it even is atomic, so the row will just be deleted, if the label really belongs to the channel.
LabelController:
/**
* Remove the specified resource from storage.
*
* #param int $id
* #return \Illuminate\Http\Response
*/
public function destroy($id)
{
$channel = $this->getChannel();
Label::where('id', $id)
->where('channel_id', $channel->id)
->delete();
return back();
}
And my question is now: How to build that with Eloquent, so it is elegant? Something like:
$channel->labels()->destroy($id);
But there is no destroy function on the relation.
Update:
I managed to achieve something in the right direction:
$channel->labels()->find($id)->delete();
This deletes the label with $id BUT just if the label has the right channel_id assigned. If not, I get the following error, which I could catch and handle:
FatalThrowableError (E_ERROR) Call to a member function delete() on null
Still, as Apache is threaded, there could be the case that another thread changes the channel_id after I read it. So the only way besides my query is to run a transaction?
If you want to delete related items of your model but not model itself you can do like this:
$channel->labels()->delete(); It will delete all labels related to the channel.
If you want to delete just some of labels you can use where()
$channel->labels()->where('id',1)->delete();
Also if your relation is many to many it will delete from third table too.
You mentioned that, you first want to check if a label has a channel. If so, then it should be deleted.
You can try something like this though:
$label = Label::find($id);
if ($label->has('channel')) {
$label->delete();
}
You can use findorfail:
$channel->labels()->findOrFail($id)->delete();

How to update 'updated_at' of model when updating a pivot table

I'm using laravel and I have a many to many relation between products and orders. There is a pivot table called order-product which has additional information that is updated from time to time. I would like to update the 'updated_at' feild in the order table when the corresponding row in the pivot table is updated or for example if a new product is added.
Is that possible?
The relation between a products and an order is belongsToMany()
Edit: here is some of the code
class Order extends Model
{
protected $table = 'order';
protected $dates = ['updated_at'];
public function products()
{
return $this->belongsToMany(Product::class, 'order_product');
}
}
When I want to remove products its by calling
$order->products()->detach()
So where do I put the $touches array thats mentioned in the laravel docs here
I've tried adding it to the product model but its not working.
You can use the Touching Parent Timestamps, here is an example (from laravel docs).
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
/**
* All of the relationships to be touched.
*
* #var array
*/
protected $touches = ['post'];
/**
* Get the post that the comment belongs to.
*/
public function post()
{
return $this->belongsTo('App\Post');
}
}

Laravel Removing Pivot data in many to many relationship

Not sure if I set this up correctly. In Laravel I'm creating two models with a many-to-may relationship
The models are Item and Tags. Each one contains a belongsTo to the other.
When I run a query like so:
Item::with('tags')->get();
It returns the collection of items, with each item containing a tags collection. However the each tag in the collection also contains pivot data which I don't need. Here it is in json format:
[{
"id":"49",
"slug":"test",
"order":"0","tags":[
{"id":"3","name":"Blah","pivot":{"item_id":"49","tag_id":"3"}},
{"id":"13","name":"Moo","pivot":{"item_id":"49","tag_id":"13"}}
]
}]
Is there anyway to prevent this data from getting at
you can just add the name of the field in the hidden part in your model like this:
protected $hidden = ['pivot'];
that's it , it works fine with me.
You have asked and you shall receive your answer. But first a few words to sum up the comment section. I personally don't know why you would want / need to do this. I understand if you want to hide it from the output but not selecting it from the DB really has no real benefit. Sure, less data will be transferred and the DB server has a tiny tiny bit less work to do, but you won't notice that in any way.
However it is possible. It's not very pretty though, since you have to override the belongsToMany class.
First, the new relation class:
class BelongsToManyPivotless extends BelongsToMany {
/**
* Hydrate the pivot table relationship on the models.
*
* #param array $models
* #return void
*/
protected function hydratePivotRelation(array $models)
{
// do nothing
}
/**
* Get the pivot columns for the relation.
*
* #return array
*/
protected function getAliasedPivotColumns()
{
return array();
}
}
As you can see this class is overriding two methods. hydratePivotRelation would normally create the pivot model and fill it with data. getAliasedPivotColumns would return an array of all columns to select from the pivot table.
Now we need to get this integrated into our model. I suggest you use a BaseModel class for this but it also works in the model directly.
class BaseModel extends Eloquent {
public function belongsToManyPivotless($related, $table = null, $foreignKey = null, $otherKey = null, $relation = null){
if (is_null($relation))
{
$relation = $this->getBelongsToManyCaller();
}
$foreignKey = $foreignKey ?: $this->getForeignKey();
$instance = new $related;
$otherKey = $otherKey ?: $instance->getForeignKey();
if (is_null($table))
{
$table = $this->joiningTable($related);
}
$query = $instance->newQuery();
return new BelongsToManyPivotless($query, $this, $table, $foreignKey, $otherKey, $relation);
}
}
I edited the comments out for brevity but otherwise the method is just like belongsToMany from Illuminate\Database\Eloquent\Model. Of course except the relation class that gets created. Here we use our own BelongsToManyPivotless.
And finally, this is how you use it:
class Item extends BaseModel {
public function tags(){
return $this->belongsToManyPivotless('Tag');
}
}
If you want to remove pivot data then you can use as protected $hidden = ['pivot']; #Amine_Dev suggested, so i have used it but it was not working for me,
but the problem really was that i was using it in wrong model so i want to give more detail in it that where to use it, so you guys don't struggle with the problem which i have struggled.
So if you are fetching the data as :
Item::with('tags')->get();
then you have to assign pivot to hidden array like below
But keep in mind that you have to define it in Tag model not in Item model
class Tag extends Model {
protected $hidden = ['pivot'];
}
Two possible ways to do this
1. using makeHidden method on resulting model
$items = Item::with('tags')->get();
return $items->makeHidden(['pivot_col1', 'pivot_col2']...)
2. using array_column function of PHP
$items = Item::with('tags')->get()->toArray();
return array_column($items, 'tags');

Categories