Symfony applications using autowiring can remove most of their service
configuration and rely on the injection of services based on argument type-hinting.
The main exception to this feature are scalar arguments, such as a service
requiring the value of the kernel.project_dir
parameter in its constructor.
The solution is simple thanks to argument binding, which lets you define scalar arguments once and apply them to all the services defined in that same config file:
1 2 3 4 5 6
# config/services.yaml
services:
# ...
_defaults:
bind:
$projectDir: '%kernel.project_dir%'
However, if some particular service needs lots of container parameters (or even
all of them, for some edge case feature) using argument binding is cumbersome.
In Symfony 4.1 we added a feature to get all container parameters as a service.
You just need to type-hint the argument with the ParameterBagInterface
class
or the new ContainerBagInterface
class (which is compatible with the PSR-11 standard):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
// 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');
// ...
}
}
A design so simple and elegant one really wonders why it hadn't been done before. Bravo!
Wonderful, thank you Nicolas ;)
So now you can actually make a decorator and add your own key=>value storage for applications/cms etc.
nice !
Giving a service easy access to all parameters seems to violate the spirit of dependency injection. Basically the same as injecting the complete container. I guess there is a use case for it but returning to a service locator pattern is just a bit strange.
I was just about to post my comment which Art Hundiak already explained. I think that this promotes Service Locator anti-pattern. As said above you could simply inject entire Container, and we all know that's not a good idea.
I find this to be anti-pattern too and I can relate to Zarko Stankovic comment. Not sure it's a good idea to promote it.
@Art @Zarko @Tristan
You guys are right - certainly the best option is to use the already-existing functionality and inject the specific parameter you need :). To give a bit more background, this was needed internally so that $this->getParameter() could work in the new AbstractController. That base controller class is not passed the entire container - just a smaller "locator" with the services it needs. So, you couldn't access the parameters. This allows $this->getParameter() to work.
Another example usage is a service "configurator" - a class whose sole purpose is to help configure other services. The configurator may need to be able to dynamically select certain parameters so that it can just inject the one it needs into the service it's configuring :).
Cheers!
Basically If I understand correctly this will bind our service with Symfony Dependency Injection Component by this ParameterBagInterface. Personally I am trying to write framework agnostic services which rely on abstraction owned by me and I implements concrete implementation which use framework features then. At the end I use DIC to connect stuff together. As long as it will be optional I am fine with it, but please please do not force it and keep symfony optionanted ;)
This is really a good news. @Ryan you made the perfect answer to explain why this "anti-pattern" implementation (I don't like the term) is a good and human solution.
Perhaps a ParameterSubscriberInterface is needed to complement the ServiceSubscriberInterface currently used by the AbstractController.
Or even allow the ServiceLocater to locate parameters as well. Never did understand why you could not just Container::get('some_parameter'); I'm sure there are reasons.
@leszek - as I understood the article, you can use the PSR-11 compliant (and therefore framework agnostic?) ContainerBagInterface instead. Is that better?
Hope this feature will stay an "option" and not become mandatory for parameter access.
Like a lot of people, I try to write code that stays "framework agnostic" and injecting the whole ParameterBag makes me feel like my service become dependent on the whole container.
But like said before, that's a great feature for the people who need it.
I can understand this, if the parameters are so raw that they need some kind of preprocessing and there is some combinations between them, so the logic of producing the right input for specific services can be outsourced to such a service. And then this code will need some care when switching between frameworks. Otherwise pushing a bag interface is a bad practice. Similarly to the console commands where the InputInterface is such a thing (the documentation shows how fetching option values and logic is intermixed). The documentation also presents a code where a console command fetches a service in the execute() method instead of becoming it injected. More or less it makes code non-reusable and doesn't follow the ports and adapters (hexagonal) architecture.
Good job implementing this!
Its not the perfect solution because you can always get into the "service locator" trap as mentioned already by others, BUT its definitively a much elegant solution than having to inject this: "@=service('kernel').getContainer().getParameterBag()" or other similar workarounds...
@Andrey Hristov I agree that what we have currently on the table is not perfect, but dont fall in the same trap as I did not so long time ago: expecting everything to happen overnight and see the "perfect symfony" appear out of thin air.
We have to appreciate that there's an ongoing effort from the core contributors to improve the general architecture and to promote decoupling. As always we all can contribute to symfony and push it in the right direction, keep that in mind!
Wouldn’t necessarily do this but if you only want to be given the parameters you need you can do the following:
your_service_params: class: 'Symfony\Component\DependencyInjection\ParameterBag\ParameterBag' calls: - ['add', [{parameter_name: '%kernel.project_dir%'}]]
and inject @your_service_params. Not as elegant though!
Nice job!
Nice!
Great!
Good Job
Nice!