How can I inject callable into Symfon2 Dependency Injection Container service - symfony

Given the the following class definition
class CacheSubscriber implements SubscriberInterface
{
public function __construct(
CacheStorageInterface $cache,
callable $canCache
) {
$this->storage = $cache;
$this->canCache = $canCache;
}
I want to define this class as a service in Symfony2 DIC.
While it is relatively clear what to do with the first argument
<service id="nsp_generic.guzzle.subscriber.cache" class="GuzzleHttp\Subscriber\Cache\CacheSubscriber">
<argument type="service" id="nsp_generic.redis.cache"/>
<!-- ??? -->
</service>
How can I define and inject the second argument?
UPDATE
User meckhardt pushed this into the right direction.
Helper class
<?php
namespace Nsp\GenericBundle\Service\Cache;
class CacheConfig {
public static function canCache() {
return true;
}
}
Service defintion
<service id="nsp_generic.guzzle.subscriber.cache" class="GuzzleHttp\Subscriber\Cache\CacheSubscriber">
<argument type="service" id="nsp_generic.guzzle.subscriber.cache.storage"/>
<argument type="collection">
<argument>Nsp\GenericBundle\Service\Cache\CacheConfig</argument>
<argument>canCache</argument>
</argument>
</service>
Thanks Martin!

Try
<argument type="collection">
<argument>ClassName::staticMethod</argument>
</argument>
or
<argument type="collection">
<argument type="service" id="serviceId" />
<argument>instanceMethodName</argument>
</argument>
http://php.net/manual/de/language.types.callable.php

Maybe create an dependency injection factory like in this example: docs
which will return your callable.

Related

Set Argument to Service

Symfony 2.8
Having a service factory, I want to pass a simple 'string' argument when call/invoke/get (however you say it) the service.
<services>
<service id="service.builder_factory" class="Domain\Bundle\Services\ServiceBuilder">
<argument name="optional" type="string"/>
<argument type="service" id="request_stack"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="doctrine.orm.entity_manager"/>
<argument key="dir">%kernel.logs_dir%</argument>
</service>
<service id="reports" class="Domain\Bundle\Services\ReportsService">
<factory service="service.builder_factory" method="__factoryMethod"/>
<tag name="service.builder"/>
</service>
</services>
And I don't know how to set that string as a parameter.
$this->getContainer()->get('reports')->setParameter('optional', 'string_to_pass');
Service factory works, but I need to pass a parameter from controller or command.
You cannot set a parameter to a service you got from the service container. It have been instantiated already. Factory service in Symfony configuration will not help you in this case too.
You can use Prototype pattern for example: get your "not configured" service from a container, clone and configure it:
class MyService {
private $reportPrototype;
public function __construct(Report $reportPrototype) // your 'report' service
{
$this->reportPrototype = $reportPrototype;
}
public function someMethod() {
$report = $this->getReport('optional');
}
protected function getReport(string $optional)
{
$result = clone $this->reportPrototype;
$result->setOptional($optional); // configure your service
return $result;
}
}
services.xml:
<services>
<service id="reports" class="Domain\Bundle\Services\ReportsService">
<argument type="service" id="request_stack"/>
<argument type="service" id="event_dispatcher"/>
<argument type="service" id="doctrine.orm.entity_manager"/>
<argument key="dir">%kernel.logs_dir%</argument>
</service>
<service id="my_service" class="MyService">
<argument type="service" id="reports"/>
</service>
</services>

Symfony2 FOSRest controller as service has an empty container

I have registered my controller as a service so I can have my repository injected into my controller. This all seems to work fine, except that now when i try to return the view it bugs on returning data.
It gives an error and tries to load fos_rest.view_handler:
Error: Call to a member function get() on a non-object
The get is being called in the symfony2 controller class on $this->container->get($id). For some reason the ContainerInterface is not being injected in the ContainerAware anymore when I use my controller as a service.
Has anyone experienced this problem before? How can I make sure the same container gets injected?
This is how I declared my class as a service:
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="acme.users.apibundle.controller.user_controller" class="Acme\Users\ApiBundle\Controller\UserController">
<argument type="service" id="acme.users.user_repository"/>
</service>
</services>
</container>
And this is my controller:
class UserController extends FOSRestController
{
private $repository;
public function __construct(UserRepository $repository)
{
$this->repository = $repository;
}
public function indexAction()
{
$users = $this->repository->findAll();
$view = $this->view($users, 200)
->setTemplate("MyBundle:Users:getUsers.html.twig")
->setTemplateVar('users');
return $this->handleView($view);
}
}
You need to inject the container into your controller using a call so that it is available in the handleView method.
Change your config to..
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="acme.users.apibundle.controller.user_controller" class="Acme\Users\ApiBundle\Controller\UserController">
<argument type="service" id="acme.users.user_repository"/>
<!-- inject the container via the setContainer method -->
<call method="setContainer">
<argument type="service" id="service_container" />
</call>
</service>
</services>

Symfony2 service container: Inject array of services parameter as an argument to another service using XML

I have a parameter which should represent array of services in my services.xml file:
<parameters>
<parameter key="decorators.all" type="collection">
<parameter type="service" id="decorator1" />
<parameter type="service" id="decorator2" />
<parameter type="service" id="decorator3" />
</parameter>
</parameters>
<services>
<service id="decorator1" class="\FirstDecorator" />
<service id="decorator2" class="\SecondDecorator" />
<service id="decorator3" class="\ThirdDecorator" />
</services>
Now I want to inject this collection to another service as an array of services:
<services>
<service id="notifications_decorator" class="\NotificationsDecorator">
<argument>%decorators.all%</argument>
</service>
</services>
But it doesn't work. Can't understand why. What am I missing?
So, you inject array of parameters no array of services. You can inject service by service via:
<services>
<service id="notifications_decorator" class="\NotificationsDecorator">
<argument type="service" id="decorator1"/>
<argument type="service" id="decorator2"/>
<argument type="service" id="decorator3"/>
</service>
</services>
Or (in my opinion better way) tag decorators services and inject them to notifications_decorator during compilation passes.
UPDATE: Working with Tagged Services
In your case you have to modify your services like this:
<services>
<service id="decorator1" class="\FirstDecorator">
<tag name="acme_decorator" />
</service>
<service id="decorator2" class="\SecondDecorator">
<tag name="acme_decorator" />
</service>
<service id="decorator3" class="\ThirdDecorator">
<tag name="acme_decorator" />
</service>
</services>
Additionaly you should remove decorators.all parameter from <parameters> section. Next, you have to add sth like addDectorator function for \NotificationsDecorator:
class NotificationsDecorator
{
private $decorators = array();
public function addDecorator($decorator)
{
$this->decorators[] = $decorator;
}
// more code
}
It would be great if you write some interface for decorator's and add this as type of $decorator for addDecorator function.
Next, you have to write own compiler pass and ask them about tagged services and add this services to another one (simillarly to doc):
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
class DecoratorCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition('notifications_decorator')) {
return;
}
$definition = $container->getDefinition('notifications_decorator');
$taggedServices = $container->findTaggedServiceIds('acme_decorator');
foreach ($taggedServices as $id => $attributes) {
$definition->addMethodCall(
'addDecorator',
array(new Reference($id))
);
}
}
}
Finally, you should add your DecoratorCompilerPass to Compiler in your bundle class like:
class AcmeDemoBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
parent::build($container);
$container->addCompilerPass(new DecoratorCompilerPass());
}
}
Good luck!
Little bit different approach with tagged services (or whatever you need) and CompilerPassInterface using array of services instead of method calls.
Here are the differences from #NHG answer:
<!-- Service definition (factory in my case) -->
<service id="example.factory" class="My\Example\SelectorFactory">
<argument type="collection" /> <!-- list of services to be inserted by compiler pass -->
</service>
CompilerPass:
/*
* Used to build up factory with array of tagged services definition
*/
class ExampleCompilerPass implements CompilerPassInterface
{
const SELECTOR_TAG = 'tagged_service';
public function process(ContainerBuilder $container)
{
$selectorFactory = $container->getDefinition('example.factory');
$selectors = [];
foreach ($container->findTaggedServiceIds(self::SELECTOR_TAG) as $selectorId => $tags) {
$selectors[] = $container->getDefinition($selectorId);
}
$selectorFactory->replaceArgument(0, $selectors);
}
}
in yaml you can do:
app.example_conditions:
class: AppBundle\Example\Conditions
arguments:
[[ "#app.example_condition_1", "#app.example_condition_2", "#app.example_condition_3", "#app.example_condition_4" ]]
and in AppBundle\Example\Conditions you receive the array...
Symfony 5.3+ and php 8.0+
class NotificationsDecorator
{
private $decorators;
public function __construct(
#[TaggedIterator('app.notifications_decorators')] iterable $decorators
) {
$this->decorators = $decorators;
}
}
#[Autoconfigure(tags: ['app.notifications_decorators'])]
interface DecoratorInterface
{
}
class FirstDecorator implements DecoratorInterface
{
}

