Best Practice for Symfony Console in NetteFilip Procházka
If you use Symfony\Console in Nette, you will be probably familiar with
php index.php command approach.
It has been obsolete since Nette 2.3, and we should all migrate to its successor.
This blog post will show you why and how.
Running console through the
www/index.php was introduced in Kdyby\Console by me (Filip Procházka) and the practice is now deprecated. This article shows why it was introduced, why it is deprecated and how to use Symfony\Console more elegantly.
Why was running through index.php introduced
For example, you might have a mailer service that handles rendering of a template for the email and then sending it. And a regular email probably has some links in it. That is what the
LinkGenerator is for, which was introduced in Nette 2.3.
Before Nette 2.3 the simplest way to solve this was to fetch the current instance of
Nette\Application\Application, pass it to the template you're trying to render and only then the URL generating was working. And to have the
cli, you had to execute
Nette\Application\Application that would create the presenter so you can generate the URL.
In short, every time you call
php www/index.php command, Kdyby\Console is
- taking over the Nette routing with
CliRoutecreates an application request for
CliPresentercalls Symfony Application and passes your command name and arguments
This guarantees you'll always have an
Nette\Application\Application that can be used for generating URLs.
Missing Http\Url problem
UI\Presenter in the past) available is not enough for generating URLs. It requires an
Http\Url instance that is by default fetched from the
Http\IRequest service. This is solved by Kdyby too. You can just configure an URL and Kdyby will rewrite the
Http\IRequest service and provide a fake instance with the URL configured.
console: url: https://pehapkari.cz/
Simplifying the execution
Executing the console through
Nette\Application\Application is no longer necessary since we have the
LinkGenerator. So how can we make this more elegant? Since we're integrating Symfony\Console, a logical approach is to look what Symfony thinks is the best practice.
Symfony has a
bin/console script with contents similar to the following snippet which is sufficient for Nette
#!/usr/bin/env php <?php declare(strict_types=1); /** @var \Nette\DI\Container $container */ $container = require __DIR__ . '/../app/bootstrap.php'; /** @var \Symfony\Component\Console\Application $consoleApplication */ $console = $container->getByType(Symfony\Component\Console\Application::class); exit($console->run());
that we can execute by typing
php bin/console help
And if we make it executable with
chmod +x bin/console
we can even drop the
php when executing it
This removes the extra layers of abstractions that are no longer needed thanks to the
LinkGenerator. But, having the
console.url option is still necessary to correctly generate URLs.
Utilizing decorator extension
With Nette 2.3 a new
DecoratorExtension was introduced that greatly simplifies command registration. We can use it, to find all services that have a given type and give them all a tag or some common setup calls.
With the extension this config
# app/config/config.neon services: - class: App\Console\FirstCommand tags: [kdyby.console.command] - class: App\Console\SecondCommand tags: [kdyby.console.command] - class: App\Console\ThirdCommand tags: [kdyby.console.command]
can be simplified to
# app/config/config.neon services: - App\Console\FirstCommand - App\Console\SecondCommand - App\Console\ThirdCommand decorator: Symfony\Component\Console\Command\Command: tags: [kdyby.console.command]
Nette extensions allow to search for a given type in compile-time, and therefore Kdyby\Console (and any other extension) could just work without tags, but tags are a predictable solution to marking services for special processing. If you want to just tag everything for processing, it's better if you do it explicitly yourself using the
Another added benefit of switching to
bin/console is automatic support of Symfony Console by
zsh. The default zsh Symfony Console integration is invoked when you execute
bin/console in your terminal and auto-completes the commands and options. Thanks Klára, for pointing this out!
Example of the refactoring
If you'd like to see a real-life code, Tomáš Votruba made PR on Github to Eventigo.cz recently.
This article was mainly about Kdyby\Console and its history, but to be fair, I have to mention Contributte\Console by Milan Felix Šulc that actually prompted the update in Kdyby\Console documentation.
Also, a side-note to whoever is using Kdyby\Doctrine - it is planned to make Kdyby\Console optional which will remove the vendor lock-in for Kdyby\Console, so you can choose what integration to use. This will allow you not to use Symfony\Console at all, or replace Kdyby\Console with Contributte\Console if you decide to do so.
There is always a better way to approach things. Please, share with us how you're using Symfony\Console in the comments.