Avoiding inheritance in Laravel

Main Thread 5 min read

Over the past few years, I've paired with hundreds of developers on their Laravel projects. One of the most common regrets (technical debt) we encounter is the use of inheritance. While inheritance is one of the pillars of object oriented programming, it's actually less beneficial when used alongside a framework like Laravel.

One of the drawbacks is that you inject a layer of separation between your code and Laravel. Ironically, this separation is often the motivation for using inheritance. And initially, things might seem fine.

Yet this separation creates a gap, or in programming what we call low cohesion. This gap gets filled with sparse code, or worse, overridden Laravel behavior.

Let’s consider the following real-world code sample:

namespace App;

use Illuminate\Database\Eloquent\Model;

class BaseModel extends Model
{
    protected $guarded = [];

    /**
     * @override
     */
    public function fill(array $attributes)
    {
        $totallyGuarded = $this->totallyGuarded();

        foreach ($this->fillableFromArray($attributes) as $key => $value) {
            $key = $this->removeTableFromKey($key);

            // The developers may choose to place some attributes in the "fillable" array
            // which means only those attributes may be set through mass assignment to
            // the model, and all others will just get ignored for security reasons.
            if ($this->isFillable($key) || in_array($key, $this->allowedOverrides())) {
                $this->setAttribute($key, $value);
            } elseif ($totallyGuarded) {
                throw new MassAssignmentException(sprintf(
                    'Add [%s] to fillable property to allow mass assignment on [%s].',
                    $key, get_class($this)
                ));
            }
        }

        return $this;
    }

    protected function allowedOverrides()
    {
        return [];
    }
}

This BaseModel parent class contains code which sets the guarded property, essentially making all child models unguarded.

It also overrides the core fill method to inject even further custom behavior when setting model attributes through mass assignment.

Again, this all seems fine in the beginning. After all, why set this behavior for every model when you can set it once in a parent class? Don't repeat yourself, right?

Well, yes, but at what cost?

Understanding the tradeoff

Everything in programming is a tradeoff. In the case of inheritance, you’re trading reuse at the expense of being unused.

Initially this seems like a good trade, when reuse is high. But over time reuse lessens, and inhreitance becomes a very hard-to-spot form of dead code. In this case, code which is not used in the child class.

Taking a look at the previous example, let's focus on the fill method. This is an example of both code which is not used and code which overrides core Laravel behavior - a lethal combination when maintaining your Laravel applications.

Distinguishing your custom code from the default fill code becomes harder over time. Furthermore, determining how the code evolved from version to version becomes challenging. Answering these questions requires meticulously reviewing the original framework code and comparing it against your own.

You're then faced with a new set of questions like, what did this code do? Why did we change it? Do we need it? Even the original author may struggle to remember these answers.

In this case, after careful review, there was a single change in the conditional:

if ($this->isFillable($key) || in_array($key, $this->allowedOverrides())) {
    $this->setAttribute($key, $value);
}

This custom code calls the custom allowOverrides method. An audit of the codebase might reveal not many models use this behavior. Now the tradeoff of inheritance is no longer in your favor.

An alternative to inheritance

Using inheritance within Laravel violates a principle I like to call grok the framework.

It doesn't take much looking around in Laravel, specifically within model classes, to see it’s much more common to add behavior with a trait than inheritance. Ready examples of these include SoftDelete and Notifiable.

Instead of forcing all models (even those which may not need the behavior) to extend a base class using inheritance, we can decorate only the classes which truly need this behavior with a trait.

For example, consider a Post model with the same behavior.

namespace App;   

use App\Traits\Unguarded;
use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    use Unguarded;

    // ...
}

This code not only aligns with Laravel more closely, but it also clearly communicates the intention of this additional functionality.

In doing so, we also alleviate the need to override core behavior. By simply decorating this and limiting the scope we know model's which use this trait require the custom fill behavior. As such, we are more free to use alternatives than override Laravel.

In doing so, we check all the boxes.

  • ✅ Clear
  • ✅ Symmetrical
  • ✅ Maintainable

Grokking Laravel

You may be wondering, how does the Unguarded trait work? It's not possible in PHP to override a property value from a trait. At least not without some nasty syntax in the use statement.

Well, even though PHP can’t do it (as of version 7.4), Laravel can.

This continues to emphasizes the importance of grokking the framework. Something that may not have been traditionally available, might be available through the framework. If we challenge ourselves to follow patterns within Laravel we may ultimately find a solution which is less complex, and more readable.

For this specific case, Laravel includes a bootTraits method. This method attempts to call methods which may exist on the trait to boot and initialize them.

We can define one of these methods, in this case an initializeUnguarded method to override the properties which unguard our model.

namespace App\Traits;

trait Unguarded
{
    public function initializeUnguarded()
    {
        self::$unguarded = true;
        $this->guarded = [];
    }
}

Summary

There are often better alternatives than inheritance. Ones that more closely align you with the framework, and make your intentions more clear. They also follow more modern practices to prefer composition over inheritance.


Want more 🔥Laravel tips? Follow me (@gonedark) on Twitter as I share ways to streamlining the code within your Laravel applications and more articles like this one.