Support the ongoing development of Laravel.io →
Article Hero Image

Laravel Under The Hood - Extending the framework

28 May, 2024 6 min read

Photo by Alen Jacob on Unsplash

Hello 👋

A few days ago, I was fixing a flaky test, and it turned out I needed some unique and valid values within my factory. Laravel wraps FakerPHP, which we usually access through the fake() helper. FakerPHP comes with modifiers like valid() and unique(), but you can use only one at a time, so you can't do fake()->unique()->valid(), which is exactly what I needed. This made me think, what if we want to create our own modifier? For example, uniqueAndValid(), or any other modifier. How can we extend the framework?

Thinking out loud

I will be dumping my train of thought.

Before jumping into any over-engineered solution, I always want to check if there is a simpler option and understand what I'm dealing with. So, let's take a look at the fake() helper:

function fake($locale = null)
{
    if (app()->bound('config')) {
        $locale ??= app('config')->get('app.faker_locale');
    }

    $locale ??= 'en_US';

    $abstract = \Faker\Generator::class.':'.$locale;

    if (! app()->bound($abstract)) {
        app()->singleton($abstract, fn () => \Faker\Factory::create($locale));
    }

    return app()->make($abstract);
}

Reading the code, we can see that Laravel binds a singleton to the container. However, if we inspect the abstract, it's a regular class that does not implement any interface, and the object is created via a factory. This complicates things. Why?

  1. Because if it were an interface, we could just create a new class that extends the base \Faker\Generator class, add some new features, and rebind it to the container. But we don't have this luxury.
  2. There is a factory involved. This means it's not a simple instantiation, there is some logic being run. In this case, the factory adds some providers (PhoneNumber, Text, UserAgent, etc..). So, even if we try to rebind, we will have to use the factory, which will return the original \Faker\Generator.

Solutions 🤔? One might think, "What's stopping us from creating our own factory that returns the new generator as outlined in point 1?" Well, nothing, we can do that, but we won't! We use a framework for several reasons, one of them being updates. What will happen if FakerPHP adds a new provider or has a major upgrade? Laravel will adjust the code, and people who haven't made any changes won't notice anything. However, we would be left out, and our code might even break (most likely). So, yes, we don't want to go that far.

So, what do we do?

Now that we've explored the basic options, we can start thinking of more advanced ones, like design patterns. We don't need an exact implementation, just something familiar to our problem. This is why I always say it's good to know them. In this case, we can "decorate" the Generator class by adding new features while maintaining the old ones. Sounds good? Let's see how!

First, let's create a new class, FakerGenerator:

<?php

namespace App\Support;

use Closure;
use Faker\Generator;
use Illuminate\Support\Traits\ForwardsCalls;

class FakerGenerator
{
    use ForwardsCalls;

    public function __construct(private readonly Generator $generator)
    {
    }

    public function uniqueAndValid(Closure $validator = null): UniqueAndValidGenerator
    {
        return new UniqueAndValidGenerator($this->generator, $validator);
    }

    public function __call($method, $parameters): mixed
    {
        return $this->forwardCallTo($this->generator, $method, $parameters);
    }
}

This will be our "decorator" (kinda). It is a simple class that expects the base Generator as a dependency and introduces a new modifier, uniqueAndValid(). It also uses the ForwardsCalls trait from Laravel, which allows it to proxy calls to the base object.

This trait has two methods: forwardCallTo and forwardDecoratedCallTo. Use the latter when you want to chain methods on the decorated object. In our case, we will always have a single call.

We also need to implement the UniqueAndValidGenerator, which is the custom modifier, but this is not the point of the article. If you are interested in the implementation, this class is basically a mixture of the ValidGenerator and UniqueGenerator that ship with FakerPHP, you can find it here.

Now, let's extend the framework, in the AppServiceProvider:

<?php

namespace App\Providers;

use Closure;
use Faker\Generator;
use App\Support\FakerGenerator;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->app->extend(
            $this->fakerAbstractName(), 
            fn (Generator $base) => new FakerGenerator($base)
        );
    }

    private function fakerAbstractName(): string
    {
        // This is important, it matches the name bound by the fake() helper
        return Generator::class . ':' . app('config')->get('app.faker_locale');
    }
}

The extend() method checks if an abstract matching the given name has been bound to the container. If so, it overrides its value with the result of the closure, take a look:

// Laravel: src/Illuminate/Container/Container.php

public function extend($abstract, Closure $closure)
{
    $abstract = $this->getAlias($abstract);

    if (isset($this->instances[$abstract])) {
         // You are interested here
        $this->instances[$abstract] = $closure($this->instances[$abstract], $this);

        $this->rebound($abstract);
    } else {
        $this->extenders[$abstract][] = $closure;

        if ($this->resolved($abstract)) {
            $this->rebound($abstract);
        }
    }
}

That's why we defined the fakerAbstractName() method, which generates the same name the fake() helper binds in the container.

Recheck the code above if you missed it, I left a comment.

Now, every time we call fake(), an instance of FakerGenerator will be returned, and we will have access to the custom modifier we introduced. Every time we invoke a call that does not exist on the FakerGenerator class, __call() will be triggered, and it will proxy it to the base Generator using the forwardCallTo() method.

That's it! I can finally do fake()->uniqueAndValid()->randomElement(), and it works like a charm!

Before we conclude, I want to point out that this is not a pure decorator pattern. However, patterns are not sacred texts; tweak them to fit your needs and solve the problem.

Conclusion

Frameworks are incredibly helpful, and Laravel comes with a lot of built-in features. However, they can't cover all the edge cases in your projects, and sometimes you might hit a dead end. When that happens, you can always extend the framework. We've seen how simple it is, and I hope you understood the main idea, which applies beyond just this Faker example.

Always start simple and look for the simplest solution to the problem. Complexity will come when it's necessary, so if basic inheritance does the trick, there's no need to implement a decorator or anything else. When you do extend the framework, ensure you don't go too far, where the loss outweighs the gain. You don't want to end up maintaining a part of the framework on your own.

If you have any thoughts we can include in the article, feel free to ping me!

Last updated 3 weeks ago.

driesvints liked this article

1
Like this article? Let the author know and give them a clap!
oussamamater (Oussama Mater) I'm a software engineer and CTF player. I use Laravel and Vue.js to turn ideas into applications 🚀

Other articles you might like

Article Hero Image November 18th 2024

Laravel Custom Query Builders Over Scopes

Hello 👋 Alright, let's talk about Query Scopes. They're awesome, they make queries much easier to r...

Read article
Article Hero Image November 19th 2024

Access Laravel before and after running Pest tests

How to access the Laravel ecosystem by simulating the beforeAll and afterAll methods in a Pest test....

Read article
Article Hero Image November 11th 2024

🍣 Sushi — Your Eloquent model driver for other data sources

In Laravel projects, we usually store data in databases, create tables, and run migrations. But not...

Read article

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.