Laravel Under The Hood - How to Extend the Framework

Laravel Under The Hood - How to Extend the Framework

posted Originally published at blog.oussama-mater.tech 5 min read

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!

If you read this far, tweet to the author to show them you care. Tweet a Thanks
How is it important for a Laravel developer to be able to develop a POS system? At what number of years of experience you able to develop a POS system in Laravel?
I dont measure experience by years because it varies from person to person, depending on the types of projects they work on, their environment, etc. As for a POS, I wouldnt build one from scratch, there are plenty of open source projects that serve as great starting points and can be customized to fit your needs.

One example is NexoPOS, a fully open source and well maintained POS built with Laravel. If you are familiar with Laravel, you can easily get started with it.

https://github.com/Blair2004/NexoPOS
Thanks Oussama. It is very helpful.
Great article, Oussama! I really enjoyed how you walked through your thought process and explored different solutions before settling on the decorator approach.
Question: have you encountered any performance considerations when extending core framework functionalities like this? Would wrapping the Generator in a custom class introduce any noticeable overhead in large-scale test suites?
This was a great read! I really appreciate the solution you proposed for creating a custom modifier for Laravel’s FakerPHP. The idea of using a "decorator" pattern to extend the functionality of Faker while keeping the original logic intact is both simple and effective. It’s also nice how you emphasized the importance of starting with simple solutions before diving into complex design patterns. I’ll definitely be using this approach in my own projects! Thanks for sharing your thought process and the detailed code example.

I wonder though, have you considered any potential downsides or limitations to this approach, especially with future updates to FakerPHP?

More Posts

Learn to manage Laravel 10 queues for efficient job handling and implement real-time notifications seamlessly.

Gift Balogun - Jan 1

Learn how to write GenAI applications with Java using the Spring AI framework and utilize RAG for improving answers.

Jennifer Reif - Sep 22, 2024

Choosing the Right Solana Wallet: Phantom vs. Solflare vs. Sollet

adewumi israel - Jan 20

Right-Sizing Microservices: Harnessing Integrators and Disintegrators for Striking the Perfect Balance

sidathm - Jan 18

Advanced Strategies: How to Implement the Strategy Pattern in Your NestJS Projects

fredydlemus - Jan 29
chevron_left