5

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?

yivi
  • 42,438
  • 18
  • 116
  • 138
Raul Perez Tosa
  • 119
  • 1
  • 6
  • are you using composer ? – Rudy David Aug 12 '21 at 09:51
  • @RudyDavid yes i'm using composer – Raul Perez Tosa Aug 12 '21 at 09:53
  • @yivi It is possible I saw some tutorials and also symfony allows inject services in to commands, you can check it here: https://symfony.com/doc/current/console.html#getting-services-from-the-service-container – Raul Perez Tosa Aug 12 '21 at 09:53
  • 1
    @yivi Aah now I understand what you mean sorry, anything I can read to achieve what I want or any documentation you know? – Raul Perez Tosa Aug 12 '21 at 10:08
  • Changing $app->add(new LocalitiesCommand()); to $app->add($container->get(LocalitiesCommand::class); and making your services public might do the trick. But quite honestly once you start using the container in these sorts of things then just using the symfony/skeleton app makes more sense. I am also assuming you only showed part of your services.yaml file. You obviously need to actually scan the directories or at least add your command as a service. – Cerad Aug 12 '21 at 13:17

1 Answers1

8

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): enter image description here

I can execute php app foo, and the command is executed correctly, using its injected dependencies:

enter image description here

A self-contained Symfony Console application, with minimal dependencies and automatic dependency injection.

(All the code for a very similar example, here).

yivi
  • 42,438
  • 18
  • 116
  • 138
  • 1
    I have built a similar solution to that one, only difference I left the `services.yaml` only for App related services configuration. Also includes DotEnv. I just pushed it to github and it's available at packagist https://packagist.org/packages/coral-media/crune – rernesto Feb 08 '22 at 07:02
  • Got the idea from https://carlalexander.ca/symfony-console-application-autowire/ – rernesto Feb 08 '22 at 07:09