Luke
Luke

Reputation: 21236

Injecting parameter based service into other service

I have a service which takes a driver to do the actual work. The driver itself is within the context of Symfony 2 is just another service.

To illustrate a simplified version:

services:
  # The driver services.
  my_scope.mailer_driver_smtp:
    class: \My\Scope\Service\Driver\SmtpDriver

  my_scope.mailer_driver_mock:
    class: \My\Scope\Service\Driver\MockDriver

  # The actual service.
  my_scope.mailer:
    class: \My\Scope\Service\Mailer
    calls:
      - [setDriver, [@my_scope.mailer_driver_smtp]]

As the above illustrates, I can inject any of the two driver services into the Mailer service. The problem is of course that the driver service being injected is hard coded. So, I want to parameterize the @my_scope.mailer_driver_smtp.

I do this by adding an entry to my parameters.yml

my_scope_mailer_driver: my_scope.mailer_driver_smtp

I can then use this in my config.yml and assign the parameter to the semantic exposed configuration [1]:

my_scope:
  mailer:
    driver: %my_scope_mailer_driver%

In the end, in the Configuration class of my bundle I set a parameter onto the container:

$container->setParameter('my_scope.mailer.driver', $config['mailer']['driver'] );

The value for the container parameter my_scope.mailer.driver now equals the my_scope.mailer_driver_smtp that I set in the parameters.yml, which is, as my understanding of it is correct, just a string.

If I now use the parameter name from the container I get an error complaining that there is no such service. E.g:

services:
  my_scope.mailer:
    class: \My\Scope\Service\Mailer
    calls:
      - [setDriver, [@my_scope.mailer.driver]]

The above will result in an error:

[Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException]                                          
The service "my_scope.mailer" has a dependency on a non-existent service "my_scope.mailer.driver"

The question now is, what is the correct syntax to inject this container parameter based service?

[1] http://symfony.com/doc/current/cookbook/bundles/extension.html

Upvotes: 5

Views: 3136

Answers (4)

Alexei Tenitski
Alexei Tenitski

Reputation: 9360

With service container expression language you have access to the following two functions in config files:

  • service - returns a given service (see the example below);
  • parameter - returns a specific parameter value (syntax is just like service)

So to convert parameter name into a service reference you need something like this:

parameters:
  my_scope_mailer_driver: my_scope.mailer_driver_smtp

services:
  my_scope.mailer:
    class: \My\Scope\Service\Mailer
    calls:
      - [setDriver, [@=service(parameter('my_scope_mailer_driver'))]]

Upvotes: 5

Touki
Touki

Reputation: 7525

This question has a similar answer here

I think the best way to use this kind of definition is to use service aliasing.
This may look like this

Acme\FooBundle\DependencyInjection\AcmeFooExtension

public function load(array $configs, ContainerBuilder $container)
{
    $configuration = new Configuration;
    $config = $this->processConfiguration($configuration, $configs);

    $loader = new Loader\YamlFileLoader(
        $container,
        new FileLocator(__DIR__.'/../Resources/config')
    );
    $loader->load('services.yml');

    $alias = $config['mailer']['driver'];
    $container->setAlias('my_scope.mailer_driver', $alias);
}

This will alias the service you've defined in my_scope.mailer.driver with my_scope.mailer_driver, which you can use as any other service

services.yml

services:
    my_scope.mailer_driver:
        alias: my_scope.mailer_driver_smtp # Fallback

    my_scope.mailer_driver_smtp:
        class: My\Scope\Driver\Smtp

    my_scope.mailer_driver_mock:
        class: My\Scope\Driver\Mock

    my_scope.mailer:
        class: My\Scope\Mailer
        arguments:
            - @my_scope.mailer_driver

With such a design, the service will change whenever you change the my_scope.mailer_driver parameter in your config.yml.
Note that the extension will throw an exception if the service doesn't exist.

Upvotes: 5

Cerad
Cerad

Reputation: 48865

At first I thought this was just a question of getting the @ symbol passed in properly. But I tried assorted combinations and came to the conclusion that you can't pass an actual service as a parameter. Maybe someone else will chime in and show how to do this.

So then I figured is was just a question of using the service definition and passing it a reference. At first I tried this in the usual extension but the container does not yet contain all the service definitions.

So I used a compiler pass: http://symfony.com/doc/current/cookbook/service_container/compiler_passes.html

The Pass class looks like:

namespace Cerad\Bundle\AppCeradBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class Pass1 implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
        // Set in the Extension: my_scope.mailer_driver_smtp
        $mailerDriverId = $container->getParameter('my_scope.mailer.driver');

        $def = $container->getDefinition('my_scope.mailer');

        $def->addMethodCall('setDriver', array(new Reference($mailerDriverId)));
    }
}

Take the calls section out of the service file and it should work. I suspect there is an easier way but maybe not.

Upvotes: 3

Michael Sivolobov
Michael Sivolobov

Reputation: 13240

@my_scope.mailer.driver needs to be a service but not defined as service. To retrieve string parameter named as my_scope.mailer.driver you need to wrap it with %: %my_scope.mailer.driver%.

So you need to pass @%my_scope.mailer.driver% as parameter to a service. Yml parser will replace %my_scope.mailer.driver% with the appropriate value of the parameter and only then it will be called as a service.

Upvotes: 0

Related Questions