I was working on a project for a client with a huge, old-school inventory system that had to sync with WooCommerce. The only way to get data out of it was a daily CSV dump. Total mess. We decided the only sane path was a standalone CLI tool, something we could drop on a cron job and let run. The Symfony console component is my go-to for this, but as the tool grew, so did the complexity.
My first pass at the commands was straightforward. Just instantiate whatever services I needed in the constructor. But by the time I added the third command for handling product variations, the dependency chain was getting ugly. I was passing the same half-dozen objects around everywhere. It was fragile and a pain to test. This is a classic sign you need proper Symfony console autowiring.
Wiring dependencies by hand gets painful fast. Change one constructor and you have to fix it in ten other places. The cleaner option is to let Symfony’s dependency injection container do that work for you.
Setting up Symfony console autowiring
Most tutorials on standalone Symfony console apps skip dependency injection, which is a mistake. The reason to pull in a framework component is to actually use what it offers. First, you need the right Composer packages.
{
"require": {
"symfony/config": "^5.4",
"symfony/console": "^5.4",
"symfony/dependency-injection": "^5.4",
"symfony/yaml": "^5.4"
}
}Next you need a configuration file that tells the container how to behave. I usually make a config directory with a services.yml file inside. In it, you tell the container to autowire everything by default and to tag all your console commands.
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/*'
App\Application:
public: true
arguments:
- !tagged console.command
_instanceof:
Symfony\Component\Console\Command\Command:
tags: ['console.command']The _instanceof section applies the console.command tag to any class that extends the base Symfony Command. Then you tell the main Application class to accept any service tagged with console.command as an argument. The container finds all of them and injects them for you, so you skip the manual $application->add(new MyCommand(...)) calls. This setup borrows heavily from a post on carlalexander.ca.
So, what’s the point?
Yeah, it’s a bit of boilerplate. You create the config file and the main executable once, and after that, adding a new command is trivial:
- Create a new command class that extends
Symfony\Component\Console\Command\Command. - Type-hint your dependencies in the constructor.
- The container automatically finds the class, injects its dependencies, and adds it to the application. Done.
This saves you a maintenance headache later. The code is cleaner and decoupled, and it’s far easier to test because you can mock dependencies freely. That is how you end up with a maintainable CLI tool rather than a throwaway script.
This stuff gets complicated fast. If you’re tired of debugging someone else’s mess and you just want your site to work, drop my team a line. We’ve probably seen it before.