简体   繁体   中英

Multi-Table Inheritance in Laravel Eloquent

For writing on a PDF document, the user should be able to create different "modules" that can be reused in several Documents . There are normal modules ( Module ) having the attributes name, posX, posY and eg TextModule which has all the attributes Module has but adds text, font, color, size . This is what you would normally achieve using inheritance. I found several ways to build single-table inheritance with Eloquent but this would lead to a lot of NULL values in the database because all Module objects won't have any text, font, color or size . Unfortunately, I have not found any multi-table inheritance documentation for Eloquent.

This is what I have so far:

class Module extends Model
{
    protected $fillable = [
        'name', 'posX', 'posY'
    ];

    public function document()
    {
        return $this->belongsTo('App\Document');
    }
}

class TextModule extends Module
{
    protected $fillable = [
        'text', 'font', 'color', 'size'
    ];
}

Furthermore, my apporach was to create two migrations (because I need multi-table inheritance) and have every common attribute in the create_modules_table migrations, whereas I have added every "special" attribute to the create_textmodules_table .

My wish is to call Module::all() to retrieve any kind of modules, so Module and TextModule in this example. For every object contained in the returned collection it should be possible to call obj->document to retrieve the corresponding document (and vice versa for Document::hasMany(Module::class) relationship). At the moment I only receive all Module objects when calling Module::all() without any error message.

Am I on the wrong track with my approach?

Instead of using a separate table for each special case of Module I would suggest a nested set implementation. It is mainly used for nested categories on web pages but can be in theory used for any kind of parent/child relationships. Have a look at the following Laravel-nestedset package.

If you don't mind your data being stored as json (so you know what you loose there) I might suggest a different approach. A very basic example, having a field text column, might be (untested code):

class Module extends Model
{
    protected $fillable = [
        'name', 'posX', 'posY', 'field'
    ];

    protected $casts = [
        'field' => 'object'
    ];

    public function document()
    {
        return $this->belongsTo('App\Document');
    }
}

class TextModule extends Module
{
    protected $appends = [
        'text', 'font', 'color', 'size'
    ];

    public function getTextAttribute(): string
    {
        return $this->field->text;
    }

    public function setTextAttribute(string $value): void
    {
        $field = $this->field;
        $field->text = $value;
        $this->field = $field;
    }

    // etc...
}

Clearly, this way, you're trading data integrity for flexibility so I would suggest it only when the first is far less important than the latter. For example I used this pattern before while creating an html email composer. Each time the management asked for a new field type it took me minutes to implement it, all without having to create new database migrations. But, again, that's just because in this specific project I didn't really mind about data integrity.

您可以使用此页面进一步参考: Laravel 用户类型和多态关系

Thanks to @sss S's link about polymorphic relationships in Laravel I finally figured out how to solve my issue:

Models

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

class TextModule extends Model {
  public function module() {
    return $this->morphOne('App\Module', 'moduleable');
  }
}

Migrations

Schema::create('modules', function (Blueprint $table) {
  $table->bigIncrements('id');
  $table->float('posX');
  // ... other fields mentioned above
  $table->morphs('moduleable'); // this creates a "moduleable_id" and "moduleable_type" field
  $table->timestamps();
});

Schema::create('textmodules', function (Blueprint $table) {
  $table->bigIncrements('id');
  // ... only the fields that are exclusive for a TextModule (= not in Module, except "id")
});

Factories

$factory->define(TextModule::class, function (Faker $faker) {
    return [
        // ... fill the "exclusive" fields as usual
    ];
});

$factory->define(Module::class, function (Faker $faker) {
  $moduleables = [
    TextModule::class,
    // ... to be extended
  ];

  $moduleableType = $faker->randomElement($moduleables);
  $moduleable = factory($moduleableType)->create();

  return [
    // ... the fields exclusive for Module
    // add the foreign key for the created "moduleable" (TextModule)
    'moduleable_id' => $moduleable->id,
    'moduleable_type' => $moduleableType
    ];
});

Controller

public function index() {
  $all = \App\Module::whereHasMorph('moduleable', '*')->with('moduleable')->get();
  return response()->json($all);
}

The wildcard * will show any specific Module (eg TextModule, ImageModule) that was configured following the steps above. Adding ->with('moduleable') directly populates the "specific" attributes for every Module . Have a look at the section "Querying Polymorphic Relationships" in the official Laravel documentation for further information.

Output

[{
   "id":1,
   "posX":34.47,
   "posY":17.04,
   "moduleable_type":"App\\TextModule",
   "moduleable_id":1,
   "created_at":"2019-12-02 20:08:01",
   "updated_at":"2019-12-02 20:08:01",
   "moduleable":{
      "id":1,
      "font":"Arial",
      "color":"#94d22e",
      "size":12,
      "created_at":"2019-12-02 20:08:00",
      "updated_at":"2019-12-02 20:08:00"
   }
}]

Because I haven't managed to find a comprehensive tutorial for this scenario on the internet I decided to publish my GitHub repository for playing around.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM