42
votes

I'm writing an open source application uses some Symfony components, and using Symfony Console component for interacting with shell.

But, i need to inject dependencies (used in all commands) something like Logger, Config object, Yaml parsers.. I solved this problem with extending Symfony\Component\Console\Command\Command class. But this makes unit testing harder and not looks correct way.

How can i solve this ?

7
are you using the dependency injection component as well? you need someone to manager the dependecy injectionP. R. Ribeiro
no, im not using dependency injection component.osm
Easy unit testing your apps requires that you use some sort of dependency injection. If you're using symfony components grab the DI component as well.P. R. Ribeiro

7 Answers

40
votes

Since Symfony 4.2 the ContainerAwareCommand is deprecated. Use the DI instead.

namespace App\Command;

use Symfony\Component\Console\Command\Command;
use Doctrine\ORM\EntityManagerInterface;

final class YourCommand extends Command
{
    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    public function __construct(EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;

        parent::__construct();
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        // YOUR CODE
        $this->entityManager->persist($object1);    
    }
}
18
votes
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;

Extends your Command class from ContainerAwareCommand and get the service with $this->getContainer()->get('my_service_id');

18
votes

It is best not to inject the container itself but to inject services from the container into your object. If you're using Symfony2's container, then you can do something like this:

MyBundle/Resources/config/services (or wherever you decide to put this file):

...
    <services>
        <service id="mybundle.command.somecommand" class="MyBundle\Command\SomeCommand">
        <call method="setSomeService">
             <argument type="service" id="some_service_id" />
        </call>
        </service>
    </services>
...

Then your command class should look like this:

<?php
namespace MyBundle\Command;

use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use The\Class\Of\The\Service\I\Wanted\Injected;

class SomeCommand extends Command
{
   protected $someService;
   public function setSomeService(Injected $someService)
   {
       $this->someService = $someService;
   }
...

I know you said you're not using the dependency injection container, but in order to implement the above answer from @ramon, you have to use it. At least this way your command can be properly unit tested.

3
votes

You can use ContainerCommandLoader in order to provide a PSR-11 container as follow:

require 'vendor/autoload.php';

$application = new Application('my-app', '1.0');

$container = require 'config/container.php';

// Lazy load command with container
$commandLoader = new ContainerCommandLoader($container, [
    'app:change-mode' => ChangeMode::class,
    'app:generate-logs' => GenerateLogos::class,
]);

$application->setCommandLoader($commandLoader);

$application->run();

ChangeMode class could be defined as follow:

class ChangeMode extends Command
{

    protected static $defaultName = 'app:change-mode';

    protected $container;

    public function __construct(ContainerInterface $container)
    {
        $this->container = $container;
        parent::__construct(static::$defaultName);
    }
...

NB.: ChangeMode should be provided in the Container configuration.

1
votes

I'm speaking for symfony2.8. You cannot add a constructor to the class that extends the ContainerAwareCommand because the extended class has a $this->getContainer() which got you covered in getting your services instead of injecting them via the constructor.

You can do $this->getContainer()->get('service-name');

1
votes

Go to services.yaml

Add This to the file(I used 2 existing services as an example):

App\Command\MyCommand:
        arguments: [
            '@request_stack',
            '@doctrine.orm.entity_manager'
        ]

To see a list of all services type in terminal at the root project folder:

php bin/console debug:autowiring --all

You will get a long list of services you can use, an example of one line would look like this:

 Stores CSRF tokens.
 Symfony\Component\Security\Csrf\TokenStorage\TokenStorageInterface (security.csrf.token_storage)

So if CSRF token services is what you are looking for(for example) you will use as a service the part in the parenthesis: (security.csrf.token_storage)

So your services.yaml will look somewhat like this:

parameters:

services:
    _defaults:
        autowire: true      
        autoconfigure: true 

# Here might be some other services...

App\Command\MyCommand:
        arguments: [
            '@security.csrf.token_storage'
        ]

Then in your command class use the service in the constructor:

class MyCommand extends Command
{
    private $csrfToken;

    public function __construct(CsrfToken $csrfToken)
    {
        parent::__construct();
        $this->csrfToken = $csrfToken;
    }
}
0
votes

In Symfony 3.4, if autowire is configured correctly, services can be injected into the constructor of the command.

public function __construct(
    \AppBundle\Handler\Service\AwsS3Handler $s3Handler
) {
    parent::__construct();

    $this->s3Handler = $s3Handler;
}