Currently I'm working on testing some services in Symfony2 and I'm trying to use Guzzle MockPlugin for controlling CURL responses. Symfony version 2.3.8 is used. I've got to an interesting behaviour and I'm not sure if this is a Symfony2 bug or not.
I have these services in services.yml:
lookup_service_client:
class: FOO
public: false
factory_service: lookup_client_builder
factory_method: build
lookup_repository_auth_type:
class: AuthType
arguments: ["#lookup_service_client"]
lookup_repository_cancel_reason:
class: CancelReason
arguments: ["#lookup_service_client"]
payment_service_client:
class: FOO
public: false
factory_service: payment_client_builder
factory_method: build
payment_repository:
class: Payment
arguments: ["#payment_service_client"]
The name of the classes are not important. You can see that both "lookup_service_client" and "lookup_service_client" are PRIVATE services.
I have a test class, which extends Symfony\Bundle\FrameworkBundle\Test\WebTestCase. In one test I need to do something like:
$lookup = $this->client->getContainer()->get('lookup_service_client');
$payment = $this->client->getContainer()->get('payment_service_client');
I expected that, setting those services as PRIVATE, will not let me retrieve the services from container in tests, but the actual result is:
$lookup = $this->client->getContainer()->get('lookup_service_client'); => returns the service instance
$payment = $this->client->getContainer()->get('payment_service_client'); => returns an exception saying: "You have requested a non-existent service"
The only difference between those tow service_client services is that "lookup_service_client" is injected in several other services, while "payment_service_client" is injected in only one other service.
So, the questions are:
Why I can retrieve from container "lookup_service_client", since I've set it to private?
Why I can retrieve "lookup_service_client", but cannot retrieve "payment_service_client" since the only difference is presented above?
Is it a Symfony2 bug that I can access private service?
There were some new changes regarding this in Symfony 4.1:
In Symfony 4.1, we did the same and now tests allow fetching private services by default.
In practice, tests based on WebTestCase and KernelTestCase now access to a special container via $client->getContainer() or the static::$container property that allows to fetch non-removed private services.
You can read more about it in the news post.
While this is not a bug, it is definitely counter intuitive. The manual specifically says:
Now that the service is private, you should not fetch the service
directly from the container:
$container->get('foo');
This may or may not work, depending on how the container has optimized
the service instanciation and, even in the cases where it works, is
deprecated. Simply said: A service can be marked as private if you do
not want to access it directly from your code.
Which is why the core team has decided to make this behavior more consistent and intuitive in Symfony 4:
Setting or unsetting a private service with the Container::set() method is deprecated in Symfony 3.2 and no longer supported in 4.0;
Checking the existence of a private service with the Container::has() will always return false in Symfony 4.0;
Requesting a private service with the Container::get() method is deprecated in Symfony 3.2 and no longer returns the service in 4.0.
2018+ and Symfony 3.4/4.0+ solution
This approach with all its pros/cons is described in this post with code examples.
The best solution to access private services is to add a Compiler Pass that makes all services public for tests. That's it. How does it look in practice?
1. Update Kernel
use Symfony\Component\HttpKernel\Kernel;
+use Symplify\PackageBuilder\DependencyInjection\CompilerPass\PublicForTestsCompilerPass;
final class AppKernel extends Kernel
{
protected function build(ContainerBuilder $containerBuilder): void
{
$containerBuilder->addCompilerPass('...');
+ $containerBuilder->addCompilerPass(new PublicForTestsCompilerPass());
}
}
2. Require or create own Compiler Pass
Where PublicForTestsCompilerPass looks like:
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
final class PublicForTestsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $containerBuilder): void
{
if (! $this->isPHPUnit()) {
return;
}
foreach ($containerBuilder->getDefinitions() as $definition) {
$definition->setPublic(true);
}
foreach ($containerBuilder->getAliases() as $definition) {
$definition->setPublic(true);
}
}
private function isPHPUnit(): bool
{
// defined by PHPUnit
return defined('PHPUNIT_COMPOSER_INSTALL') || defined('__PHPUNIT_PHAR__');
}
}
To use this class, just add the package by:
composer require symplify/package-builder
But of course, the better way is to use own class, that meets your needs (you might Behat for tests etc.).
Then all your tests will keep working as expected!
Let me know, how that works for you.
Check them in the container:
container:debug lookup_service_client
container:debug payment_service_client
in your example they both have class "FOO", maybe that's the case
Related
I am developing a Symfony 3 application. Symfony profiler logs tell me:
Relying on service auto-registration for type "App\Entity\SubDir\Category"
is deprecated since version 3.4 and won't be supported in 4.0.
Create a service named "App\Entity\SubDir\Category" instead.
Yet, this is a simple ORM bean:
/**
* #ORM\Entity
* #ORM\Table(name="category")
*/
class Category
{
...
How should I get rid of this issue? Do I really need to declare ORM entities as services in services.yaml? If yes, how?
Update
In fact, my entity is in a sub directory. I have amended my question.
In my service.yaml, I have tried:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Repository,Tests,Entity/SubDir}'
...but to no avail.
Do you have any Classes under Service-auto registration which use an Entity as constructor argument?
That's where your problem comes from.
You need to ask yourself if the concerning class really is a service or just a plain object of which you always create the instance yourself.
If it is not used as a service through the container you have 2 options:
You can exclude this class also through the glob pattern like for example
AppBundle\:
resource: '...'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../{Entity,PathToYourNotService}'
or you can set the following parameter in your config
parameters:
container.autowiring.strict_mode: true
with this option the container won't try to create a service class with arguments that are not available as services and you will get a decisive error. This is the default setting for sf4
A good example for a class that triggers exactly this error would be a custom event class that takes an entity as payload in the constructor:
namespace AppBundle\Event;
use AppBundle\Entity\Item;
use Symfony\Component\EventDispatcher\Event;
class ItemUpdateEvent extends Event
{
const NAME = 'item.update';
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function getItem()
{
return $this->item;
}
}
Now if this file isn't excluded specifically the container will try to auto register it as service. And because the Entity is excluded it can't autowire it. But in 3.4 there's this fallback which triggers this warning.
Once the strict_mode is activated the event just won't be available as service and if you tried using it as one an error would rise.
In our integration tests, we need to get/set a few services, so need them to be public.
What we currently do is configure every such service this way:
App\Infrastructure\Mail\Transport\SenderInterface:
public: '%services_are_public%'
And our test environment is configured as such:
parameters:
services_are_public: true
Is there a way to make all services public by default instead, in a given environment?
If you are using Symfony\Bundle\FrameworkBundle\Test\WebTestCase or Symfony\Bundle\FrameworkBundle\Test\KernelTestCase (which you probably should, for functional/integration testing), there is no need to make services public.
These classes include a simple method to get a "special" container that is able to get private services directly:
$container = static::$container;
This has been the case since Symfony 4.1, and it's documented here.
If the above for some reason doesn't work for you, you could create a services_test.yaml file and add this:
# config/services_test.yaml
services:
_defaults:
public: true
... but this would only affect autowired services. If you need to access a services wired by a bundle, that service would remain private.
Finally, you could create a compiler pass to make all services public:
class MakeServicesPublicPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container): void
{
foreach ($container->getDefinitions() as $id => $definition) {
$definition->setPublic(true);
}
foreach ($container->getAliases() as $id => $alias) {
$alias->setPublic(true);
}
}
}
... and register this compiler pass only on your testing kernel.
Personally, I'd say the first option is the one to go, if possible in your scenario.
I am migrating our project to Symfony 4. In my test suites, we used PHPUnit for functional tests (I mean, we call endpoints and we check result). Often, we mock services to check different steps.
Since I migrated to Symfony 4, I am facing this issue: Symfony\Component\DependencyInjection\Exception\InvalidArgumentException: The "my.service" service is already initialized, you cannot replace it.
when we redefine it like this : static::$container->set("my.service", $mock);
Only for tests, how can I fix this issue?
Thank you
Replacing is deprecated since Symfony 3.3. Instead of replacing service you should try using aliases.
http://symfony.com/doc/current/service_container/alias_private.html
Also, you can try this approach:
$this->container->getDefinition('user.user_service')->setSynthetic(true);
before doing $container->set()
Replace Symfony service in tests for php 7.2
Finally, I found a solution. Maybe not the best, but, it's working:
I created another test container class and I override the services property using Reflection:
<?php
namespace My\Bundle\Test;
use Symfony\Bundle\FrameworkBundle\Test\TestContainer as BaseTestContainer;
class TestContainer extends BaseTestContainer
{
private $publicContainer;
public function set($id, $service)
{
$r = new \ReflectionObject($this->publicContainer);
$p = $r->getProperty('services');
$p->setAccessible(true);
$services = $p->getValue($this->publicContainer);
$services[$id] = $service;
$p->setValue($this->publicContainer, $services);
}
public function setPublicContainer($container)
{
$this->publicContainer = $container;
}
Kernel.php :
<?php
namespace App;
use Symfony\Component\HttpKernel\Kernel as BaseKernel;
class Kernel extends BaseKernel
{
use MicroKernelTrait;
public function getOriginalContainer()
{
if(!$this->container) {
parent::boot();
}
/** #var Container $container */
return $this->container;
}
public function getContainer()
{
if ($this->environment == 'prod') {
return parent::getContainer();
}
/** #var Container $container */
$container = $this->getOriginalContainer();
$testContainer = $container->get('my.test.service_container');
$testContainer->setPublicContainer($container);
return $testContainer;
}
It's really ugly, but it's working.
I've got a couple of tests like this (the real code performs some actions and returns a result, the test-version just returns false for every answer).
If you create and use a custom config for each environment (eg: a services_test.yaml, or in Symfony4 probably tests/services.yaml), and first have it include dev/services.yaml, but then override the service you want, the last definition will be used.
app/config/services_test.yml:
imports:
- { resource: services.yml }
App\BotDetector\BotDetectable: '#App\BotDetector\BotDetectorNeverBot'
# in the top-level 'live/prod' config this would be
# App\BotDetector\BotDetectable: '#App\BotDetector\BotDetector'
Here, I'm using an Interface as a service-name, but it will do the same with '#service.name' style as well.
As I understood it, it means that class X was already injected(because of some other dependency) somewhere before your code tries to overwrite it with self::$container->set(X:class, $someMock).
If you on Symfony 3.4 and below you can ovverride services in container regardless it privite or public. Only deprication notice will be emmited, with content similar to error message from question.
On Symfony 4.0 error from the question was thown.
But on Symfony 4.1 and above you can lean on special "test" container. To learn how to use it consider follow next links:
https://symfony.com/blog/new-in-symfony-4-1-simpler-service-testing
https://dev.to/nikolastojilj12/symfony-5-mocking-private-autowired-services-in-controller-functional-tests-24j4
Trying to upgrade a project from Symfony 3.3 to 3.4. I've done composer update symfony/symfony --with-depdencies and added public: false to my services.yml file.
Now when I run my PHPUnit tests, I get this error:
Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException : The service "templating.loader.cache" has a dependency on a non-existent service "templating.loader.wrapped".
Any ideas why this happens? I can't find any Google results or any Symfony documentation references for this at all...
Problem was found to be caused by overriding the definition of templating.loader.cache to public in a compiler pass class to allow access during functional tests.
Based off code here: https://github.com/symfony/symfony-docs/issues/8097
tl;dr do not do this:
final class TestCompilerPass implements CompilerPassInterface
{
/** {#inheritdoc} */
public function process(ContainerBuilder $container)
{
foreach ($container->getDefinitions() as $id => $definition) {
$definition->setPublic(true);
}
}
}
Instead limit the services you make public to the ones you actually require.
Unless you prepared your code for private services you shouldn't use the public: false tag. That is used to mark services as private. Probably somewhere in your code you have something like $var = $container->get('example'); which calls a public service. You can read more here.
I am developing a Symfony 3 application. Symfony profiler logs tell me:
Relying on service auto-registration for type "App\Entity\SubDir\Category"
is deprecated since version 3.4 and won't be supported in 4.0.
Create a service named "App\Entity\SubDir\Category" instead.
Yet, this is a simple ORM bean:
/**
* #ORM\Entity
* #ORM\Table(name="category")
*/
class Category
{
...
How should I get rid of this issue? Do I really need to declare ORM entities as services in services.yaml? If yes, how?
Update
In fact, my entity is in a sub directory. I have amended my question.
In my service.yaml, I have tried:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Repository,Tests,Entity/SubDir}'
...but to no avail.
Do you have any Classes under Service-auto registration which use an Entity as constructor argument?
That's where your problem comes from.
You need to ask yourself if the concerning class really is a service or just a plain object of which you always create the instance yourself.
If it is not used as a service through the container you have 2 options:
You can exclude this class also through the glob pattern like for example
AppBundle\:
resource: '...'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../{Entity,PathToYourNotService}'
or you can set the following parameter in your config
parameters:
container.autowiring.strict_mode: true
with this option the container won't try to create a service class with arguments that are not available as services and you will get a decisive error. This is the default setting for sf4
A good example for a class that triggers exactly this error would be a custom event class that takes an entity as payload in the constructor:
namespace AppBundle\Event;
use AppBundle\Entity\Item;
use Symfony\Component\EventDispatcher\Event;
class ItemUpdateEvent extends Event
{
const NAME = 'item.update';
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function getItem()
{
return $this->item;
}
}
Now if this file isn't excluded specifically the container will try to auto register it as service. And because the Entity is excluded it can't autowire it. But in 3.4 there's this fallback which triggers this warning.
Once the strict_mode is activated the event just won't be available as service and if you tried using it as one an error would rise.