Symfony ParameterBagInterface not working in phpunit - php

I use ParameterBagInterface in my service and this is code:
private function getConfigs(): array { return (array)$this->params->get($this->config); }
now when I create unit test for this function , return is empty array but I have content in services.yml in parameters
test: { url: 'hello test' }
parameterBagInterface not working in phpunit

Related

Symfony 6 services autowiring issue ContainerInterface [duplicate]

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'

Why does Symfony DI container multiplies instances of a service when API tests are run?

I have a service that should have only one instance and I was sure that DI container takes care about it. And when I send a request by Postman everything works properly, container returns always the very same instance. I have even added an additional field with a random number generated during object creation for debugging.
class ReadModel
{
private ?Database $database;
// Temporary field for debugging purpose
private int $tmp;
public function __construct()
{
$this->database = null;
$this->tmp = rand(1, 1000);
}
public function setDatabase(Database $database): void
{
$this->database = $database;
dump($this->tmp); // returns e.g. 836 when codeception test is run
}
public function getCollection(string $collectionName): Collection
{
dump($this->tmp); // returns e.g. 390 when codeception test is run
return $this->database->selectCollection($collectionName);
}
}
When a request is sent by Postman (dev environment) both numbers returned by dump() function are equal (the same instance is injected to other services that uses it). But when I run a Codeception test, this service behaves differently. When I call setDatabase() method of ReadModel in one service and getCollection methond in another one, I receive two different values of $tmp field. That means that two different instances of ReadModel service have been injected to my services. How is it possible and what can I do to have the same behavior in both environments (dev and test)?
ReadModel service is autowired so I don't paste services.yaml content.
My stack: Symfony 5.1, Codeception 4.1
PS. By Codeception tests I mean end2end API tests like this one, not unit tests:
<?php
use Codeception\Util\HttpCode;
class CreateProductCest
{
public function _before(ApiTester $I)
{
// Authentication stuff...
}
public function shouldCreateProductWithNoException(ApiTester $I)
{
$data = [
'name' => 'test123',
];
$I->sendPost('/product/create', $data);
$I->seeResponseIsJson();
$I->seeResponseCodeIs(HttpCode::OK);
$I->seeResponseContainsJson([]);
}
}
Examples of method calls of ReadModel object. Most of the code is cut off, I leaved only relevant parts of code.
class JwtDecodedListener
{
private ReadModel $readModel;
public function __construct(
ReadModel $readModel
) {
$this->readModel = $readModel;
}
public function onJWTDecoded(JWTDecodedEvent $event)
{
// some code
$client = new Client("mongodb://{$mongoDbUser}:{$mongoDbPass}#{$this->mongoHost}/{$mongoDb}");
$this->readModel->setDatabase($client->selectDatabase($mongoDbUser));
}
}
class ProductProjection
{
private ReadModel $readModel;
public function __construct(
ReadModel $readModel
) {
$this->readModel = $readModel;
}
public function whenProductWasCreated(Product $product): void
{
// $collectionName is from a factory
$collection = $this->readModel->getCollection($collectionName);
}
}
Edit: As suggested in the comments I added dump() call in the constructor of ReadModel class and my conclusion is the same. Post request executed from postman - one constructor cal. The same request executed by Codeception - 2 constructor calls. Still have no idea what's the reason.

In a Symfony 4 application, how do I pass an environment variable to a service class?

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

PHPUnit only recognizes three of my four tests

I have a class containing four unit tests. The class looks like this:
class TestWorkflowService extends TestCase
{
private $containerMock;
private $workflowEntityMock;
private $workflowService;
public function setup()
{
$this->containerMock = $this->createMock(ContainerInterface::class);
$this->workflowService = new WorkflowService($this->containerMock);
$this->workflowEntityMock = $this->createMock(WorkflowInterface::class);
}
public function testGetWorkflowProcessByEntityNullResult()
{
self::assertNull($this->workflowService->getWorkflowProcessByEntity($this->workflowEntityMock));
}
public function testGetProcessHandlerByEntityNullResult()
{
self::assertNull($this->workflowService->getProcessHandlerByEntity($this->workflowEntityMock));
}
public function testRestartWorkflow()
{
$modelStateMock = $this->createMock(ModelState::class);
$processHandlerMock = $this->createMock(ProcessHandler::class);
$processHandlerMock->method('start')->willReturn($modelStateMock);
$this->containerMock->method('get')->willReturn($processHandlerMock);
self::assertNull($this->workflowService->restartWorkflow($this->workflowEntityMock));
}
public function setEntityToNextWorkflowState()
{
$modelStateMock = $this->createMock(ModelState::class);
$processHandlerMock = $this->createMock(ProcessHandler::class);
$processHandlerMock->method('start')->willReturn($modelStateMock);
$this->containerMock->method('get')->willReturn($processHandlerMock);
self::assertNull($this->workflowService->setEntityToNextWorkflowState($this->workflowEntityMock));
}
}
... but when I run PHPUnit, I get this result:
... 3
/ 3 (100%)
Time: 2.17 seconds, Memory: 5.75MB
OK (3 tests, 3 assertions)
Why is my fourth test not being recognized?
PHPUnit identifies test methods using the following rules:
The tests are public methods that are named test*.
Alternatively, you can use the #test annotation in a method's docblock to mark it as a test method.
This is so you can have other public methods in your test class without them being interpreted as tests (although I'm not sure why you'd ever actually do this).
Change your method name to testSetEntityToNextWorkflowState, or tag it with the #test annotation.

How to access an application parameters from a service?

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'

Categories