Creating Multilevel Relationships with Laravel (Including “belongs to many through”!)


Laravel only provide 2 relationship types out of the box, that supports more than one level of related models: hasOneThrough and hasManyThrough. They are not truly “multilevel” either, as they only support two levels.

hasOneThrough allows you to define relationships like “A Country has one NationalFlag through Constitution.”. This works because a Country only has one Constitution, and a Constitution only has one NationalFlag defined in it.

hasManyThrough allows you to define relationships like “A Country has many Districts through Provinces .”. This works because a Country has many Provinces, and a Province has many Districts.

Has Many Deep

What if a District has many Divisions in it? Can we write a relationship in Country model, that would allow us to fetch Divisions of Districts of Provinces of that Country? No — you can’t do that with Laravel out of the box.

That’s where staudenmeir/eloquent-has-many-deep comes in.

This art piece of a Laravel module allows you to do just that (And more!). Look at this beautiful code:

class Country extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function divisions()
    {
        return $this -> hasManyDeep(
            Division::class,
            [
                Province::class,
                District::class,
            ]
        );
    }
}

What did we just do? We need to look at the DB structures for these entities, first of all.

countries
- id
- name

provinces
- id
- name
- country_id

districts
- id
- name
- province_id

divisions
- id
- name
- district_id

The first argument of the hasManyDeep method asks for the name of the model that we ultimately would like to reach. The next argument asks for an array, that contains the names of the bridging models, in their order of relationship (Eg: Country → has many → Provinces → has many → Districts → has many → Divisions.). As we’re writing the relationship in the Country class, we don’t have to specify that, and as we’ve already specified the ultimately related model in the first argument, we don’t have to specify that in the array either; that leaves us with Province and District, and we specify them in that order.

Cool? Cool!

Belongs to Many Through

What if there’s an entity called IntOrg (International Organization) that has a many to many relationship with Country entity? There are many IntOrgs and many Countrys (I know it’s “countries”; I just want to keep consistency with my entity names.) are members of many of those IntOrgs. This arrangement prompts us to add two more tables to our DB: One for IntOrgs and one as the pivot table of the many to many relationship between Country and IntOrg.

int_orgs
- id
- name

country_int_org
- country_id
- int_org_id

Now, what if we need to add a provincesOfCountries method to the IntOrg class, that would return Provinces of all the Countrys that belong to an IntOrg? We’d do the following.

class IntOrg extends Model
{
    use \Staudenmeir\EloquentHasManyDeep\HasRelationships;

    public function provincesOfCountries()
    {
        return $this -> hasManyDeep(
            Province::class,
            [
                `country_int_org`,
                Country::class,
            ]
        );
    }
}

Just like before, the first argument to hasManyDeep is the ultimately related model, and the second argument is an array. But the array is a bit different: the first element of the array is the name of the pivot table that facilitate the manyToMany relationship between IntOrg and Country. So, every time there is a pivot table between two entities, you have to mention its name in the array of relationships.

Belongs to Many Through with a U-turn

What if we need to add a provincesOfCountriesWithSharedIntOrgs to the Country entity, that would return Provinces of Countrys with shared IntOrgs as the current Country?

class Country extends Model
{
    public function provincesOfCountriesWithSharedIntOrgs()
    {
        return $this -> hasManyDeep(
            Province::class,
            [
                CountryIntOrg::class,
                IntOrg::class,
                CountryIntOrg::class . ' as country_int_org2',
                self::class,
            ]
        );
    }
}

class CountryIntOrg extends Illuminate\Database\Eloquent\Relations\Pivot
{
    use Staudenmeir\EloquentHasManyDeep\HasTableAlias;
}

A Country → belongs to many → IntOrgs → belongs to many → Countrys → has many → Provinces.

This time, we had to introduce a pivot model, because as we have to go through the same pivot table country_int_org twice, so we have to alias the second occasion (Or the first one, or both; they just have to have different names.) with another name, otherwise MySQL would freak out. To set an alias, we have to introduce a pivot model and use the trait Staudenmeir\EloquentHasManyDeep\HasTableAlias in it.

Postface

Using the functionality introduced by hasManyDeep, I was able to plainly delete large chunks of custom code I had to write to augment the lack of multi level relationships in Laravel, in a project I am currently working on at my workplace.

You can find an issue report I made in this module’s GitHub repo at https://github.com/staudenmeir/eloquent-has-many-deep/issues/26. I created that before I found how to alias pivot tables. Reading through the issue report can help you understand it a bit more as well. The author of the repo was good enough to update the README.md to clarify aliasing.

I believe this should be a native functionality of the Laravel framework. Let’s hope it’d be so in the future.

,

Leave a Reply

Your email address will not be published. Required fields are marked *