SPQRInc
SPQRInc

Reputation: 188

Polymorphic relationship with pivot data

I would like to solve the following issue:

I got multiple models like:

Each model should be able to have one or more Fields with pivot data.

Field:

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:

enter image description here

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?

Upvotes: 2

Views: 4630

Answers (2)

TsaiKoga
TsaiKoga

Reputation: 13394

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:

enter image description here

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

Upvotes: 11

Sreeram Nair
Sreeram Nair

Reputation: 2387

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

Upvotes: 0

Related Questions