Skip to content

[Console] Command as service #3621

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Mar 24, 2014
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 117 additions & 0 deletions components/console/commands_as_services.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.. index::
single: Console; Commands as Services

How to Define Commands as Services
==================================

.. versionadded:: 2.4
Support for registering commands in the service container was introduced in
version 2.4.

By default, Symfony will take a look in the ``Command`` directory of your
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... of each bundle ...

bundles and automatically register your commands. For the ones implementing
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to be careful because we're talking about the ContainerAwareCommand, not the actual ContainerAwareInterface.

If a command extends the ContainerAwareCommand, Symfony...

the ``ContainerAwareCommand`` interface, Symfony will even inject the container.
While making life easier, this default implementation has some drawbacks in some
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would suggest adding this to the previous paragraph

situations:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... this has some limitations ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And I rephrased each bullet point below so that they all sound like limitations (not things that you will be able to do after registering as as service), since we mention limitations/drawbacks here.


* Define the command elsewhere than in the ``Command`` directory;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* Your command must live in the ``Command`` directory.

* Conditionally register your command, depending on the current environment or
on the availability of some dependencies;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* There's no way to conditionally register your service based on the environment
  or availability of some dependencies;

* Access dependencies before the ``setContainer()`` is called (for example in
the ``configure()`` method);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

* You can't access the container in the ``configure()`` method (because ``setContainer``
  hasn't been called yet);

* Reuse a command many times, but with different dependencies or parameters
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • You can't use the same class to create many commands (i.e. each with different configuration).


To solve those problems, you can register your command as a service by simply
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... solve these ...

... as a service and tag it with console.command:

defining it with the ``console.command`` tag:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In these cases we tag ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-1 never use the first person


.. configuration-block::

.. code-block:: yaml

# app/config/config.yml
services:
acme_hello.command.my_command:
class: Acme\HelloBundle\Command\MyCommand
tags:
- { name: console.command }

.. code-block:: xml

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<services>
<service id="acme_hello.command.my_command"
class="Acme\HelloBundle\Command\MyCommand">
<tag name="console.command" />
</service>
</services>
</container>

.. code-block:: php

// app/config/config.php
$container
->register('acme_hello.command.my_command', 'Acme\HelloBundle\Command\MyCommand')
->addTag('console.command')
;

Using Dependencies and Parameters to Set Default Values for Options
-------------------------------------------------------------------

Imagine you want to provide a default value for the ``name``option. You could
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing a space between "name" and "option"

pass one of the following as the 5th argument of ``addOption()``:

* an hardcoded string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • a hardcoded string;

* a value coming from the configuration (allows the user to change it easily);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear that we're really talking about a dependency injection parameter, so I propose:

  • a container parameter (e.g. something from parameters.yml);

* a value computed by a service (e.g. a repository).

With a ``ContainerAwareCommand`` you wouldn't be able to retrieve the
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By extending ContainerAwareCommand, only the first is possible, because you can't
access the container inside the configure() method. Instead, inject any parameter or
service you need into the constructor. For example, suppose you have some NameRepository service that you'll use to get your default value:

configuration parameter, because the ``configure()`` method is called in the
constructor. The only solution is to inject them::

// src/Acme/DemoBundle/Command/GreetCommand.php
namespace Acme\DemoBundle\Command;

use Acme\DemoBundle\Entity\NameRepository;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;

class GreetCommand extends Command
{
protected $nameRepository;

public function __construct(NameRepository $nameRepository)
{
$this->nameRepository = $nameRepository;
}

protected function configure()
{
$defaultName = $this->nameRepository->findLastOne();

$this
->setName('demo:greet')
->setDescription('Greet someone')
->addOption('name', '-n', InputOption::VALUE_REQUIRED, 'Who do you want to greet?', $defaultName)
;
}

protected function execute(InputInterface $input, OutputInterface $output)
{
$name = $input->getOption('name');

$output->writeln($name);
}
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, just update the arguments of your service configuration like normal to inject the NameRepository. Great, you now have a dynamic default value!

.. caution::

When running the console, every command is instantiated, which means every
``configure()`` method is called. Be careful with database queries, as
they could impact performance.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful not to actually do any work in configure (e.g. make database queries), as your code will be run, even if you're using the console to execute a different command.

1 change: 1 addition & 0 deletions components/console/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Console

introduction
usage
commands_as_services
single_command_tool
events
helpers/index
1 change: 1 addition & 0 deletions components/console/introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -527,6 +527,7 @@ Learn More!
* :doc:`/components/console/usage`
* :doc:`/components/console/single_command_tool`
* :doc:`/components/console/events`
* :doc:`/components/console/commands_as_services`

.. _Packagist: https://packagist.org/packages/symfony/console
.. _ANSICON: https://github.com/adoxa/ansicon/releases
53 changes: 0 additions & 53 deletions cookbook/console/console_command.rst
Original file line number Diff line number Diff line change
Expand Up @@ -62,59 +62,6 @@ This command will now automatically be available to run:

$ app/console demo:greet Fabien

.. _cookbook-console-dic:

Register Commands in the Service Container
------------------------------------------

.. versionadded:: 2.4
Support for registering commands in the service container was added in
version 2.4.

Instead of putting your command in the ``Command`` directory and having Symfony
auto-discover it for you, you can register commands in the service container
using the ``console.command`` tag:

.. configuration-block::

.. code-block:: yaml

# app/config/config.yml
services:
acme_hello.command.my_command:
class: Acme\HelloBundle\Command\MyCommand
tags:
- { name: console.command }

.. code-block:: xml

<!-- app/config/config.xml -->
<?xml version="1.0" encoding="UTF-8" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

<service id="acme_hello.command.my_command"
class="Acme\HelloBundle\Command\MyCommand">
<tag name="console.command" />
</service>
</container>

.. code-block:: php

// app/config/config.php

$container
->register('acme_hello.command.my_command', 'Acme\HelloBundle\Command\MyCommand')
->addTag('console.command')
;

.. tip::

Registering your command as a service gives you more control over its
location and the services that are injected into it. But, there are no
functional advantages, so you don't need to register your command as a service.

Getting Services from the Service Container
-------------------------------------------

Expand Down