Jamie
Jamie

Reputation: 91

How to define a hasMany relationship to multiple classes?

In Laravel, let's say I want to have a Mission with different MissionObjectives, each MissionObjective has it's own model. I have Mission, CheckboxObjective, RadioObjetive and SpinnerObjective. I need to connect the Objectives to the Mission, but I don't want to use a different method for each Objective, I want them all in one method. I tried polymorphic relations but they just don't seem to work for this. I would need a Many-to-One, sort of, which doesn't exist.

This is what I want:

Mission class:

public function objectives() {
    return *All objectives*;
}

Objective classes:

public function mission() {
    return $this->hasOne('App\Models\Mission');
}

Upvotes: 2

Views: 1608

Answers (1)

SirPilan
SirPilan

Reputation: 4857

I found 2 ways to do this. Both do the job, neither one is perfect.

Overview

  1. Missions which have multiple *Objective

    Mission (1:n) *Objective - one-to-many

    • Mission hasMany *Objective
    • Objective(Abstract) belongsTo Mission
    • *Objective extends Objective

  1. 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.

Upvotes: 1

Related Questions