Why I Removed The Service Container From Console Applications
11 hours ago
The service container gives us a lot of features, such as Dependency Injection (DI), Service Definition & Management, Autowiring, Autoconfiguration, Service Tags, Lazy Loading, and PSR-11 Compatibility.

Very nice, super cool. But do we really need all of this? More often than not, the answer is simply: we don’t.

Let’s look at an example of a console application with the service container. We need a service, so we inject it via the constructor or as a method parameter. To be honest, I got so used to this approach that I didn’t even think about it—just inject, inject, and inject. (Don’t do drugs, kids.)

Then you go to test your methods. Ah, now you need to mock many of the services you’ve just injected. This makes your tests more complicated, and mocking has its own labyrinthine set of downsides if misused.

Basically, we can’t instantiate the object directly in the tests, and it creates a bloated application.

Now let’s look at an example without the service container. First of all, to create a console app, we really need just two files.

console.php:
declare(strict_types=1);
use Symfony\Component\Console\Application;
require __DIR__ . '/../vendor/autoload.php';

$application = new Application();
$application->add(new SomeCommand(project: new SomeObject()));

try {   
    $application->run();
} catch (Exception) {}



Console bin file:
#!/usr/bin/env php
__DIR__ . '/console.php';
At this point, you might think, “Well, that sucks. We have to inject any object we create manually.”

Actually, that’s the whole point. By taking this approach, we force ourselves to write more testable, more efficient, and more minimalist code.

If I want to inject something, I really need to think about whether it’s necessary and, if so, how I can write it as minimally as possible.

Let’s have a look at a test now:
public function testCalculateTotalScore(): void {   
    $project = new Project();   
    $upgrade = new Upgrade();   
    $project->setUpgrade($upgrade);   
    $upgrade->setFrameworkVersionUpgradabilityScore(80);   
    $upgrade->setDependenciesUpgradabilityScore(60);   
    $upgrade->setPhpVersionUpgradabilityScore(90);   
    $upgrade->setCodebaseSizeUpgradabilityScore(70);

    $upgradeCalculator = new UpgradeCalculator();   
    $result = $upgradeCalculator->calculateTotalScore(project: $project);   
    $expectedScore = round(63.5, 2);

    $this->assertEquals($expectedScore, $result);
}
Woah! No mocking! We can instantiate the objects directly.

Wait, you mean to say our test is actually testing the actual thing it's supposed to be testing? Oh, the audacity!

You might say, “Ah, well, what about scalability?” This is a contextual question and depends on the use case, but not every project needs to scale—and more importantly, to what scale?

Okay, but what about breaking the Single Responsibility Principle? That’s a good point and not to be overlooked. If you take this approach, you may be forced to break the principle purely to have functionality available when you need it. However, I would argue that the benefits far outweigh this rule-breaking.

For example, here the UpgradeCalculator is handling not only the calculation but also updating the state of the process. To facilitate this, I have to pass the SymfonyStyle object to update the progress bar.
(new UpgradeCalculator())->calculate($this->project, $io);
Yes, it’s breaking the rules, but let’s look at the alternative. I would have to:

  1. Create an event
  2. Dispatch the event
  3. Create a subscriber to handle the event
  4. Have the subscriber update the progress bar
This would require the EventDispatcher component. All that added complexity—rather than just passing it in as a method parameter? I think not.

The service container is fantastic and has many use cases. But I think now, more than ever, we need to be thinking about how to slim down our applications and get rid of absolutely everything we can. If we can create tested, high-quality, minimalist, yet still complex applications that require less maintenance, why wouldn’t we?