Polymorphic relationship with pivot data - laravel

I would like to solve the following issue:
I got multiple models like:
Product
Customer
Each model should be able to have one or more Fields with pivot data.
Field:
id
title
type
required
Example:
Product has a field called video_url, type should be string containing the pivot value http://youtube.com/....
Customer has a field called external_id, type should be integer containing the pivot value 242.
The fields should be added dynamically by the user. The user should be able to decide, whether the field is morphing to Product or Customer (or even more later).
Maybe this helps to understand:
What I am doing right now
At the moment I created a new Model for each, product and customer
For customers:
class CustomerField extends Model
{
/**
* #return \Illuminate\Database\Eloquent\Relations\belongsToMany
*/
public function customers()
{
return $this->belongsToMany(Customer::class)->withPivot('value');
}
}
For products:
class ProductField extends Model
{
/**
* #return \Illuminate\Database\Eloquent\Relations\belongsToMany
*/
public function products()
{
return $this->belongsToMany(Product::class)->withPivot('value');
}
}
At the moment this works out, but of course, it's not the most elegant way to solve it.
My question
Is there a possibility to morph a field dynamically to Product or Customer with an additional pivot?

I think this is what you want Polymorphic:Many-to-Many
You don't need to add ProductField and CustomerField models,
you just need to add Product, Customer and Field model.
The fields will dynamically belongs to product or customer by fieldable_type. Even you have more models, it will store the model name to this fieldable_type.
And the tables you need to be created like this below:
fieldables table has fieldable_id and fieldable_type;
fieldable_type will set your model name automatically, like App\Product, and you can custom that by yourself in AppServiceProvider:
Relation::morphMap([
'products' => 'App\Product',
'customers' => 'App\Customer',
]);
In Product Model:
class Product extends Model
{
/**
* Get all of the fields for the product.
*/
public function fields()
{
return $this->morphToMany('App\Field', 'fieldable')->withPivot('value');
}
}
In Customer Model:
class Customer extends Model
{
/**
* Get all of the fields for the customer.
*/
public function fields()
{
return $this->morphToMany('App\Field', 'fieldable')->withPivot('value');
}
}
In Field Model:
class Field extends Model
{
/**
* Get all of the products that are assigned this field.
*/
public function products()
{
return $this->morphedByMany('App\Product', 'fieldable');
}
/**
* Get all of the customers that are assigned this field.
*/
public function customers()
{
return $this->morphedByMany('App\Customer', 'fieldable');
}
}
CRUD with Pivot Value:
After that, you can easily create, get, update, delete pivot value like:
Field::first()->products; # return the products with pivot value
Field::first()->customers; # return the customers with pivot value
Customer::first()->fields;
$field = Field::first();
# create new relationship with pivot value between customer and fields:
Customer::first()->fields()->attach($field, ['value' => 'customer new value field']);
# update pivot with value:
Customer::first()->fields()->sync([$field->id => ['value' => 'update customer value field']]);
# Delete pivot
Customer::first()->fields()->detach($field->id);

The best practice is to use a separate table to hold meta information so that you can easily add/remove "columns" as needed
For example, you could set your meta table up like this:
create table `ProductField` (
products_id int(11),
column_name varchar(255),
value varchar(255),
)
Then in your products model, add functionality to get, insert, check if exists, etc.
public function getMeta($column) {
$meta = DB::table('ProductField ')
->select('column_name', 'value')
->where('products_id', '=', $this->id)
->where('column_name', '=', $column)
->get();
if (!$meta->isEmpty()) {
return $meta;
} else {
return null;
}
}
public function addMeta($column, $value) {
DB::table('ProductField ')->insert(
[
'products_id' => $this->id,
'column_name' => $column,
'value' => $value,
]
);
}
The same way you can achieve dynamic nature for Customers too.
You can also use an array to store the feilds and then dynamically add them to the model
foreach ($request->input('cost') as $key=>$cost) {
Price::create([
'product_id' => $request->product_id[$key],
'date' => Carbon::now(),
'cost' => $cost,
'trend' => 0
]);
}
If you know that there will only be certain dynamic fields ahead of time, you could opt to create accessor methods for them

Related

Model file name changes the table name in database

