Chuniversiteit logomarkChuniversiteit.nl
“Heap, Heap, Array!”

Handling model inheritance in Laravel’s Eloquent ORM without third-party libraries (is a bad idea)

It’s technically possible to make Eloquent models inherit from each other, but I probably wouldn’t recommend it.

A square peg (Laravel) is forced into a round hole (best practices)
It’s like forcing a square peg into a round hole

In this post I’m going to pretend that we’re building a Laravel app. It doesn’t really matter what the app does. What’s important is that for some undisclosed and very unimportant reason this app’s domain involves two types of animals: cats and dogs.

Almost every introductory textbook or blog post on object-oriented programming will tell you that you should model this domain using inheritance and polymorphism, so that’s exactly what we’ll do here.

The figure below shows the class diagram for our domain. As you can see, it’s pretty simple:

  • All animals have names, can greet others and sit;
  • Additionally, dogs can be asked to roll over;
  • Cats must have a servant, while dogs must have an owner.

We want our application to create Animal objects and store them in its database.

This should be an easy task for Eloquent, Laravel’s , but curiously enough its documentation doesn’t mention inheritance, subclasses or polymorphic hierarchies even once. So how do we go about implementing this model using Eloquent?

Without inheritance

Link

There are two common ways to implement inheritance in Laravel, but before we go into these approaches let me first show why we would want to use inheritance.

The snippets below show a first attempt to implement the domain model without inheritance. This means we have an Animal class which we’ll use for everything.

We can distinguish between different types of Animals using an Animal\Type enum:

The Animal entity class contains all fields and methods that we need for both types of animals. I tried to make its methods and a little bit more sane by making them throw exceptions when someone attempts to access them on the wrong type of Animal. It’s ugly, but at least it (sort of) works.

The migration for the Animal entity is simple. We only have to create one table, which we’ll name animals:

I wrote an AnimalTest with a few unit tests to illustrate what it’s like to work with this Animal class:

Most things works as expected, but there are a few things that make me sad.

All fields in our class diagram are required, but this is not actually the case in our implementation. Only the name and type fields are required. Unless we add some custom validation, Laravel happily lets us create cats without servants and dogs without owners.

Moreover, as far as static analysers (like those in your IDE) are concerned, all Animals are equal. They don’t understand that cats can only have servants and that only dogs can roll over, and thus are unable to provide suggestions or point out mistakes. Instead, you’ll be forced to use if statements everywhere and simply hope for the best:

(Don’t use) single-table inheritance

Link

We’d like to rely less on if statements and , and more on type annotations that clearly communicate to programmers and static analysis tools what we’re dealing with:

Doctrine, Eloquent’s major competitor, provides support for several types of inheritance mapping, of which single table inheritance is probably the easiest to understand. In our case, this would mean that we have a single table animals that stores the data for all instances of Animal, including that of its child classes Cat and Dog.

Let’s see if we can try to get something like this working using Eloquent. We’ll start with a new version of the Animal class. This time we make it abstract so that it cannot be instantiated directly. We also get rid of everything that’s not shared by all animals:

We’ll move Cat-specific attributes and methods to the Cat class, which extends from Animal:

The Dog class looks a lot like Cat, but its internal type is Type::DOG and it’s capable of barking and rolling over:

The @property PHPDoc annotation on these classes tell IDEs that only cats can have servants and only dogs can have owners. The greet() method has also become a lot simpler now that the logic for cats and dogs no longer resides in the same file. These seem like clear improvements!

However, we have also done some things that might come back and bite us in the ass later:

  • By default, Laravel determines the table name of an entity by pluralising its name and converting it to snake case. But that’s not what we want: cats and dogs are animals, and thus should all be stored in the animals table.

  • Each subclass has its own . In this case, we’d like to array_merge() the fillable attributes of each subclass with those of its parent class. Sadly this isn’t possible, so we’ll either have to use a non-standard method to tell Laravel which attributes are “fillable” or manually repeat ourselves across all Animal subclasses (which we’ve done here).

  • Eloquent doesn’t understand class hierarchies. This is why we have to tell it explicitly how the Cat and Dog classes should behave, by providing some definitions in the static boot() function. For now I have only told it that Cats and Dogs are distinguished by the value of their type attribute (which has now become internal) and that its value should be used when fetching entities via the query builder.

Despite these changes, the migration code that we need for Animal, Cat and Dog is identical to the migration that we used previously:

That is, of course, not to say that nothing has changed. The updated version of the AnimalTest shows that things have indeed changed:

The code has become a bit clearer, as it now involves actual Cats and Dogs rather than generic Animals with a type. However, not only is it still possible to create dogs without owners, we can now also create dogs that have servants and cats that have owners?!

Another major problem is that relationships with Animals don’t work. Imagine we want to keep track of the type of food that each animal eats. Normally, we’d create a new Food entity that might look a bit like this:

We’ll also define the inverse of this new relationship on the Animal class:

But this won’t work the way we want it to, because when we try to retrieve the animals that eat a particular type of food, PHP will tell us that it can’t instantiate Animal classes because they’re abstract:

Although I’m sure that all these issues can be fixed by overriding default Eloquent functionality, it’s rarely a good idea to fight the framework. I guess we’ll have to look for a more Laravel-esque approach…

Multi-table inheritance

Link

Eloquent provides support for polymorphic relationships, in which an entity can be related to multiple other types of entities which can be completely unrelated to each other. , but it appears to be the most straightforward way to implement something that looks a bit like multi-table inheritance.

Let’s start with a minimal Animal entity class again:

This Animal class has a MorphTo relationship that’s named species. We’ll refer to this same relationship in the Cat and Dog classes, which extend from Animal. For instance, this is what the Cat class looks like:

The Dog class is virtually identical to the Cat class, except for a few small additions (which I’ll get back to later):

As is already implied by the name “multi-table inheritance”, the migration creates three tables; one for each class:

The morphs() method creates two columns, species_type and species_id, which are used to store the fully-qualified class name (FQCN) and the id of the corresponding entity respectively.

Naturally I’ve also updated the AnimalTest, which doesn’t contain any failing tests this time:

This approach, although better than the two before it, is still far from perfect. My biggest gripe about this implementation is that the model is all wrong.

Yes, the Cat and Dog classes inherit from Animal in the traditional sense, which means that cats and dogs “are” animals. However, the Eloquent relationship suggests that Cats and Dogs “have” an Animal. test_cat_happy_path() clearly shows how awkward it is to work with these classes:

  • Cat::create() doesn’t give us a “complete” object. We also need to create the corresponding Animal entity by calling $cat->animal()->create(), which will likely end up getting a different id than the Cat.

  • We cannot retrieve the name of a Cat directly by accessing the name attribute – we have to do it through the species relationship: $cat->animal->name.

These issues can be solved by patching the boot() function in each Model and possibly overriding some other default Eloquent behaviour (like how Dogs name attribute gets and sets its value).

Is this approach better than sprinkling if statements everywhere? I personally think it’s worth it, but only if you can completely hide the implementation of these classes from the outside world.