I found 2 ways to do this. Both do the job, neither one is perfect.
Overview
Missions which have multiple *Objective
Mission (1:n) *Objective - one-to-many
Mission hasMany *Objective
Objective(Abstract) belongsTo Mission
*Objective extends Objective
Missions which have multiple Objective which in turn each can be of kind *Objective (intermediate layer)
Mission (1:n) Objective - one-to-many
Objective (1:1) *Objective - one-to-one (polymorphic)
Mission hasMany Objective
Objective morphTo *Objective
*Objective extends Objective
You'd use 2 only if you need an itermediate layer if you want to inherit common attributes for *Objective from Objective - which totally makes sense, since they all at least share mission_id.
You won't be able to get inherited attributes by *Objective out of the box, because the model for any *Objective is bound to a specific table, hence it cant reach the attributes of the inherited Objective class. There is a module for this though: parental
edit: Nevermind, parental only supports STI(Single Table Inheritance) which means we can have multiple models referring the same table.
Solution 1
Tables
missions
id - int
*_objectives (e.g. main_objectives)
id - int
mission_id - int
Models
class Mission extends Model
{
const OBJECTIVE_TYPES = [
MainObjective::class,
// ...
];
public function __get($name)
{
switch ($name) {
case 'objectives':
$objectives = new Collection();
foreach (static::OBJECTIVE_TYPES as $objectiveType) {
$objectives = $objectives->concat($this->hasMany($objectiveType)->get()->all());
}
return $objectives;
default:
return parent::__get($name);
}
}
}
abstract class Objective extends Model
{
public function mission() {
return $this->belongsTo(Mission::class, 'mission_id', 'id');
}
}
class MainObjective extends Objective
{
}
Example
$mission = Mission::find(1);
foreach ($mission->objectives as $objective) {
echo 'Class: ' . get_class($objective) . PHP_EOL;
}
Output (In case we have 2 MainObjectives and 1 SecondaryObjective)
Class: App\Models\MainObjective
Class: App\Models\MainObjective
Class: App\Models\SecondaryObjective
Solution 2
Tables
missions
id - int
objectives
id - int
mission_id - int
objectiveable_type - string
objectiveable_id - int
*_objectives (e.g.: main_objectives)
id - int
objectiveable_id refers the id of the table used by the model in objectiveable_type.
objectiveable_type has to be fully qualified. Example: App\Models\MainObjective
Models
class Mission extends Model
{
public function __get($name)
{
if ($name === 'objectives') {
return new Collection(array_map(function ($objective) {
return $objective->objectiveable;
}, $this->hasMany(Objective::class)->get()->all()));
}
return parent::__get($name);
}
}
class Objective extends Model
{
public function objectiveable()
{
return $this->morphTo();
}
protected function _mission() {
return $this->belongsTo(Mission::class, 'mission_id', 'id');
}
public function __get($name)
{
switch ($name) {
case 'mission':
return $this->morphMany(Objective::class, 'objectiveable')->get()->first()->_mission;
default:
return parent::__get($name);
}
}
}
class MainObjective extends Objective
{
}
Example
$mission = Mission::find(1);
foreach ($mission->objectives as $objective) {
echo 'Class: ' . get_class($objective) . PHP_EOL;
}
Output (In case we have 2 MainObjectives and 1 SecondaryObjective)
Class: App\Models\MainObjective
Class: App\Models\MainObjective
Class: App\Models\SecondaryObjective
Pretty much the same as in the first solution. The big difference here are the attributes. you won't be able to access the attributes of Objective of MainObjective without extending this further.
@Laravel/Eloquent
PLEASE make this easy - my head hurts.