I have been using symfony/console for making commands and registering them like that, everything works fine:
bin/console:
#!/usr/bin/env php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Commands\LocalitiesCommand;
use Symfony\Component\Console\Application;
$app = new Application();
$app->add(new LocalitiesCommand(new LocalitiesGenerator()));
$app->run();
src/Commands/LocalitiesCommand.php:
<?php
declare(strict_types=1);
namespace App\Commands;
use App\LocalitiesGenerator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
final class LocalitiesCommand extends Command
{
protected static $defaultName = 'app:generate-localities';
public function __construct(private LocalitiesGenerator $localitiesGenerator)
{
parent::__construct();
}
protected function configure(): void
{
$this
->setDescription('Generate localities.json file')
->setHelp('No arguments needed.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->localitiesGenerator->generateJsonLocalities();
$output->writeln("File localities.json generated!");
return Command::SUCCESS;
}
}
Now I want to autoinject the service with symfony/dependency-injection, I was reading the documentation and did some changes:
new bin/console:
#!/usr/bin/env php
<?php
require_once __DIR__ . '/../vendor/autoload.php';
use App\Commands\LocalitiesCommand;
use Symfony\Component\Console\Application;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use Symfony\Component\Config\FileLocator;
$container = new ContainerBuilder();
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/src/config'));
$loader->load('services.yaml');
$container->compile();
$app = new Application();
$app->add(new LocalitiesCommand());
$app->run();
config/services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
But still asks me to add my service in the constructor when I instantiate my command. Why is it not working?
First, let's clear up a misconception:
But still asks me to add my service in the constructor when I instantiate my command. Why is it not working?
If you call new Foo()
, then you no longer are getting autowired DI benefits. If you want to use autowire and automatic dependency injection, you need to let Symfony work for you. When you call new
, you are instantiating the object manually, and you need to take care of DI on your own.
With that out of the way, how would you get to do this?
First, composer.json
with the basic dependencies and autoloader declaration:
The full directory structure will end up being like this:
<project_dir>
├── composer.json
├── app
├── src/
│ ├── ConsoleCommand/
│ │ └── FooCommand.php
│ └── Text/
│ └── Reverser.php
├── config/
│ ├── services.yaml
Now, each of the parts:
The composer.json
file with all the dependencies and autoloader:
{
"require": {
"symfony/dependency-injection": "^5.3",
"symfony/console": "^5.3",
"symfony/config": "^5.3",
"symfony/yaml": "^5.3"
},
"autoload": {
"psr-4": {
"App\\": "src"
}
}
}
The front-controller script, the file running the application (app
, in my case):
#!/usr/bin/env php
<?php declare(strict_types=1);
use Symfony\Component;
require __DIR__ . '/vendor/autoload.php';
class App extends Component\Console\Application
{
public function __construct(iterable $commands)
{
$commands = $commands instanceof Traversable ? iterator_to_array($commands) : $commands;
foreach ($commands as $command) {
$this->add($command);
}
parent::__construct();
}
}
$container = new Component\DependencyInjection\ContainerBuilder();
$loader = new Component\DependencyInjection\Loader\YamlFileLoader($container, new Component\Config\FileLocator(__DIR__ . '/config'));
$loader->load('services.yaml');
$container->compile();
$app = $container->get(App::class);
$app->run();
The service container configuration for the project:
# config/services.yaml
services:
_defaults:
autowire: true
_instanceof:
Symfony\Component\Console\Command\Command:
tags: [ 'app.command' ]
App\:
resource: '../src/*'
App:
class: \App
public: true
arguments:
- !tagged_iterator app.command
One FooCommand
class:
<?php declare(strict_types=1);
// src/ConsoleCommand/FooCommand.php
namespace App\ConsoleCommand;
use App\Text\Reverser;
use Symfony\Component\Console;
class FooCommand extends Console\Command\Command
{
protected static $defaultName = 'foo';
public function __construct(private Reverser $reverser)
{
parent::__construct(self::$defaultName);
}
protected function execute(Console\Input\InputInterface $input, Console\Output\OutputInterface $output): int
{
$output->writeln('Foo was invoked');
$output->writeln($this->reverser->exec('the lazy fox'));
return self::SUCCESS;
}
}
The above depends on the App\Text\Reverser
service, which will be injected automatically for us by the DI component:
<?php declare(strict_types=1);
namespace App\Text;
class Reverser
{
public function exec(string $in): string
{
return \strrev($in);
}
}
After installing and dumping the autoloader, by executing php app
(1) I get that the foo
command is available (2):
I can execute php app foo
, and the command is executed correctly, using its injected dependencies:
A self-contained Symfony Console application, with minimal dependencies and automatic dependency injection.
(All the code for a very similar example, here).