Symfony2 - Different services, same arguments - how to avoid duplication

I have one controller defined as service and it has different arguments.
It's something like this:
<service id="my.controller" class="%my.controller.class%">
<argument type="service" id="form.factory"/>
<argument type="service" id="templating"/>
<argument type="service" id="router"/>
<argument type="service" id="validator"/>
<call method="setEntityManager">
<argument type="service" id="doctrine.orm.entity_manager" />
</call>
<call method="getExpenseRepository">
<argument>Expense</argument>
</call>
</service>
Now I need another controller, which will use the same arguments like the one above. What to do to avoid writing this again with only changing the service id and class?
And one more thing - in the first controller I have:
private $formFactory;
private $templating;
private $router;
private $validator;
public function __construct($formFactory, $templating, $router, $validator)
{
$this->formFactory = $formFactory;
$this->templating = $templating;
$this->router = $router;
$this->validator = $validator;
}
Can I avoid rewriting it in the second one?
Thanks very much in advance! :)
You can create an abstract parent service and inherit the other services from it, a sample config would be
<service id="my.parentcontroller" class="%my.parentcontroller.class%" abstract="true">
<argument type="service" id="form.factory"/>
<argument type="service" id="templating"/>
<argument type="service" id="router"/>
<argument type="service" id="validator"/>
<call method="setEntityManager">
<argument type="service" id="doctrine.orm.entity_manager" />
</call>
<call method="getExpenseRepository">
<argument>Expense</argument>
</call>
</service>
<service id="my.controller1" class="%my.controller1.class%" parent="my.parentcontroller"/>
<service id="my.controller2" class="%my.controller2.class%" parent="my.parentcontroller"/>
Also your controllers classes should inherit from the abstract parent controller.
You can create and specify a common parent service to reduce repetition :
http://symfony.com/doc/current/components/dependency_injection/parentservices.html
You can also define an abstract controller class.
Your child controllers will only need to extends this class and call the parent service.

