I switched this website from Symfony to Laravel 2,5 years ago, and I love Laravel Container ever since.
Symfony and Laravel containers are very similar - read this compare post if you know one and want to understand the other.
Yet, I always patch Laravel Container in /vendor
for all projects I use.
Single line change.
Why and where?
Imagine this situation: we're using an open-source package and we want slightly different behavior from one file. We have 3 options:
We fork the package, add our code, and maintain it forever. A plan that sounds too good to be true. In 5 years, this will lead up to a huge mess that someone else will have to clean up.
We can also create a pull request, contribute to the open-source project, and wait for the release of your feature... it might take time, but if others will benefit from it, it's worth it.
We patch the file locally in your /vendor
. This doesn't mean copying /vendor/some/package
to /my-vendor/some/package
, basically a local fork.
Instead, we use the native git patching feature, which allows us to add only the patch file itself. This way we still get updates in the future release, while keeping our feature intact. Win, win!
There is just one catch: in this case, it's not a slightly different feature, but the complete opposite.
Let's say we have a huge project like Rector and we use Laravel Container (illuminate/container
package) as a foundation.
What happens when we try to resolve a service that is not registered in the container?
use Illuminate\Container\Container;
$container = new Container();
$setListProvider = $container->get(SetListProvider::class);
The container will create it using reflection. Nice!
But what happens if we ask for the same service again?
use Illuminate\Container\Container;
$container = new Container();
$setListProvider = $container->get(SetListProvider::class);
// 100 files and 1000 lines later
$setListProvider = $container->get(SetListProvider::class);
Those are 2 different instances.
If we have a project with fewer than 100 services, we most likely won't hit a bottleneck here. Creating duplicated services for the same job doesn't make sense, but we're good performance-wise.
Now, imagine we have 50 services running, each of which has 5-10 dependencies. That's 50 instances of the very same 5-10 services, 250-500 waste instances of the same type, including their constructor dependencies.
Creating service by type via constructor reflection is performance-heavy, and this might hit us hard.
That means our container behaves like a factory, not like a service locator. Why?
You're right, we could use a $container->singleton(...)
call to define all of those services explicitly. But at that point, the framework stopped working for us, and we started working for the framework.
I want a zero-line-config, not 500+ lines I have to maintain across dozens of repositories.
Instead, we want to use the container as a service locator. We ask for a service, and Laravel will handle it for us. If we want a new instance, we use the new
keyword.
This is the default behavior of illuminate/container
we want to change. With a patch.
Fortunately, Laravel Container has a very clean and simple architecture. Only 2 files.
There is a single line that decides: is it a singleton or should we create a new service?
Here: https://github.com/illuminate/container/Container.php#L903
// If the requested type is registered as a singleton, we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
if ($this->isShared($abstract) && ! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
How do we teach Laravel container to always consider service "shared" (= singleton)?
We change the left condition to always true
:
--- /dev/null
+++ ../Container.php
@@ -800,7 +800,7 @@
// If the requested type is registered as a singleton, we'll want to cache off
// the instances in "memory" so we can return it later without creating an
// entirely new instance of an object on each subsequent request for it.
- if ($this->isShared($abstract) && ! $needsContextualBuild) {
+ if (! $needsContextualBuild) {
$this->instances[$abstract] = $object;
}
This way, all our services are created only once. This keeps our ServiceProvider
to a minimum size. We only need them, if we do something more complicated.
We use this patch in many places, so it would be a waste of time to create a new patch for each project. Instead, we store patches in an open-source repository on GitHub.
That way, you can try it in 2 lines:
composer require symplify/vendor-patches --dev
composer.json
{
"extra": {
"patches": {
"illuminate/container": [
"https://raw.githubusercontent.com/rectorphp/vendor-patches/main/patches/illuminate-container-container-php.patch"
]
},
"composer-exit-on-patch-failure": true,
"enable-patching": true
},
"config": {
"allow-plugins": {
"cweagans/composer-patches": true
}
}
}
composer install
This will trigger all patch files and apply them to your /vendor
.
If you update to Laravel 13, the patch will be applied automatically (unless the container is completely rewritten, but we keep patching up to date since Laravel 9).
Happy coding!