Do you want to migrate your Symfony project to Laravel and not sure if it "handles it"? Switching containers is pretty straightforward for the most parts.
But can Laravel handle advanced features such as compiler passes?
Let's take it step by step and list the Symfony container life-cycle. It has 3 separate steps that run one after another:
The first step is the syntax sugar difference between Laravel and Symfony. The last step is absent and is not needed in Laravel.
The middle step is what Symfony calls compiler passes.
One example for all in Easy Coding Standard - some checkers from PHP CodeSniffer/php-cs-fixer exclude each other and would run in an infinity loop, e.g., turn all spaces to tabs and turn all tabs to space.
ECS has a compiler pass to avoid this loop:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class AvoidCheckersLoopCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
$checkerClasses = [];
// here, we check all the registered services
foreach ($containerBuilder->getDefinitions() as $definition) {
$serviceClass = $definition->getClass();
if ($this->isSniffOrFixerClass($serviceClass)) {
$checkerClasses[] = $serviceClass;
}
}
$this->ensureNoMutuallyExcludingCheckers($checkerClasses);
}
}
As we know, the compiler passes in Symfony are run after every service is registered*.
One month ago, I migrated the ECS container from Symfony to Laravel, and I had to solve this issue. When I googled "compiler pass in Laravel", I got Symfony docs, my post about compiler passes, and the third link goes to Blade template documentation.
GPT is more helpful and shows me the beforeResolving()
method. How do we use it to achieve a similar check?
This method has 2 use cases. The first way is the following:
use Illuminate\Container\Container;
$container = new Container();
$container->beforeResolving(SomeType::class, function () {
// call this
});
The callback in the 2nd argument will be run before the resolving of SomeType
service. This means when a project needs SomeType
, it will invoke this closure. But we don't want to wait for a specific type; we want to call closure before resolving any type.
That's where the other way comes to the rescue:
use Illuminate\Container\Container;
$container = new Container();
$container->beforeResolving(function () {
// call this
});
That's the missing piece. You can probably figure out the rest now. I'll share my way of making the most out of this feature.
I've added a closure that checks for conflicting checkers:
use Illuminate\Container\Container;
$container = new Container();
$container->beforeResolving(function (Container $container): void {
$sniffsIterator = $container->tagged(\PHP_CodeSniffer\Sniffs\Sniff::class);
$fixersIterator = $container->tagged(\PhpCsFixer\Fixer\FixerInterface::class);
$checkerClasses = [];
foreach ($sniffxIterator as $sniff) {
$checkerClasses[] = get_class($sniff);
}
foreach ($fixersIterator as $fixer) {
$checkerClasses[] = get_class($fixer);
}
$this->ensureNoMutuallyExcludingCheckers($checkerClasses);
});
This works, but it's also running before any service is built. Do we have 500 services? This closure is run 500 times. That's not very environmentally friendly.
We add a helper variable to ensure the callback is run just once:
use Illuminate\Container\Container;
$hasRun = false;
$container = new Container();
$container->beforeResolving(function (Container $container) use (&$hasRun) {
if ($hasRun) {
return;
}
// ...
$hasRun = true;
});
That's it! As always, have you found a better way of achieving the same result? Let me know, I want to use it.
Happy coding!
Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!