Symfony 2 registering a doctrine listener not working

Services.xml file:
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<service id="task.task_history_insertion" class="Acme\Bundle\EventListener\TaskHistoryInsertion">
<argument type="service" id="service_container" />
<tag name="doctrine.event_listener" event="postPersist" method="postPersist"/>
</service>
</services>
</container>
TaskHistoryInsertion.php
class TaskHistoryInsertion implements EventSubscriber
{
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function getSubscribedEvents()
{
return array(
Event::postPersist
);
}
public function postPersist(LifecycleEventArgs $args)
{
//not being called
}
}
Any ideas on why postPersist isn't being called after persisting?
Make sure you use the right tag for your service. You need to use doctrine.event_subscriber :
<service id="task.task_history_insertion" class="Acme\Bundle\EventListener\TaskHistoryInsertion">
<argument type="service" id="service_container" />
<tag name="doctrine.event_subscriber"/>
</service>
You are mixing event subscriber and event listener!
I would go for a event listener:
Remove
implements EventSubscriber
and
public function getSubscribedEvents()
{
return array(
Event::postPersist
);
}
Make sure you use
use Doctrine\ORM\Event\LifecycleEventArgs;
and that the services.xml gets loaded in src/Acme/Bundle/DependencyInjection/AcmeExtension.php.
Clear the cache and it should work.
The official documentation can be found at
http://symfony.com/doc/current/cookbook/doctrine/event_listeners_subscribers.html
When you want to implement eventListener - you should name the method in your listener class exactly like an event. In your example - you should have public method called postPersist. And the listener class shouldn't implement EventSubscriber.
There is a link that can give you much clearer picture about this topic http://docs.doctrine-project.org/en/latest/reference/events.html

Resources