How to make all Symfony DI services public? - php

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.

Related

How to access not-injected services directly on Symfony 4+?

I'm trying to update Symfony 2.8 to Symfony 4 and I am having serious problems with the Services Injection.
I'm looking the new way to use Services inside Controllers, with auto-wiring:
use App\Service\AuxiliarService;
class DefaultController extends AbstractController
{
public function index(AuxiliarService $service)
{
$var = $service->MyFunction();
....
This way works fine, but I dislike the explicit way to refer MyService as a parameter of the function. This way I don't even need to register the Service in the services.yaml
Is there any way to use Services as in Symfony 2.8:
class DefaultController extends Controller
{
public function index()
{
$var = $this->get('AuxiliarService')->MyFunction(); /*Doesn't need to be explicit indicate before*/
....
With the services.yaml
services:
auxiliar_service:
class: AppBundle\Services\AuxiliarService
arguments:
entityManager: "#doctrine.orm.entity_manager"
container: "#service_container" #I need to call services inside the service
This way I don't need to indicate the Service as a parameter in the function of the Controller. In some cases, inside a Service, I need to call more than 10 services depends on the data, so indicate them as a parameter in the function is annoying.
Another doubt in Symfony 4, is how to call a Service inside another Service without pass it as an argument or parameter. It used to be possible by injecting the service container to be able to call a service inside a service:
$this->container->get('anotherService')
In Symfony 4, I think it is more expensive (in code) use Service because you have to explicitely indicate them when you are going to use them.
tldr; you can achieve that by using Service Subscribers & Locators.
In your controller:
use App\Service\AuxiliarService;
class DefaultController extends AbstractController
{
public function index(AuxiliarService $service)
{
$var = $service->MyFunction();
}
public static function getSubscribedServices()
{
return array_merge(parent::getSubscribedServices(), [
// services you want to access through $this->get()
'auxiliar_service' => AuxiliarService:class,
]);
}
// rest of the implementation
}
If your service needs to implement a similar pattern, you'll need to implement ServiceSubscriberInterface (AbstractController, that you are extending for your controller, already does that for you).
class AuxiliaryService implements ServiceSubscriberInterface
{
private $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
protected function has(string $id): bool
{
return $this->container->has($id);
}
protected function get(string $id)
{
return $this->container->get($id);
}
public static function getSubscribedServices()
{
return [
// array_merge is not necessary here, because we are not extending another class.
'logger' => LoggerInterface::class,
'service2' => AnotherService::class,
'service3' => AndMore::class
];
}
}
That being said, you are very probably not doing things right if you want to continue this way
Before Symfony 4+ you could do $this->get('service') because these controllers all had access to the container. Passing the dependency container around for this it is an anti-pattern, and shouldn't be done.
If you do not declare your dependencies, your dependencies are hidden. Users of the class do not know what it uses, and it's easier to break the system by changing the behaviour of one of the hidden dependencies.
Furthermore, with Symfony providing auto-wiring and a compiled container; dependency injection is both easier to implement and faster to execute.
That you are having trouble with implementing this probably reveals deeper issues with your code in general, and you should do some work on segregating the responsibilities of your classes. The fact that one service may depend on that many other services which you can't even know until runtime it's a very strong smell that the concerns are not well separated.
Try to adapt to the changes, it will do your application and yourself good in the long term (even if brings a small amount of pain right now).

Symfony upgrade 3.3 to 3.4 service not found

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.

Symfony2 access private services in tests

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

Disable SonataUserBundle sonata.user.admin.group service

I'm working with SonataAdminBundle and SonataUserBundle.
SonataUserBundle registers a service sonata.user.admin.group which is automatically detected by SonataAdminBundle to set links in the admin dashboard to group CRUD operations.
How can I disable sonata.user.admin.group? I've been following that recipes in Symfony2 documentation:
How to Override any Part of a Bundle - Services and Configuration
Compiling the Container - Creating a Compiler Pass
Working with Container Parameters and Definitions
So far, I have the following code in my bundle definition to add a compiler pass:
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new CompilerPass());
}
And here it is the compiler pass:
<?php
namespace NS\Service\CompilerPass;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class CompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$container->removeDefinition('sonata.user.admin.group');
}
}
I thought that this should work but no. Symfony is throwing an exception telling me that sonata.user.admin.group service does not exist. But it exists, and if I do $container->getDefinition('sonata.user.admin.group') the actual definition is return.
Thanks
Try marking the service as abstract and set its public property to false e.g.
#in any services.yml
services:
sonata.user.admin.group:
abstract: true
public: false
#...
Addition to completeness:
And add to the CompilerPass:
$container->getDefinition('sonata.user.admin.group')->setSynthetic(true);
You've removed the service definition but it's still used on the dashboard. That's why Symfony complains (dashboard tries to access it). It's not an optional service.
You could try to overwrite the dashboard template and avoid using the service? This way service wouldn't be called and you wouldn't have to remove it. If service is not used it's never created.
Alternative would be overloading the service with your implementation.

