I saw 2 links to Symfony\Console in today's Week of Symfony (what a time reference, huh?). There are plenty of such posts out there, even in Pehapkari community blog: Best Practice for Symfony Console in Nette or Symfony Console from the Scratch.
But nobody seems to write about the greatest bottleneck of Console applications - static cancer. Why is that?
Your web application has an entry point in www/index.php
, where it loads the DI Container, gets Application
class and calls run()
on it (with explicit or implicit Request
):
require __DIR__ . '/vendor/autoload.php';
// Kernel or Configurator
$container = $kernel->getContainer();
$application = $container->get(Application::class);
$application->run(Request::createFromGlobals());
Console Applications (further as CLI Apps) have very similar entry point. Not in index.php
, but usually in bin/something
file.
When we look at entry points of popular PHP Console Applications, like:
$runner = new PHP_CodeSniffer\Runner();
$runner->runPHPCS();
$application = new PhpCsFixer\Console\Application();
$application->run();
$application = new Symfony\Component\Console\Application('PHPStan');
$application->add(new AnalyseCommand());
$application->run();
If we mimic such approach in web apps, how would our www/index.php
look like?
require __DIR__ . '/vendor/autoload.php';
$application = new Application;
$application->addController(new HomepageController);
$application->addController(new PostController);
$application->addController(new ContactController);
$application->addController(new ProducController);
// ...
$application->run();
How do you feel seeing such code? I feel a bit weird and I don't get on well with static code.
On the other hand, if we take the web app approach to cli apps:
$container = $kernel->getContainer();
$application = $container->get(Application::class);
$application->run(new ArgInput);
I wish I knew this answer :). In my opinion and experience with building cli apps, there might be few...
CLI apps almost always start with simple plain PHP code:
# bin/turn-tabs-to-spaces.php
$input = $argv[1];
// 1st PSR-2 rule: replace tabs with spaces
return str_replace('\t', ' ', $input);
No container, no dependency injection, sometimes not even dependencies. Just see the PHP-CS-Fixer v0.00001.
When the proof of concepts works, the application grows.
It's easy, quick and simple.
Who would use container right from the start of 1 command, right?
new
static, it's difficult to migrate.The container is slowly appearing not as the backbone of application as in web apps, but as part of commands.
E.g. AnalyseCommand
in PHPStan:
use Symfony\Component\Console\Command\Command;
class AnalyseCommand extends Command
{
// ...
protected function execute(InputInterface $input, OutputInterface $output): int
{
$container = $this->containerFactory->createFromConfig($input->getOption('config'));
$someService = $container->get(SomeService::class);
// ...
}
}
Or in FixerFactory
in PHP CS Fixer:
# much simplified
class FixerFactory
{
public function registerBuiltInFixers()
{
static $fixers = [];
foreach (Finder::findAllFixerClasses() as $fixerClass) {
$fixers[] = new $fixerClass;
}
}
}
Imagine a code like this in your web application:
class ProductController
{
/**
* @var Connection
*/
private $connection;
public function __construct(Connection $connection)
{
$this->connection = $connectoin;
}
public function detail($id);
{
$productRepository = new ProductRepository($this->connection);
$product = $productRepository->get($id);
// ...
}
}
How do you feel about it?
CLI apps authors often struggle with the question: When should be the container created?
Command
?Command
scope?Command
s?Command
?And how to create container when user provides config with services via --config
option?
The complexity of this question usually leads to choice 2 or 1.
I won't get into more details now, since I'll write about possible solutions in following posts.
This application cycle has these steps:
Application
with new
$application->add(new SomeCommand)
Application
new
Compare it to a web application:
www/index.php
fileApplication
from itWhy not inspire by web apps, where Controllers are lazy and dependency injection is the first-class citizen? Moreover, Symfony 3.4 allows Lazy Commands, that make application cycle more and more similar to web apps. Be careful - there are few WTFs during migration to Lazy Commands, as Shopsys describes.
# bin/rector
// ...
$container = $kernel->getContainer();
$application = $container->get(Application::class);
$application->run();
new
service approach, if you're used to it.static::$vars
inside classes etc.I wish there was Rector for that like there is for Doctrine Repositories as Services, but it is a too complex task at the moment. Maybe one day.
In the meantime you can use few guides:
ForbiddenStaticFunctionSniff
NoClassInstantiationSniff
That's what works for me in CLI apps I've been working on. Look for yourself to get real code inspiration:
Which approach do you find the best in your own practice for the long-term code?
Happy injecting!
Do you learn from my contents or use open-source packages like Rector every day?
Consider supporting it on GitHub Sponsors.
I'd really appreciate it!