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.