From my controllers, I access the application parameters (those in /app/config) with
$this->container->getParameter('my_param')
But I don't know how to access it from a service (I imagine my service class is not supposed to extend Symfony\Bundle\FrameworkBundle\Controller\Controller).
Should I map needed parameters into my service registration like this:
#src/Me/MyBundle/Service/my_service/service.yml
parameters:
my_param1: %my_param1%
my_param2: %my_param2%
my_param3: %my_param3%
or something similar? How should I access to my application parameters from a service?
This question seems like the same but mine actually answers to it (parameters from a controller), I'm talking about accessing from a service.
You can pass parameters to your service in the same way as you inject other services, by specifying them in your service definition. For example, in YAML:
services:
my_service:
class: My\Bundle\Service\MyService
arguments: [%my_param1%, %my_param2%]
where the %my_param1% etc corresponds to a parameter named my_param1. Then your service class constructor could then be:
public function __construct($myParam1, $myParam2)
{
// ...
}
The Clean Way 2018
Since 2018 and Symfony 3.4 there is much cleaner way - easy to setup and use.
Instead of using container and service/parameter locator anti-pattern, you can pass parameters to class via it's constructor. Don't worry, it's not time-demanding work, but rather setup once & forget approach.
How to set it up in 2 steps?
1. config.yml
# config.yml
parameters:
api_pass: 'secret_password'
api_user: 'my_name'
services:
_defaults:
autowire: true
bind:
$apiPass: '%api_pass%'
$apiUser: '%api_user%'
App\:
resource: ..
2. Any Controller
<?php declare(strict_types=1);
final class ApiController extends SymfonyController
{
/**
* #var string
*/
private $apiPass;
/**
* #var string
*/
private $apiUser;
public function __construct(string $apiPass, string $apiUser)
{
$this->apiPass = $apiPass;
$this->apiUser = $apiUser;
}
public function registerAction(): void
{
var_dump($this->apiPass); // "secret_password"
var_dump($this->apiUser); // "my_name"
}
}
Instant Upgrade Ready!
In case you use older approach, you can automate it with Rector.
Read More
This is called constructor injection over services locator approach.
To read more about this, check my post How to Get Parameter in Symfony Controller the Clean Way.
(It's tested and I keep it updated for new Symfony major version (5, 6...)).
There is a very clean new way to achieve it since symfony 4.1
<?php
// src/Service/MessageGeneratorService.php
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class MessageGeneratorService
{
private $params;
public function __construct(ParameterBagInterface $params)
{
$this->params = $params;
}
public function someMethod()
{
$parameterValue = $this->params->get('parameter_name');
...
}
}
source : https://symfony.com/blog/new-in-symfony-4-1-getting-container-parameters-as-a-service.
Instead of mapping your needed parameters one by one, why not allowing your service to access the container directly? Doing so, you do not have to update your mapping if there is new parameters added (which relate to your service).
To do so:
Make following changes to your service class
use Symfony\Component\DependencyInjection\ContainerInterface; // <- Add this
class MyServiceClass
{
private $container; // <- Add this
public function __construct(ContainerInterface $container) // <- Add this
{
$this->container = $container;
}
public function doSomething()
{
$this->container->getParameter('param_name_1'); // <- Access your param
}
}
Add #service_container as "arguments" in your services.yml
services:
my_service_id:
class: ...\MyServiceClass
arguments: ["#service_container"] // <- Add this
With Symfony 4.1 the solution is quite simple.
Here is a snippet from the original post:
// src/Service/MessageGenerator.php
// ...
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class MessageGenerator
{
private $params;
public function __construct(ParameterBagInterface $params)
{
$this->params = $params;
}
public function someMethod()
{
$parameterValue = $this->params->get('parameter_name');
// ...
}
}
Link to the original post:
https://symfony.com/blog/new-in-symfony-4-1-getting-container-parameters-as-a-service
As solution to some of issues mentioned, I define an array parameter then inject it. Adding a new parameter later just requires addition to parameter array without any change to service_container arguments or construct.
So extending on #richsage answer:
parameters.yml
parameters:
array_param_name:
param_name_1: "value"
param_name_2: "value"
services.yml
services:
my_service:
class: My\Bundle\Service\MyService
arguments: [%array_param_name%]
Then access inside class
public function __construct($params)
{
$this->param1 = array_key_exists('param_name_1',$params)
? $params['param_name_1'] : null;
// ...
}
#richsage is correct (for Symfony 3.?) but it did not work for my Symfony 4.x. So here is for Symfony 4.
in services.yaml file
parameters:
param1: 'hello'
Services:
App\Service\routineCheck:
arguments:
$toBechecked: '%param1%' # argument must match in class constructor
in your service class routineCheck.php file do constructor like so
private $toBechecked;
public function __construct($toBechecked)
{
$this->toBechecked = $toBechecked;
}
public function echoSomething()
{
echo $this->toBechecked;
}
Done.
Symfony 3.4 here.
After some researches, I don't think passing parameters to a class/service via it's constructor, is always a good idea.
Imagine if you need to pass to a controller/service some more parameters than 2 or 3. What then? Would be ridiculous to pass, let's say, up to 10 parameters.
Instead, use the ParameterBag class as a dependency, when declaring the service in yml, and then use as many parameters as you wish.
A concrete example, let's say you have a mailer service, like PHPMailer, and you want to have the PHPMailer connection parameters in the paramters.yml file:
#parameters.yml
parameters:
mail_admin: abc#abc.abc
mail_host: mail.abc.com
mail_username: noreply#abc.com
mail_password: pass
mail_from: contact#abc.com
mail_from_name: contact#abc.com
mail_smtp_secure: 'ssl'
mail_port: 465
#services.yml
services:
app.php_mailer:
class: AppBundle\Services\PHPMailerService
arguments: ['#assetic.parameter_bag'] #here one could have other services to be injected
public: true
# AppBundle\Services\PHPMailerService.php
...
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
...
class PHPMailerService
{
private $parameterBag;
private $mailAdmin;
private $mailHost;
private $mailUsername;
private $mailPassword;
private $mailFrom;
private $mailFromName;
private $mailSMTPSecure;
private $mailPort;
}
public function __construct(ParameterBag $parameterBag)
{
$this->parameterBag = $parameterBag;
$this->mailAdmin = $this->parameterBag->get('mail_admin');
$this->mailHost = $this->parameterBag->get('mail_host');
$this->mailUsername = $this->parameterBag->get('mail_username');
$this->mailPassword = $this->parameterBag->get('mail_password');
$this->mailFrom = $this->parameterBag->get('mail_from');
$this->mailFromName = $this->parameterBag->get('mail_from_name');
$this->mailSMTPSecure = $this->parameterBag->get('mail_smtp_secure');
$this->mailPort = $this->parameterBag->get('mail_port');
}
public function sendEmail()
{
//...
}
I think this is a better way.
In symfony 4, we can access the parameters by means of dependency injection:
Services:
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
MyServices {
protected $container;
protected $path;
public function __construct(Container $container)
{
$this->container = $container;
$this->path = $this->container->getParameter('upload_directory');
}
}
parameters.yml:
parameters:
upload_directory: '%kernel.project_dir%/public/uploads'
Related
From my controllers, I access the application parameters (those in /app/config) with
$this->container->getParameter('my_param')
But I don't know how to access it from a service (I imagine my service class is not supposed to extend Symfony\Bundle\FrameworkBundle\Controller\Controller).
Should I map needed parameters into my service registration like this:
#src/Me/MyBundle/Service/my_service/service.yml
parameters:
my_param1: %my_param1%
my_param2: %my_param2%
my_param3: %my_param3%
or something similar? How should I access to my application parameters from a service?
This question seems like the same but mine actually answers to it (parameters from a controller), I'm talking about accessing from a service.
You can pass parameters to your service in the same way as you inject other services, by specifying them in your service definition. For example, in YAML:
services:
my_service:
class: My\Bundle\Service\MyService
arguments: [%my_param1%, %my_param2%]
where the %my_param1% etc corresponds to a parameter named my_param1. Then your service class constructor could then be:
public function __construct($myParam1, $myParam2)
{
// ...
}
The Clean Way 2018
Since 2018 and Symfony 3.4 there is much cleaner way - easy to setup and use.
Instead of using container and service/parameter locator anti-pattern, you can pass parameters to class via it's constructor. Don't worry, it's not time-demanding work, but rather setup once & forget approach.
How to set it up in 2 steps?
1. config.yml
# config.yml
parameters:
api_pass: 'secret_password'
api_user: 'my_name'
services:
_defaults:
autowire: true
bind:
$apiPass: '%api_pass%'
$apiUser: '%api_user%'
App\:
resource: ..
2. Any Controller
<?php declare(strict_types=1);
final class ApiController extends SymfonyController
{
/**
* #var string
*/
private $apiPass;
/**
* #var string
*/
private $apiUser;
public function __construct(string $apiPass, string $apiUser)
{
$this->apiPass = $apiPass;
$this->apiUser = $apiUser;
}
public function registerAction(): void
{
var_dump($this->apiPass); // "secret_password"
var_dump($this->apiUser); // "my_name"
}
}
Instant Upgrade Ready!
In case you use older approach, you can automate it with Rector.
Read More
This is called constructor injection over services locator approach.
To read more about this, check my post How to Get Parameter in Symfony Controller the Clean Way.
(It's tested and I keep it updated for new Symfony major version (5, 6...)).
There is a very clean new way to achieve it since symfony 4.1
<?php
// src/Service/MessageGeneratorService.php
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class MessageGeneratorService
{
private $params;
public function __construct(ParameterBagInterface $params)
{
$this->params = $params;
}
public function someMethod()
{
$parameterValue = $this->params->get('parameter_name');
...
}
}
source : https://symfony.com/blog/new-in-symfony-4-1-getting-container-parameters-as-a-service.
Instead of mapping your needed parameters one by one, why not allowing your service to access the container directly? Doing so, you do not have to update your mapping if there is new parameters added (which relate to your service).
To do so:
Make following changes to your service class
use Symfony\Component\DependencyInjection\ContainerInterface; // <- Add this
class MyServiceClass
{
private $container; // <- Add this
public function __construct(ContainerInterface $container) // <- Add this
{
$this->container = $container;
}
public function doSomething()
{
$this->container->getParameter('param_name_1'); // <- Access your param
}
}
Add #service_container as "arguments" in your services.yml
services:
my_service_id:
class: ...\MyServiceClass
arguments: ["#service_container"] // <- Add this
With Symfony 4.1 the solution is quite simple.
Here is a snippet from the original post:
// src/Service/MessageGenerator.php
// ...
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
class MessageGenerator
{
private $params;
public function __construct(ParameterBagInterface $params)
{
$this->params = $params;
}
public function someMethod()
{
$parameterValue = $this->params->get('parameter_name');
// ...
}
}
Link to the original post:
https://symfony.com/blog/new-in-symfony-4-1-getting-container-parameters-as-a-service
As solution to some of issues mentioned, I define an array parameter then inject it. Adding a new parameter later just requires addition to parameter array without any change to service_container arguments or construct.
So extending on #richsage answer:
parameters.yml
parameters:
array_param_name:
param_name_1: "value"
param_name_2: "value"
services.yml
services:
my_service:
class: My\Bundle\Service\MyService
arguments: [%array_param_name%]
Then access inside class
public function __construct($params)
{
$this->param1 = array_key_exists('param_name_1',$params)
? $params['param_name_1'] : null;
// ...
}
#richsage is correct (for Symfony 3.?) but it did not work for my Symfony 4.x. So here is for Symfony 4.
in services.yaml file
parameters:
param1: 'hello'
Services:
App\Service\routineCheck:
arguments:
$toBechecked: '%param1%' # argument must match in class constructor
in your service class routineCheck.php file do constructor like so
private $toBechecked;
public function __construct($toBechecked)
{
$this->toBechecked = $toBechecked;
}
public function echoSomething()
{
echo $this->toBechecked;
}
Done.
Symfony 3.4 here.
After some researches, I don't think passing parameters to a class/service via it's constructor, is always a good idea.
Imagine if you need to pass to a controller/service some more parameters than 2 or 3. What then? Would be ridiculous to pass, let's say, up to 10 parameters.
Instead, use the ParameterBag class as a dependency, when declaring the service in yml, and then use as many parameters as you wish.
A concrete example, let's say you have a mailer service, like PHPMailer, and you want to have the PHPMailer connection parameters in the paramters.yml file:
#parameters.yml
parameters:
mail_admin: abc#abc.abc
mail_host: mail.abc.com
mail_username: noreply#abc.com
mail_password: pass
mail_from: contact#abc.com
mail_from_name: contact#abc.com
mail_smtp_secure: 'ssl'
mail_port: 465
#services.yml
services:
app.php_mailer:
class: AppBundle\Services\PHPMailerService
arguments: ['#assetic.parameter_bag'] #here one could have other services to be injected
public: true
# AppBundle\Services\PHPMailerService.php
...
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
...
class PHPMailerService
{
private $parameterBag;
private $mailAdmin;
private $mailHost;
private $mailUsername;
private $mailPassword;
private $mailFrom;
private $mailFromName;
private $mailSMTPSecure;
private $mailPort;
}
public function __construct(ParameterBag $parameterBag)
{
$this->parameterBag = $parameterBag;
$this->mailAdmin = $this->parameterBag->get('mail_admin');
$this->mailHost = $this->parameterBag->get('mail_host');
$this->mailUsername = $this->parameterBag->get('mail_username');
$this->mailPassword = $this->parameterBag->get('mail_password');
$this->mailFrom = $this->parameterBag->get('mail_from');
$this->mailFromName = $this->parameterBag->get('mail_from_name');
$this->mailSMTPSecure = $this->parameterBag->get('mail_smtp_secure');
$this->mailPort = $this->parameterBag->get('mail_port');
}
public function sendEmail()
{
//...
}
I think this is a better way.
In symfony 4, we can access the parameters by means of dependency injection:
Services:
use Symfony\Component\DependencyInjection\ContainerInterface as Container;
MyServices {
protected $container;
protected $path;
public function __construct(Container $container)
{
$this->container = $container;
$this->path = $this->container->getParameter('upload_directory');
}
}
parameters.yml:
parameters:
upload_directory: '%kernel.project_dir%/public/uploads'
I am creating an app using Symfony 4 and Docker. In my .env file, I have the following line:
DEVICE_CREATION_SECRET=123456
... and in my services.yaml file, I have the following definition:
VMS\Application\DigitalRetail\Handler\DeviceForwardHandler:
arguments:
- env(DEVICE_CREATION_SECRET)
... which I expect to hand off my secret (123456) to my class, since I have this in that class:
public function __construct(string $deviceCreationSecret)
{
$this->deviceCreationSecret = $deviceCreationSecret;
}
But when I run my app and dump out the value, I get env(DEVICE_CREATION_SECRET) rather then my secret (123456). What do I need to get access to that secret?
I think this way should work:
VMS\Application\DigitalRetail\Handler\DeviceForwardHandler:
arguments:
- '%env(DEVICE_CREATION_SECRET)%'
https://symfony.com/doc/current/configuration/external_parameters.html
Go to services.yml:
parameters:
DEVICE_CREATION_SECRET: '%env(DEVICE_CREATION_SECRET)%'
After this, on the class, inject parameterBagInterface:
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
private $deviceCreationSecret;
private $params;
public function __construct(
string $deviceCreationSecret,
ParameterBagInterface $params
)
{
$this->deviceCreationSecret = $deviceCreationSecret;
$this->params = $params;
}
And then, for get parameter:
$this->params->get('DEVICE_CREATION_SECRET');
I have a Sumfony 4.3 command that processes some data and loops through a number of "processors" to do the processing. The code uses a factory (autowired) which then instantiates the command.
use App\Entity\ImportedFile;
use App\Service\Processor\Processor;
class Factory implements FactoryInterface
{
/** #var array */
private $processors;
/** #var TestClausesInterface */
private $testClauses;
private $em;
private $dataSetProvider;
private $ndviFromNasaService;
private $archivalHashService;
private $mailer;
private $projectDir;
public function __construct(
TestClausesInterface $testClauses,
ValidProcessorList $processors,
EntityManagerInterface $em,
DataSetProvider $dataSetProvider,
NDVIFromNasaService $ndviFromNasaService,
ArchivalHashService $archivalHashService,
\Swift_Mailer $mailer,
$projectDir)
{
$this->processors = $processors;
$this->testClauses = $testClauses;
$this->em = $em;
$this->dataSetProvider = $dataSetProvider;
$this->ndviFromNasaService = $ndviFromNasaService;
$this->archivalHashService = $archivalHashService;
$this->mailer = $mailer;
$this->projectDir = $projectDir;
}
public function findProcessorForFile(ImportedFile $file)
{
...
if ($found){
$candidates = $this->recursive_scan( $this->projectDir.'/src/Processor');
foreach ($candidates as $candidate){
if (substr($candidate,0,strlen('Helper')) === 'Helper'){
continue;
}
try {
$candidate = str_replace($this->projectDir.'/src/Processor/', '', $candidate);
$candidate = str_replace('/','\\', $candidate);
$testClassName = '\\App\\Processor\\'.substr( $candidate, 0, -4 );
/* #var Processor $test */
if (!strstr($candidate, 'Helper')) {
$test = new $testClassName($this->testClauses, $this->em, $this->dataSetProvider, $this->ndviFromNasaService, $this->archivalHashService, $this->mailer, $this->projectDir);
}
However I still have to:
autowire all arguments both in the Factory and Processor top class
pass all arguments in correct order to the Processor
I have around 70 subclasses of Processor. All of them use EntityInterface, but only a couple use SwiftMailer and the other dependencies.
As I am adding services to be used only by a few Processors, I am looking for a way to autowire these arguments only at the Processor level. Ideally, also without adding service definitions to services.yml
In summary, I would like to be able to add a dependency to any subclass of Processor, even if it is a parent class of other subclasses and have the dependency automatically injected.
There is much it is not immediately obvious in your code, but the typical way to resolve this is by using a "service locator". Docs.
Let's imagine you have several services implementing the interface Processor:
The interface:
interface Processor {
public function process($file): void;
}
Couple implementation:
class Foo implements Processor
{
public function __construct(DataSetProvider $dataSet, ArchivalHashService $archivalHash, \Swift_Mailer $swift) {
// initialize properties
}
public function process($file) {
// process implementation
}
public static function getDefaultIndexName(): string
{
return 'candidateFileOne';
}
}
Couple implementations:
class Bar implements Processor
{
public function __construct(\Swift_Mailer $swift, EntityManagerInterface $em) {
// initialize properties
}
public function process($file) {
// process implementation
}
public static function getDefaultIndexName(): string
{
return 'candidateFileTwo';
}
}
Note that each of the processors have completely different dependencies, and can be auto-wired directly, and that each of them has a getDefaultIndexName() method.
Now we'll "tag" all services implementing the Processor interface:
# services.yaml
services:
# somewhere below the _defaults and the part where you make all classes in `src` available as services
_instanceof:
App\Processor:
tags:
- { name: "processor_services", default_index_method: 'getDefaultIndexName' }
Attention here: The documentation says that if you define a public static function getDefaultIndexName() it will be picked by default. But I've found this not to be working at the moment. But if you define the default_index_method you can wire it to a method of your choice. I'm keeping the getDefaultIndexName for the time being, but you can pick something of your own choice.
Now, if you need this processes in a console command, for example:
use Symfony\Component\DependencyInjection\ServiceLocator;
class MyConsoleCommand
{
private ServiceLocator $locator;
public function __construct(ServiceLocator $locator)
{
$this->locator = $locator;
}
}
To inject the service locator you would do:
#services.yaml
services:
App\HandlerCollection:
arguments: [!tagged_locator { tag: 'processor_services' } ]
And to fetch any of the processors from the service locator you would do:
$fooProcessor = $this->locator->get('candidateFileOne');
$barProcessor = $this->locator->get('candidateFileTwo');
Summping up, basically what you need is:
Define a shared interface for the processors
Use that interface to tag all the processor services
Define a getDefaultIndexName() for each processor, which helps you match files to processors.
Inject a tagged service locator in the class that need to consume this services
And you can leave all services auto-wired.
Note: You could use an abstract class instead of an interface, and it would work the same way. I prefer using an interface, but that's up to you.
For completion sake, here is a repo with the above working for Symfony 4.3.
In Synfony 3.3, the new best practice for DI
is to use normal constructor dependency injection (or "action"
injection in controllers) instead of fetching public services via
$this->get() (though that does still work)
as seen in offical documentation
So no need to specify services as we can type hint them in class controllers :
class InvoiceMailer
{
private $generator;
public function __construct(InvoiceGenerator $generator)
{
$this->generator = $generator
}
// ...
}
This seems to work well, but what if I extends a class and add more parameters in my constructors ???
use Symfony\Component\HttpKernel\Exception\HttpException;
class MyClass extends HttpException
{
private $generator;
public function __construct(InvoiceGenerator $generator, \Exception $previous = null, array $headers = [], $code = 0)
{
$this->generator = $generator;
$statusCode = $generator->getStatusCode();
$message = $generator->getTitle();
parent::__construct($statusCode, $message, $previous, $headers, $code);
}
// ...
}
Now I get a circular reference error :
[Symfony\Component\DependencyInjection\Exception\ServiceCircularReferenceException]
Circular reference detected for service "AppBundle\Service\MyClass", path: "AppBundle\Service\MyClass -> AppBundle\Service\MyClass".
So, what is the best practice in this case ??
Thanks.
For that case you might have to define the service explicitly as \Exception $previous argument in fact is parent class of MyClass (through HttpException) so the autowiring method try to inject/create an instance of MyClass again on this argument, result: "Circular Reference".
This is an abstraction of what happens to you:
namespace App\Foo;
class MyClass extends \Exception
{
public function __construct(\Exception $previous = null)
{
}
}
Same error, so you can to solve it passing a null value to this argument:
# service.yml
services:
# ...
App\Foo\MyClass:
$previous: ~
or changing its definition manually in a compiler pass or DI extension.
I would like to use $this->container->get in a custom class I've created.
I've done my reading and found out that I should use ContainerInterface in the constructor, which I do, but I still get this error:
Error: Call to a member function get() on a non-object
Here is the code:
MyClass.php
namespace path\to\MyClass;
use Symfony\Component\DependencyInjection\ContainerInterface;
class MyClass {
private $container;
public $user_id;
public function __contruct(ContainerInterface $container) {
$this->container = $container;
$this->user_id = $user_id;
return $this;
}
/**
* #param string $data Some data
* #return array A response
*/
public function generatePDF($data)
{
// Create the folders if needed
$pdf_folder = __DIR__.'/../../../../web/pdf/'.$this->user_id.'/';
if(!file_exists($pdf_folder))
mkdir($pdf_folder, 0755, TRUE);
$file_id = "abc1";
// Set the file name
$file = $pdf_folder.$file_id.'.pdf';
// Remove the file if it exists to prevent errors
if(file_exists($file)) {
unlink($file);
}
// Generate the PDF
$this->container->get('knp_snappy.pdf')->generateFromHtml(
$this->renderView(
'StrimeGlobalBundle:PDF:invoice.html.twig',
$data
),
$file
);
}
}
Do you guys have any idea of what could be the problem?
Thanks for your help.
You need to declare your class as a service in the Symfony configuration.
Please have a look to the Symfony service container page.
Here is an explanation to inject the container in the constructor:
# services.yml
services:
app.my_class:
class: TheBundle\Service\MyClass
arguments: ['#service_container']
Or as said JimL in comment, you can inject the service you need (which is recommanded):
class MyClass
{
private $pdfService;
public function __construct(\Your\Service\Namespace\Class $pdfService)
{
$this->pdfService = $pdfService;
}
// ...
}
And in your service.yml file
# services.yml
services:
app.my_class:
class: TheBundle\Service\MyClass
arguments: ['#knp_snappy.pdf']
The container can also be injected with a setter. See this link
Hope this helps!
Your class cannot "see" any service (including the container) unless you "inject" it. In YourCustomBundle/Resources/config/services.xml you need to define the service and it's dependencies. Read up on Dependency Injection and it should make more sense.
Also, #JimL is right, you shouldn't inject the entire container to access one service, simply inject the one service (knp_snappy.pdf)