Injecting parameter based service into other service - symfony

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

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.

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'))]]

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.

#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.

Related

symfony 3 tagging services for using in autowiring service

I tried to create an interface to create tagged services that can be injected into another service based on the documentation here https://symfony.com/doc/3.4/service_container/tags.html
I created an interface like
namespace AppBundle\Model;
interface PurgeInterface {
//put your code here
public function purge ();
}
put the definition into the service.yml:
_instanceof:
AppBundle\Model\PurgeInterface:
tags: ['app.purge']
and create services on this interface.
console debug:container shows my services as properly tagged.
I created another service which should work with the tagged services but this do not work.
services.yml:
purge_manager:
class: AppBundle\Service\PurgeManager
arguments: [!tagged app.purge]
The service looks like:
namespace AppBundle\Service;
use AppBundle\Model\PurgeInterface;
class PurgeManager {
public function __construct(iterable $purgers) {
dump($purgers);
}
}
If I test this I get:
Type error: Too few arguments to function AppBundle\Service\PurgeManager::__construct(), 0 passed in /.....Controller.php on line 21 and exactly 1 expected
I havenĀ“t tried to create a compiler pass because I just want to understand why this is not working as it should based on the documentation
Thanks in advance
Sebastian
You can use tags, manual service definition and _instanceof in config. It's one of Symfony ways, but it requires a lot of YAML coding. What are other options?
Use Autowired Array
I've answered it here, but you use case much shorter and I'd like to answer with your specific code.
The simplest approach is to autowire arguments by autowired array.
no tag
support PSR-4 autodiscovery
no coding outside the service
1 compiler pass
Example
namespace AppBundle\Service;
use AppBundle\Model\PurgeInterface;
class PurgeManager
{
/**
* #param PurgeInterface[] $purgers
*/
public function __construct(iterable $purgers) {
dump($purgers);
}
}
This is also called collector pattern.
How to Integrate
Read a post with an example about this here
Or use the Compiler pass
If there are some incompatible classes, exclude them in the constructor of compiler pass:
$this->addCompilerPass(new AutowireArrayParameterCompilerPass([
'Sonata\CoreBundle\Model\Adapter\AdapterInterface'
]);

monolog.logger.db service has been removed

I'm trying to refactor some Symfony 3 code to Symfony 4.
I am getting the following error when attempting to log:
The "monolog.logger.db" service or alias has been removed or inlined
when the container was compiled. You should either make it public, or
stop using the conta iner directly and use dependency injection
instead.
My logging code:
$logger = $container->get('monolog.logger.db');
$logger->info('Import command triggered');
Monolog config:
monolog:
channels: ['db']
handlers:
db:
channels: ['db']
type: service
id: app.monolog.db_handler
app.monolog.db_handler config (Note, I tried public: true here and it had no affect:
app.monolog.db_handler:
class: App\Util\MonologDBHandler
arguments: ['#doctrine.orm.entity_manager']
How can I get this wired up correctly in Symfony 4?
By default all services in Symfony 4 are private (and is the recommended pratice) so you need to "inject" in each Controller each needed service (personally I use a custom CommonControllerServiceClass).
You can also create a public service "alias" to continue accessing the service as you did, but it's not the best pratice to follow (also because I guess you will have many other services to fix).
mylogger.db:
alias: monolog.logger.db
public: true
then you can get the service from the container:
$logger = $container->get('mylogger.db');
Alister's answer is a good start, but you can utilise service arguments binding instead of creating a new service for each logger:
services:
_defaults:
autowire: true
bind:
$databaseLogger: '#monolog.logger.db'
Then just change the argument name in your class:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $databaseLogger = null) {...}
It appears that App\Util\MonologDBHandler may be the only thing that is actively using monolog.logger.db - via a container->get('...') call. (If not, you will want to use this technique to tag the specific sort of logger into more services).
You would be better to allow the framework to build the app.monolog.db_handler service itself, and use the container to help to build it. Normally, to inject a logger service, you will just need to type-hint it:
// in App\Util\MonologDBHandler.php
use Psr\Log\LoggerInterface;
public function __construct(LoggerInterface $logger = null) {...}
However, that will, by default, setup with the default #logger, so you need to add an extra hint in the service definition of the handler that you want a different type of logger:
services:
App\Log\CustomLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: db }
Now, the logger in CustomLogger should be what you had previously known as monolog.logger.db.
You can also alias a different interface (similar to how the LoggerInterface is aliased to inject '#logger') to the allow for the tagging.

Symfony: How to use configuration parameters in a service class?

