Laravel Under The Hood - Extending the framework
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?
- 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. - 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
andforwardDecoratedCallTo
. 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!
driesvints liked this article