When Symfony Http Kernel is a Too Big Hammer to Use

I've been a big fan of Symfony components for ages. I use them as core bricks of my projects migrate other frameworks to it, and every 6 months, I'm excited about what new features are coming in the next minor release.

But, one tough spot has been bothering me for the last 4 years. I tried to find my way out of it, hack around it or accept it. In March 2021, we downgrade Rector 0.10 from PHP 8 to 7.1, and the issue became visible more than ever.

I knew there was a time for a change.

Http Kernel and Console Applications

The command-line application is anything we run from CLI - PHPUnit, PHPStan, Rector, Composer, or ECS. At the start of building such an application, you stand in front of an important decision:

use Symfony\Component\Console\Application;

$application = new Application();
$fileAnalyzer = new FileAnalyzer();
$application->addCommand(new ProcessCommand($fileAnalyzer));
use Symfony\Component\Console\Application;

$application = $container->get(Application::class);

I grew up on DI containers, so I'm spoiled by the automatic injection and service management this pattern handle for me. That's why I picked symfony/http-kernel to build console application.

What about symfony/dependency-injection?

The name seems quite fitting, right? We could provide a single config file, and that's it. Unfortunately, the name is a bit misleading. It does not handle compiler passes, extensions, service autodiscovery, container cache, and container build. You'll find most of the building bricks there, but the glue is missing.

There is no "container factory" class in symfony/dependency-injection, that would handle even the simplest use case:

use Symfony\Component\DependencyInjection\ContainerFactory;
use Symfony\Component\Console\Application;

$containerFactory = new ContainerFactory();
$container = $containerFactory->createFromConfigs([__DIR__ . '/config/config.php']);

$application = $container->get(Application::class);

On the other hand, there is an "optional dependency" on symfony/http-kernel package. Some Symfony components are decoupled well, but some are mutually dependent even if you don't need them:

And we're forced to use Kernel with Http.

Btw, both articles by Matthias Noback and Paul M. Jones are not just critics of tight coupling, but great specific tips about how to create a future proof and solid architecture design. If you haven't seen them, make sure you grasp the main ideas, and they will serve you in the future.

There is no "Http" in CLI

The command-line application run in a command line. There is no HTTP request, no browser, no routes, no session, no-cache.

Instead of URL with parameters, we run command-line arguments and options:

composer require symfony/http-kernel --dev

The 4 Components you don't Need, but Must Have

If there few more "http" classes we never use, we can live with that.

But what do we get when we actually run the composer command above?

There are some packages related to http that's expected. But why is there debugging package in our production tool?

So when we require symfony/http-kernel, we also get 4 more packages we don't use:

Maybe we could say:

That's "optional dependency" in case of
"dev" environment to report bugs.

That's completely ok for request and response http workflow, but one more package we'll never use, but have to maintain.

Now we understand why most CLI app developers decided not to use any container but create their services manually.

The Downgrade Covariance of Symfony Config

You might think:

What do you mean by 'maintain'? It's few dependencies
that we'll never use and just take little space on a disk.
What's the big deal?

Let's get back to the start. In April 2021, we started to develop Rector on PHP 8 and release PHP 7.1 downgraded version. Downgraded and scoped version means fully downgraded and scoped /vendor. Yes, including all Symfony components we use.

The downgrade PHP market is still quite a niche, but the community is interested more and more in this field. In December 2021 alone, there have been over 20 brand new downgrade rules contributed from Rector users.

Try to Downgrade Invalid Code with Invalid Types

When we started downgrading Rector, we often came to incompatible types in FileLoader classes. The covariant parameters added in PHP 7.4 started to cause problems on PHP 7.3 and bellow.

The most problematic was a downgrade of import() methods.

In config FileLoader.php they require bool parameter:

public function import($resource, $type = null, bool $ignoreErrors = false) {}

But in a dependency injection FileLoader.php that inherits former class, it is string|bool:

public function import($resource, $type = null, bool|string $ignoreErrors = false) {}

We worked on a downgrade fix for 2 months before figuring our rules were correct. The problem is in contracts of Symfony method, which is invalid on PHP 7.3 and below.

Accidental Complexity?

The problem is not about invalid contracts, and anyone could make a mistake in those. The problem is in maintaining code we don't need nor use.

Need for Really Decoupled Container Factory

I wish there were a more straightforward container factory service that we could add to the project:


* symfony/dependency-container-factory
* symfony/dependency-injection

That would allow us only to create the container. With no extra dependencies, with a focus on service tree construction:

use Symfony\Component\DependencyInjection\ContainerFactory;
use Symfony\Component\Console\Application;

$compilerPasses = [...];
$extensions = [...];

$containerFactory = new ContainerFactory($compilerPasses, $extensions);
$container = $containerFactory->createFromConfigs([__DIR__ . '/config/config.php']);

$application = $container->get(Application::class);

It would make CLI apps much cleaner and easier to use Symfony in them.

What do you think? What is your approach to creating service in your CLI applications?

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!