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 District
s through Province
s .”. This works because a Country
has many Province
s, and a Province
has many District
s.
Has Many Deep
What if a District
has many Division
s in it? Can we write a relationship in Country
model, that would allow us to fetch Division
s of District
s of Province
s 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 → Province
s → has many → District
s → has many → Division
s.). 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 IntOrg
s and many Country
s (I know it’s “countries”; I just want to keep consistency with my entity names.) are members of many of those IntOrg
s. This arrangement prompts us to add two more tables to our DB: One for IntOrg
s 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 Province
s of all the Country
s 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 Province
s of Country
s with shared IntOrg
s 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 → IntOrg
s → belongs to many → Country
s → has many → Province
s.
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.