jvndev
jvndev

Reputation: 322

Laravel strategy-like design pattern -> polymorphic relationships

What would be the best way to achieve this in Laravel? I'm mentioning Laravel specifically because I'm still rather new to Laravel. I'm trying to implement it therein, and I'm sure there's an aspect of Laravel that I'm not familiar with yet that will solve this elegantly.

Let me clarify with a simple example:

The original design

Suppose you have a car model:

class Car extends Model
{
    protected $fillable = [
        'manufacturer',
        'model',
        'engine', //json field emulating a table
    ]
}

The non-normalized 'engine' string field contains json. The json will always have a 'type' attribute but from there it goes in all directions. Examples (using my incomplete knowledge of car engines):

Use case

The Car model will have a Vue page with a dropdown list of the various engine types. On selection, a component-specific to the engine type and it's respective attributes will be presented.

My solutions

  1. Keep it as is

I use an Engine JsonResource to emulate a model for communication between the controller and the page.

  1. A single Engine model with dynamically added attributes and then inject it into the Car model. I've only speculated on this. The attributes added in the constructor doesn't reflect. But the gist of it is:
class Engine extends Model
{
    public function __construct(array $attributes=[])
    {
        parent::__construct($attributes);

        foreach ($this->attributes()->get() as $attr) {
            $name = $attr->name;

            $this->append($name)->$name = $attr->value;
            $this->fillable[] = $name;
        }
    }

    public function attributes() {
        return $this->hasMany(EngineAttribute::class);
    }
}

class EngineAttribute extends Model
{
    protected $fillable = [
        'name',
        'value',
    ];
}

The wood and the trees

The first solution works the best so far but violates 1NF and isn't properly scalable. The second solution introduces horrible complexity further on in the app, and I'd rather avoid it.

Suggestions, alternate solutions, suggested revisions to my question, or general illumination will be greatly appreciated.

(edit: fixed the JSON examples)

Update

dung ta van: thanks again for your answer my man. But I see your Laravel style and I raise you with something even more Laravel style. It was an arduous journey. Filled with strange traits and terrifying Pivot extending joining models spawned by hideous joining tables capturing the souls of many a model via their true class name. But I return safely with a tatter of sanity luckily still intact and bring us this prize: polymorphic relationships I applied this strange magic I found to your already nice code and lo! It started transforming into:

<?php

class EngineType extends Model {
    public function attributes() 
    {
        return $this->morphMany(
            EngineAttribute::class,
            'attributable'
        );
    }
}

class EngineAttribute extends Model {
    public function attributable()
    {
        return $this->morphTo();
    }
}

class Car extends Model {

    public function engineType() {
        return $this->hasOne(EngineType::class);
    }
}

The tome of Ottwell I referenced above contains the rest of the incantations, but this untested alchemy will provide the idea.

Upvotes: 1

Views: 405

Answers (1)

dung ta van
dung ta van

Reputation: 1026

In my opinion, the option 2nd is better, it's clearer and scalable, but we can improve a bit, we can remove Engine class

In php style

class EngineType {
    public $id;
    public $typeName;
}

class EngineAttribute {
    public $cardId;
    public $engineTypeId;
    public $name;
    public $value;
}

class Car {
    public $id;
    public $manufacturer;
    public $model;
    public $engineTypeId;
}

In laravel style:

class EngineType extends Model {
}

class EngineAttribute extends Model {
}

class Car extends Model {

    public function engineType() {
        return $this->hasOne(EngineType::class);
    }

    public function attributes() {
        return $this->hasManyThrough(EngineAttribute::class, EngineType::class, 'card_id', 'engine_type_id', 'id', 'id');
    }

    public function toJson() {
        return json_encode($this);
    }
}

Upvotes: 1

Related Questions