I want to use the parameters in parameters.yml in my service class mailer
but I got this error while instantiate the mailer class:
$mailer = new Mailer();
knowing that the parameters are defined in parameters.yml:
Warning: Missing argument 1 for AppBundle\Service\Mailer::__construct(), called in
namespace AppBundle\Service
class Mailer
{
private $mailer_user;
private $mailer_password;
private $mailer_name;
private $mailer_host;
public function __construct($mailer_user, $mailer_password ,$mailer_name ,$mailer_host)
{
$this->mailer_name = $mailer_user;
$this->mailer_password = $mailer_password;
$this->mailer_user = $mailer_name;
$this->mailer_host = $mailer_host;
}
//.....
}
services.yml:
mailer:
class: 'AppBundle\Service\Mailer'
arguments: [%mailer_user%, %mailer_password% ,%mailer_name% ,%mailer_host%]
To learn "how can I use the parameters in parameters.yml in my service class" (and to see how a service can be used in a Symfony app) just read the Symfony docs paying attention to the Symfony version of the documentation you are reading:
Introduction to Parameters
How to Set external Parameters in the Service Container
Service Container
BTW you should never instanciate a service class directly like you did:
$mailer = new Mailer();
but retrieve the service instance from the Service Container (Symfony will take care to automatically inject all the configured dependancies) like we usually do in a Controller (the example below access the Service using the shortcode provided extending the base Controller provided by the FrameworkBundle included in the Symfony standard version):
$mailer = $this->container->get('mailer');

Symfony not respecting overriding class argument for test environment

I have a bundle with a services.yml where the service definition uses a parameter contained within the same file for the class parameter, like so:
parameters:
application.servicename.class: Application\Service\ServiceName
services:
application.servicename:
class: %application.servicename.class%
Now I want to override the service class for my test environment. However, overriding the parameter in config_test.yml does not result in an object of the overriding class being instantiated.
Adding the following to config_test.yml:
parameters:
application.servicename.class: Application\Mock\Service\ServiceName
...still causes the service to be instantieted from Application\Service\ServiceName. If I try passing application.servicename.class as an argument to the service and dump it in the constructor, the overriden value of Application\Mock\Service\ServiceName is displayed.
Why is Symfony not respecting the overridden value for the service class when preparing the service?
You should move
parameters:
application.servicename.class: Application\Service\ServiceName
From services.yml to config.yml becasuse in my opninion you are overriding the value of the paremeter in config_test.yml with the value you have in services.yml
I think what you're looking for is a Extension class in your Bundle:
http://symfony.com/doc/current/cookbook/bundles/extension.html
I think you might be able to change priorities loading the config files
Here's an example of implementation
public function load(array $configs, ContainerBuilder $container {
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('your.config.parameter', $config['your']['config']['parameter']);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config'));
$loader->load('services.yml');
}
As it turns out, this problem was not related to Symfony loading configuration but rather an assumption that the incorrect class was loaded. This assumption was caused by the fact that the method calls of both the original service (and the mock extending it) was marked as private.
Had this not been a problem, I belive what I was attempting to do should be possible, ref http://symfony.com/doc/2.8/cookbook/bundles/override.html#services-configuration
Sorry to waste your time.

How to retrieve service definition in controller when service was defined in yaml config?

I'm playing with Symfony2 DependencyInjection. I defined service in yaml and want to $container->getDefinition($serviceId) in controller but got InvalidArgumentException. Is it possible to retrieve service definition in controller when service was defined in yaml config?
//services.yml
services:
patriots:
class: CwBundle\Utils\PatriotsClass
calls:
- [setBrady, ['%brady%']]
broncos:
class: CwBundle\Utils\BroncosClass
arguments: [#patriots]
//controller
$container = new ContainerBuilder() ;
$a = $container->hasDefinition('patriots');
$b = $container->findDefinition('broncos');
$c = $container->getMethodCalls('patriots');
//print_r $a,$b,$c
The service definition "broncos" does not exist.
I have a feeling that this entire symfony.com/doc/components/dependency_injection section is for those who define services in PHP, not yaml.
edit:
the reason of the confusion is that Chapter:"Working with Container Service Definitions" is before Chapter "Compiling the Container" in Symfony DI Documentation.
Yes you can manipulate your service no matter you use yaml, php or xml.
In your exemple you create a fresh new ContainerBuilder so it's empty and obviously you can't retreive a service from this new instance.
From your controller you'll have the container, it's already compiled and you can't alter services.
You can only alter services before container is build. It's in your Extension file inside DependencyInjection folder or when using CompilerPass. at this time you will have access to ContainerBuilder.
To get a service in controller you call
$this->container->get('name_of_service')
In your controller you don't need to instantiate your own container but you need to use the application container.
So try this:
$container = $this->container;
instead of this;
$container = new ContainerBuilder() ;
Hope this help

Resources