Symfony2 global functions

For example i have algorithmic function, which calculates specific hash-code. Function itself is 300+ lines of code. I need to use that functions many times in many different controllers in my bundle. Where can i store my calculate_hash() to use it in my bundle ? Can i access it from other bundles ?
Can i also write global calculate_hash() which have access to entity manager ?
Didn't find my answer here.
In the Symfony2 world, this is clearly belonging to a service. Services are in fact normal classes that are tied to the dependency injection container. You can inject them the dependencies you need. For example, say your class where the function calculate_hash is located is AlgorithmicHelper. The service holds "global" functions. You define your class something like this:
namespace Acme\AcmeBundle\Helper;
// Correct use statements here ...
class AlgorithmicHelper {
private $entityManager;
public function __construct(EntityManager $entityManager) {
$this->entityManager = $entityManager;
}
public function calculate_hash() {
// Do what you need, $this->entityManager holds a reference to your entity manager
}
}
This class then needs to be made aware to symfony dependecy container. For this, you define you service in the app/config/config.yml files by adding a service section like this:
services:
acme.helper.algorithmic:
class: Acme\AcmeBundle\Helper\AlgorithmicHelper
arguments:
entityManager: "#doctrine.orm.entity_manager"
Just below the service, is the service id. It is used to retrieve your service in the controllers for example. After, you specify the class of the service and then, the arguments to pass to the constructor of the class. The # notation means pass a reference to the service with id doctrine.orm.entity_manager.
Then, in your controller, you do something like this to retrieve the service and used it:
$helper = $this->get('acme.helper.algorithmic');
$helper-> calculate_hash();
Note that the result of the call to $this->get('acme.helper.algorithmic') will always return the same instance of the helper. This means that, by default, service are unique. It is like having a singleton class.
For further details, I invite you to read the Symfony2 book. Check those links also
The service container section from Symfony2 book.
An answer I gave on accesing service outside controllers, here.
Hope it helps.
Regards,
Matt
Braian in comment asked for Symfony 3 answer, so here is one Symfony 3.3 (released May 2017):
1. The original class remains the same
namespace Acme\AcmeBundle\Helper;
use Doctrine\ORM\EntityManager;
final class AlgorithmicHelper
{
/**
* #var EntityManager
*/
private $entityManager;
public function __construct(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function calculateHash()
{
// Do what you need, $this->entityManager holds a reference to your entity manager
}
}
2. Service registration is much simpler
# app/config/services.yml
services:
_defaults: autowire # this enabled constructor autowiring for all registered services
Acme\AcmeBundle\Helper\AlgorithmicHelper: ~
3. Use constructor injection to get the service
use Acme\AcmeBundle\Helper\AlgorithmicHelper;
class SomeController
{
/**
* #var AlgorithmicHelper
*/
private $algorithmicHelper;
public function __construct(AlgorithmicHelper $algorithmicHelper)
{
$this->algorithmicHelper = $algorithmicHelper;
}
public function someAction()
{
// some code
$hash = $this->algorithmicHelper->calculateHash();
// some code
}
}
You can read about Symfony 3.3 dependency injection (in this case registering services in config and using it in controller) news in these 2 posts:
https://www.tomasvotruba.cz/blog/2017/05/07/how-to-refactor-to-new-dependency-injection-features-in-symfony-3-3/
https://symfony.com/blog/the-new-symfony-3-3-service-configuration-changes-explained

Categories