Hello i have a table called order_product that i want to get values from it and the model for that table called order_product with values:
public $timestamps = false;
protected $fillable = [
'order_id',
'product_id',
'amount',
];
This is the code of the model Order :
public $timestamps = true;
protected $fillable = [
'order_number',
'client_id',
'description',
];
public function client()
{
return $this->belongsTo(Client::class);
}
public function products()
{
return $this->belongsToMany(Product::class);
}
public function orders()
{
return $this->belongsToMany(order_product::class);
}
A professional guy helped me and explained to me how the relation worked so the client and products work very good but the orders makes error in the sql.
This is the code im executing in the controller:
$orders = Order::where('id', $id)->firstOrFail();
$orders->load('client', 'products','orders');
The error that i get is:
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'user_project_db.order_products' doesn't exist
What should be the name of the file order_product so the query can execute properly?
I change my answer after reading your answers below.
Your table relationship is orders - order_product - products.
https://webdevetc.com/blog/laravel-naming-conventions/
under Pivot tables
The way you named your pivot table is already correct.
order_product is to connect orders to products in a many-to-many.
So i think you can try to do the following.
Inside model Product add this relationship.
public function orders()
{
return $this->belongsToMany(Order::class, 'order_product');
}
And in model Order add the other connection
public function products()
{
return $this->belongsToMany(Product::class, 'order_product');
}
belongsToMany accepts 2 parameter, 1st is model destination, and 2nd is table pivot name, in your case order_product.
With this , an extra model OrderProduct is optional.
To add a product into order , you can use attach
$order = Order::find($order_id);
$order->products()->attach($product_id);
Or if you have extra fields within pivot table
// first implement this change inside Order model
return $this->belongsToMany(Product::class, 'order_product')
->withPivot('price', 'qty');
// to attach for example price and qty
$order->products()->attach($product_id, ['price' => 200', 'qty'=> 1]);
To query the price
$order_product = $order->products()
->where('product_id', $product_id)
->first()->pivot;
$price = $order_product->price;
$qty = $order_product->qty;
And back to your own query.
No need to add orders() inside Order model.
And load only the first 2 relationship should be enough.
$order->load('clients', 'products');
protected $table = 'order_products; in the model will tell Laravel that the Order model's data is stored in a table by that name.
However, typically you'd have an Order model, a Products model, and a pivot table (potentially with a pivot model, if you need it) titled order_products. https://laravel.com/docs/9.x/eloquent-relationships#defining-custom-intermediate-table-models

How to fix laravel eloquent many to many relation insertion error on wrong table

I have a model class with the name CourseSession and it's table is course_session, and I have another model named Student and it's table is student and every session has many students and also every student could be in many sessions. So I have created a third table named student_session with student_id, session_id, presence and laravels timestamp columns.
Creating a session needs to have a list of students and I get them from $request like this:
$validatedData = $request->validate([
'students' => 'required',
]);
students is array of students id!
this is my CourseSession model:
class CourseSession extends Model
{
protected $table = 'course_sessions';
protected $guarded = [];
public function course()
{
return $this->belongsTo('App\Course');
}
public function students()
{
return $this->belongsToMany('App\Student', 'student_session','session_id', 'student_id');
}
}
and this is my student model:
class Student extends Model
{
protected $guarded = [];
public function courses()
{
return $this->belongsToMany('App\Course');
}
public function exams()
{
return $this->belongsToMany('App\Exam');
}
public function sessions()
{
return $this->belongsToMany('App\CourseSession', 'student_session', 'student_id', 'session_id');
}
}
I create a session like this:
$session = new CourseSession();
$session->course_id = $course->id;
$session->save();
and I save students this way:
$sessions = $session->students()->create(['session_id' => $session->id, 'student_id' => 1, 'presence' => 1]);
but I get this error:
SQLSTATE[42S22]: Column not found: 1054 Unknown column 'session_id' in 'field list' (SQL: insert into `students` (`session_id`, `student_id`, `presence`, `updated_at`, `created_at`) values (25, 1, 1, 2019-04-28 14:24:48, 2019-04-28 14:24:48))
according to error, it tries to write data on students table but I want to write them on student_session table!
what is wrong in my codes?
For this you want to use attach() instead of create().
$session->students()->attach(1, ['presence' => 1]);
In the above example 1 is the student_id. You don't need to specify the session_id as your calling this from a Session model. The array at the end is any additional data you want to add to the pivot table.
Since you have additional data in your student_session pivot table, it might also be an idea to add this to your belongsToMany relationships e.g.
public function sessions()
{
return $this->belongsToMany('App\CourseSession', 'student_session', 'student_id', 'session_id')
->withPivot('presence')
->withTimestamps();
}
This will include the presence and timestamps columns in the pivot data as well (obviously, you don't have to do this if you don't want to though).

HasMany relation(or any relation) data inserted but not return on the same time | Laravel | Eloquent Model

I have a laravel application in which the setting_types and user's settings are saved into different models.
User.php:
/*
* Getting the user's notification setting.
*/
public function notificationSetting()
{
return $this->hasMany('App\NotificationSetting');
}
/*
* Controller function to get the user's settings
*/
public function getSetting(Request $request)
{
$userSetting = $user->notificationSetting;
// check new settings are inserted for user or not.
if (someCondition) {
// add new settings for user.
$user->notificationSetting()->save(new NotificationSetting(['user_id' => $user_id, "notification_type_id" => 121]));
print_r($user->notificationSetting); // still rec. Old values.
}
return $user->notificationSetting;
}
As you can see that I insert the relation object but I didn't receive on the same time. and if I hit again (this time my someCondition become false) so it will return the update records.
Since the save() method returns a boolean, you could write it like this:
$user->notificationSetting()
->save(
$notificationSetting = new NotificationSetting([
'user_id' => $user_id,
'notification_type_id' => 121
])
);
return $notificationSetting;
You might also be able to use the create() method instead, that will return the instance of the model, but only if the attributes are fillable of course.
If you want to retrieve all the related records of a model at any time, you can use the load() method like this:
$user->load('notificationSetting');
It is also important the use the plural form for a hasMany relation in order to distinguish it from a hasOne or a belongsTo relation:
public function notificationSettings()
{
return $this->hasMany('App\NotificationSetting');
}

Laravel getting users from multiple messages

I've got offers table with id's
Messages table with columns offer_id and from
and Users table with id's
I want to get users starting with offer $this in OfferResource
My goal is to get users that have replied to the offer with at least one message.
I started to configure Offer model with function messages to get messages
public function messages(){
return $this -> hasMany('App\Message');
}
so I'm able to get all messages (starting from offer resource):
'users' => $this->messages
How should I now configure messages model to get all users instead of messages?
I tried to write in Message model :
public function fromContact()
{
return $this->hasOne(User::class, 'id', 'from');
}
and then:
'users' => $this->messages->fromContact
but i've got error: "message": "Property [fromContact] does not exist on this collection instance.",
How should I correct my code to make this work?
I am assuming the from field on the Messages table is populated using user ID. Then you could establish belongsToMany relationship between the Offer and User models. Since this is actually a many-to-many relation with a pivot table messages.
In the Offer model define
public function users()
{
return $this->belongsToMany('App\User', 'messages', 'offer_id', 'from');
}
Then from the OfferResource you could load the offers data like this—
$offers = App\Offer::with('users')->get();
Then loop over the $offers like this:
foreach ($offers as $offer) {
dd($offer->users); // one offer will have multiple users as a Collection
}
Similarly for an $offer of ID 1 you could do this
$offer = App\Offer::with('users')->find(1)
Then to get the users that commented on this offer just use $offer->users
See the official documentation for defining many-to-many relationship.
In the Message model you have to spesify the column that refer to the user :
public function fromContact()
{
return $this->hasOne(User::class, 'from');
}
And then after geting the messages loop over them to get the user like this :
foreach ($this->messages as $message) {
$user = $message->fromContact;
// do somthing with the user :)
}
Your messages table has from field that is referencing User model and offer_id field which referencing Offer model that means you have ManyToMany relations between Offer and User.
Offer Model
public function users(){
return $this->belongsToMany(User::class, 'messages', 'offer_id', 'from')->using('App\Message');
}
Message Pivot
class Message extends Pivot {
protected $table = 'messages';
}
User model
public function offers(){
return $this->belongsToMany(Offer::class, 'messages', 'from', 'offer_id')->using('App\Message');
}
OfferResource
public function toArray($request)
{
$users = $this->users;
return [
'id' => $this->id,
... //add your offer fields here
'users' => $users->toArray(), // here $users is a laravel Collection
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
Access from Controller or Route
Route::get('/offer', function () {
return new OfferResource(Offer::with('users')->find(1)); //eager load users
});

Laravel Eloquent Relation Model

I have 3 table on my database :
table 1 = user (hasMany order)
table 2 = order (hasMany order_detail, belongsTo user)
table 3 = order_detail (belongsTo order)
On my order_detail model i add this function :
public function order() {
return $this->belongsTo('App\Order');
}
so i can call the order data without define it from controller, i just define order_detail on my controller
$order_detail->order->invoice_number
but how to call the user data from the order detail?
I try use this
$order_detail->order->user
but it didn't work for me..
Is there any idea to call the grand parent relation?
In order model add function for order_detail:
public function order_details() {
return $this->hasMany('order_detail');
}
and for user:
public function user() {
return $this->belongsTo('user');
}
In user model add:
public function orders() {
return $this->hasMany('order');
}
Then, you can call in the controller:
$details = Order::find(1)->order_details->where('order_detail_id', 5)->first();
$userName = $details->username;
// ^
// column name in order_detail table
More info in the docs.
I think this is the more complete way to define the relationships:
// User model
public function orders(){
// I'm telling that users has many order. The orders have an user_id field that match with the ID field of the user.
return $this->hasMany('App/Order', 'user_id' , 'id');
}
// Order model
public function order_details(){
// I'm telling that order has many order_details. The order_details have an order_id field that match with the ID field of the order.
return $this->hasMany('App/OrderDetail', 'order_id' , 'id');
}
public function user(){
// I'm telling that an order belongs to an user. The user has an ID field that match with the order_id field of the order.
return $this->belongTo('App/User', 'id', 'user_id');
}
// OrderDetail Model
public function order(){
// I'm telling that an order detail belongs to an order. The order has an ID field that match with the order_id field of the order detail.
return $this->belongTo('App/Order', 'id', 'order_id');
}
I have seen that you put just the model name as first parameter in your relation definition. I think you must put the relative path from the root to the Model. In my case, I have the models as childs of the App folder.

Resources