Laravel: chained relationships in a verbose way

Viewed 59

I have several models chained by 1:N relationships:

User (1 -> N) Asignatura (1 -> N) Clase (1 -> N) Asiento

I want to obtain a collection of Asiento models, containing all the Asientos of a User.

I tried the "obvious"

$user->asignaturas->clases->asientos;

but as $user->asignaturas is a collection, ->clases is not recognized as a valid method (all due relationships, asignaturas(), clases() and asientos() has been created in their respective models)

So I did the following to solve the problem:

$user = User::findorFail(Auth::user()->id);
$asignaturasArr = $user->asignaturas->pluck('id')->toArray();
$clases = Clase::whereIn('asignatura_id', $asignaturasArr)->get();
$clasesArr = $clases->pluck('id')->toArray();
$asientos = Asiento::whereIn('clase_id', $clasesArr)->get();

but I suspect that Laravel should have a less verbose way to do the work, and I am not being able to find it.

I will be grateful to have any advice from any expert.

Thank you very much in advance and best regards.

2 Answers

$user->asignaturas->clases->asientos; will not work as asignaturas is a collection, so you would have to do this to work: $user->asignaturas->first()->clases->first()->asientos; but you would have to iterate over each asignaturas and clases and that is for surely not ideal.

So, you could try using hasManyThrough but that would also not work 100%, as it only allows 3 level deep, it would only work for User -> Asignaturas -> Clases (so you get all User's Clases but you would still need to iterate each Clase to get every Asientos), or it would only work for Asignaturas -> Clases -> Asientos (but you would have to write this in Asignatura model, not in user, I still think this would work for you).

So, if we use the last one I mentioned, your Asignatura model should have this:

class Asignatura extends Model
{
    public function asientos()
    {
        return $this->hasManyThrough(Asiento::class, Clase::class);
    }
}

Have in mind that this must follow a standard convention, read the documentation about it, and if it is not working, share your models and tables' names so we can help you make it work.

Then, in your User model:

class User extends Model
{
    public function asientos()
    {
        $asientos = collect();

        $this->asignaturas->each(function ($asignatura) use ($asientos) {
            $asignatura->asientos->each(function ($asiento) use ($asientos) {
                $asientos->push($asiento);
            });
        });

        return $asientos;
    }
}

But, that code is not 100% good, as you are looping a lot, still will work.

Other solution will be to just do a normal query (manually joining) and get the data that way, but I will not do it here as I have no idea what your tables' names are, you can find good tutorials about this, you will have to join tables, really simple to do.


Other possible solution you could try using, but I have never used, is using a package that someone created because of this exact problem, it is called staudenmeir/eloquent-has-many-deep. It can handle "infinite" levels instead of just 3. This is the SO question related to it.

Al last, I think that I found the Eloquent (elegant) solution to the problem. Is the following:

$asientos = $user->asignaturas->map->clases->unique()->flatten()->map->asientos->unique()->flatten();

Using map and unique()->flatten() (and, of course, the necessary relationships in the models), you can chain as much relationships levels as you want.

Related