Symfony autowiring Event Listener - symfony

Symfony 4.2.2
To cache all responses in one controller, I'm using an event listener for the kernel.controller event. My Event listener needs a couple of services and info:
EnityManagerInterface
the controller being cached
kernel cache folder
I have set this up like this:
namespace App\Listener;
use App\Controller\DataOutputController;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
class CachedOutput{
protected $cacheFolder;
protected $em;
protected $controller;
public function __construct($cacheFolder, EntityManagerInterface $em, DataOutputController $controller )
{
$this->cacheFolder = $cacheFolder;
$this->em = $em;
$this->controller = $controller;
}
public function findCachedObject(FilterControllerEvent $event, $eventName, TraceableEventDispatcher $dispatcher
){
$params = $event->getRequest()->attributes->get('_route_params');
$fileType = $this->em->getRepository('App:FileType')->find($params->get('fileType'));
$dataSet = $this->controller->getDataSet($params->get('dataSetSearch')?:'latest', $fileType->getType());
$cacheFile = $this->cacheFolder.'/output/DS'.$dataSet->getId().'-FT'.$fileType->getId().'.html';
if (file_exists($cacheFile)){
$fh = fopen($cacheFile,'r');
return new Response(fpassthru($fh));
}
}
}
services:
# default configuration for services in *this* file
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: true # Allows optimizing the container by removing unused services; this also means
# fetching services directly from the container via $container->get() won't work.
# The best practice is to be explicit about your dependencies anyway.
bind:
$projectDir: '%kernel.project_dir%'
# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
controller.return_cached_output:
class: App\Listener\CachedOutput
arguments:
$cacheFolder: "%kernel.cache_dir%"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: findCachedObject }
However, I still get an error regarding the cache folder:
Cannot autowire service "App\Listener\CachedOutput": argument "$cacheFolder" of method "__construct()" has no type-hint, you should configure its value explicitly.
What am I missing?
Update:
Have tried to use alias for the service like this:
App\Listener\CachedOutput:
public: false
arguments:
$cacheFolder: "%kernel.cache_dir%"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: findCachedObject }
return_cached_output:
alias: App\Listener\CachedOutput
public: true
with no success

You need to type-hint in your __construct(string $cacheFolder, ...)

Related

Symfony Dependency injection to class

I have a class in App\Util which needs the MailerInterface Dependency. So I added it directly to the constructor like this:
public function __construct(MailerInterface $mailer)
{
$this->mailer = $mailer;
}
Then I added the argument in the services.yaml:
services:
...
App\Util\OwnerMailValidation:
arguments: ['#mailer']
In the end I used exactly the same code as provided by the symfony documentation but I keep getting the error:
Too few arguments to function App\Util\OwnerMailValidation::__construct(), 0 passed [...] 1 expected
My complete servicey.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
exclude:
- '../src/DependencyInjection/'
- '../src/Entity/'
- '../src/Kernel.php'
- '../src/Tests/'
App\Controller\:
resource: '../src/Controller/'
tags: ['controller.service_arguments']
App\Util\OwnerMailValidation:
class: App\Util\OwnerMailValidation
arguments: ['#mailer']
If you have a single implementation of MailerInterface you shouldn't write anything in your service.yaml
Symfony can figure it out on his own.
If it still doesn't work try to do this
App\Util\OwnerMailValidation:
arguments:
$mailer: '#<the class that implements MailerInteface that you want to inject>'
As #cerad said autowiring works if you inject your class into another.
If you're doing $validation = new OwnerMailValidation(); it just won't work because it requires a MailerInterface in the constructor.
You should inject your OwnerMailValidation into the controller method or into the class constructor.
Try this config:
services:
...
App\Util\OwnerMailValidation:
class: App\Util\OwnerMailValidation

How to inject Doctrine Entity Manager into Symfony 4 Service

I have a controller
use Doctrine\ORM\EntityManagerInterface:
class ExampleController{
public function someFunction(ExampleService $injectedService){
$injectedService->serviceFunction();
}
}
With a Service
use Doctrine\ORM\EntityManagerInterface;
class ExampleService{
public function __construct(EntityManagerInterface $em){
...
}
}
However, calls to someFunction() fail due to 0 parameters being passed (the EntityManagerInterface is not being injected). I am attempting to use the EntityManager from the Service. Autowiring is on. I've tried the solutions for Symfony3 but they don't seem to work unless I'm missing something.
Edit: Here is my services.yaml:
services:
_defaults:
autowire: true
autoconfigure: true
public: false
App\:
resource: '../src/*'
exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'
App\Controller\:
resource: '../src/Controller'
tags: ['controller.service_arguments']
Use only in Symfony 4.
use Doctrine\ORM\EntityManagerInterface;
use App\Entity\Name; //if you use entity for example Name
class ExampleService{
private $em;
public function __construct(EntityManagerInterface $em)
{
$this->em = $em;
}
function newName($code) // for example with a entity
{
$name = new Name();
$name->setCode($code); // use setter for entity
$this->em->persist($name);
$this->em->flush();
}
}
I know it's an old post, but just in case somebody struggles with this, there's a typo in the use statment:
use Doctrine\ORM\EntityManagerInterface: //<- see that's a colon, not a semicolon
Agree with Yarimadam. Service container, dependency injection and autowiring is not a story about injecting into methods. Dependencies injected into objects we are calling "services".
When application is up, service container is built injecting one services into another ones via class constructor or "set" method invocation.
Your ExampleController::someFunction is intended to be called only by you, so only way how this method will receive $injectedService as an argument, is that you will pass it evidently. This is the wrong way.
A classic symfony service with autowiring uses constructor injection method to inject dependencies. In your case, you don't have a constructor.
You may consider to add a constructor method and set dependency to a private class property. And use accordingly.
Or you can utilize setter injection.
Service Configuration:
services:
app.example_controller:
class: Your\Namespace\ExampleController
calls:
- [setExampleService, ['#exampleService']]
Controller Class:
class ExampleController
{
private $exampleService;
public function someFunction() {
$this->exampleService->serviceFunction();
}
public function setExampleService(ExampleService $exampleService) {
$this->exampleService = $exampleService;
}
}

Symfony autowire #required method with event dispatcher never called

Considering this following service class:
namespace AppBundle\Listener\Entity;
use AppBundle\Entity\Payment;
use AppBundle\Event\PaymentEvent;
use AppBundle\Event\PaymentEvents;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class PaymentEntityListener
{
/**
* #var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* #required
*
* #param EventDispatcherInterface $eventDispatcher
*/
public function setEventDispatcher(EventDispatcherInterface $eventDispatcher)
{
dump($eventDispatcher);
$this->eventDispatcher = $eventDispatcher;
}
/**
* #param Payment $payment
*/
public function postPersist(Payment $payment)
{
$this->eventDispatcher->dispatch(PaymentEvents::ADD, new PaymentEvent($payment));
}
}
The method PaymentEntityListener::setEventDispatcher should be call with the EventDispatcherInterface matched service.
This method is correctly configured according to the debug command:
$ ./bin/console debug:container --show-private AppBundle\\Listener\\Entity\\PaymentEntityListener
Information for Service "AppBundle\Listener\Entity\PaymentEntityListener"
=========================================================================
---------------- -------------------------------------------------
Option Value
---------------- -------------------------------------------------
Service ID AppBundle\Listener\Entity\PaymentEntityListener
Class AppBundle\Listener\Entity\PaymentEntityListener
Tags -
Calls setEventDispatcher
Public no
Synthetic no
Lazy no
Shared yes
Abstract no
Autowired yes
Autoconfigured yes
---------------- -------------------------------------------------
But this method is never called.
Here is my services.yml:
imports:
- { resource: legacy_aliases.yml }
services:
_defaults:
autowire: true
autoconfigure: true
public: false
_instanceof:
Doctrine\ORM\Decorator\EntityManagerDecorator:
public: true
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository}'
AppBundle\Controller\:
resource: '../../src/AppBundle/Controller'
tags: ['controller.service_arguments']
PowerDNSBundle\:
resource: '../../src/PowerDNSBundle/*'
exclude: '../../src/PowerDNSBundle/{Entity,Repository}'
PowerDNSBundle\Doctrine\ORM\PowerDNSEntityManager:
arguments:
$wrapped: '#doctrine.orm.powerdns_entity_manager'
I don't know why the method is not called and why Symfony does not throw any exception.
FYI, the following command:
./bin/console debug:container --show-private Symfony\\Component\\EventDispatcher\\EventDispatcherInterface
Does return a service match.
Where can be the issue?
Thanks for your help.
I finally found why: This listener didn't have the correct tag:
AppBundle\Listener\Entity\:
resource: '../../src/AppBundle/Listener/Entity'
tags: ['doctrine.orm.entity_listener']
Because of that, I guess the service is not recognized by Doctrine and this one try to create the listener itself. But in this case, the method will never be called.

Symfony 3.3.5 config service

I have some class ObjectManager and want create service for him, I create config
services:
_defaults:
autowire: true
autoconfigure: true
public: false
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
AppBundle\Service\:
resource: '../../src/AppBundle/Service'
public: true
app.object_manager:
class: AppBundle\Service\ObjectManager
arguments:
- '#jms_serializer'
- '#validator'
and after get this service in controller and have error
$objectManager = $this->get('app.object_manager');
You have requested a non-existent service "app.object_manager".
Why this happened, symfony 3.3.5 have different way for config services ?
First of all, when you want to be able to call a service with get, you have to define it public.
app.object_manager:
class: AppBundle\Service\ObjectManager
public: true
But you can do that also in top of the services.yml, to define, that all services are public:
services:
_defaults:
autowire: true
autoconfigure: true
public: true
That way, you don't have to do it explicitly at every service.
Furthermore, I would recommend you, to use the new autowiring feature of Symfony. That way, you do not have to define a service in the services.yml.
Just define the constructor in your ObjectManager class like that:
class ObjectManager
{
private $jms_serializer;
private $validator;
public function __construct(SerializerInterface $jms_serializer, ValidatorInterface $validator)
{
$this->jms_serializer = $jms_serializer;
$this->validator = $validator;
}
}
He will find the correct services by autowiring then. And you do not have to define anything in the services.yml.
So, to sum up your services.yml should just contain the following:
services:
_defaults:
autowire: true
autoconfigure: true
public: true
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
Looks like you need to explicitly define your services as public since Symfony 3 if you want to use them through get().
# app/config/services.yml
services:
# default configuration for services in *this* file
_defaults:
# ...
public: false
More info here - http://symfony.com/doc/current/service_container.html#public-versus-private-services
You can actually use Symfony 3.3+ to it's full potential.
If I understand you correctly, you need to get AppBundle\Service\ObjectManager to your Controller.
In that case...
1. Modify your services.yml
services:
_defaults:
autowire: true
AppBundle\:
resource: '../../src/AppBundle/*'
exclude: '../../src/AppBundle/{Entity,Repository,Tests}'
2. Require AppBundle\Service\ObjectManager via constructor (anywhere you need it)
use AppBundle\Service\ObjectManager;
final class MyController
{
/**
* #var ObjectManager
*/
private $objectManager;
public function __construct(ObjectManager $objectManager)
{
$this->objectManager = $objectManager;
}
public function someAction()
{
$this->objectManager->someCoolMethod();
}
}
That's all!
Nothing fancy, just pure Symfony awesomes!

Symfony autowiring monolog channels

Following this documentation, I can create many channels which will create services with the following name monolog.logger.<channel_name>
How can I inject these services into my service with DI injection and autowiring ?
class FooService
{
public function __construct(LoggerInterface $loggerInterface) { }
}
Yaml
#existing
foo_service:
class: AppBundle\Services\FooService
arguments: ["#monolog.logger.barchannel"]
# what I want to do
foo_service:
autowire: true # how to inject #monolog.logger.barchannel ?
Starting from MonologBundle 3.5 you can autowire different Monolog
channels by type-hinting your service arguments with the following
syntax: Psr\Log\LoggerInterface $<channel>Logger. For example, to
inject the service related to the app logger channel use this:
public function __construct(LoggerInterface $appLogger)
{
$this->logger = $appLogger;
}
https://symfony.com/doc/current/logging/channels_handlers.html#monolog-autowire-channels
I wrote (maybe more complicated) method. I don't want to tag my autowired services to tell symfony which channel to use.
Using symfony 4 with php 7.1.
I built LoggerFactory with all additional channels defined in monolog.channels.
My factory is in bundle, so in Bundle.php add
$container->addCompilerPass(
new LoggerFactoryPass(),
PassConfig::TYPE_BEFORE_OPTIMIZATION,
1
); // -1 call before monolog
This is important to call this compiler pass before monolog.bundle because monolog after pass removes parameters from container.
Now, LoggerFactoryPass
namespace Bundle\DependencyInjection\Compiler;
use Bundle\Service\LoggerFactory;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class LoggerFactoryPass implements CompilerPassInterface
{
/**
* You can modify the container here before it is dumped to PHP code.
* #param ContainerBuilder $container
* #throws \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* #throws \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException
*/
public function process(ContainerBuilder $container): void
{
if (!$container->has(LoggerFactory::class) || !$container->hasDefinition('monolog.logger')) {
return;
}
$definition = $container->findDefinition(LoggerFactory::class);
foreach ($container->getParameter('monolog.additional_channels') as $channel) {
$loggerId = sprintf('monolog.logger.%s', $channel);
$definition->addMethodCall('addChannel', [
$channel,
new Reference($loggerId)
]);
}
}
}
and LoggerFactory
namespace Bundle\Service;
use Psr\Log\LoggerInterface;
class LoggerFactory
{
protected $channels = [];
public function addChannel($name, $loggerObject): void
{
$this->channels[$name] = $loggerObject;
}
/**
* #param string $channel
* #return LoggerInterface
* #throws \InvalidArgumentException
*/
public function getLogger(string $channel): LoggerInterface
{
if (!array_key_exists($channel, $this->channels)) {
throw new \InvalidArgumentException('You are trying to reach not defined logger channel');
}
return $this->channels[$channel];
}
}
So, now you can inject LoggerFactory, and choose your channel
public function acmeAction(LoggerFactory $factory)
{
$logger = $factory->getLogger('my_channel');
$logger->log('this is awesome!');
}
After some searching I have found some kind of workaround using tags and manually injecting several parameters to autowired service.
My answer looks similar to #Thomas-Landauer. The difference is, I do not have to manually create logger service, as the compiler pass from monolog bundle does this for me.
services:
_defaults:
autowire: true
autoconfigure: true
AppBundle\Services\FooService:
arguments:
$loggerInterface: '#logger'
tags:
- { name: monolog.logger, channel: barchannel }
You can use the bind parameter:
services:
_defaults:
autowire: true # Automatically injects dependencies in your services.
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
public: true
bind:
$loggerMyApi: '#monolog.logger.my_api'
Then you can use it in your service's constructor:
use Psr\Log\LoggerInterface;
...
public function __construct(LoggerInterface $loggerMyApi)
{
...
}
I didn't find a way to autowire the very logger channel. However, I found a way to use autowire in principle, and inject just the logger manually. With your class FooService, this is how services.yml could look like (Symfony 3.3):
# services.yml
services:
_defaults:
autowire: true
autoconfigure: true
AppBundle\Services\FooService:
arguments:
$loggerInterface: '#monolog.logger.barchannel'
So the "trick" is to inject the logger channel explicitly, while still having all other dependencies of this service injected through autowiring.
From the documentation it is now possible to autowire based on the type hinting of the argument name.
// autowires monolog with "foo" channel
public function __construct(\Psr\Log\LoggerInterface $fooLogger);
Essentially, you've got two options:
First, service tagging:
services:
App\Log\FooLogger:
arguments: ['#logger']
tags:
- { name: monolog.logger, channel: foo }
Then you can use your CustomLogger as a dependency elsewhere
Second, you can rely on Monolog to auto-register loggers for each custom channel within the configuration:
# config/packages/prod/monolog.yaml
monolog:
channels: ['foo', 'bar']
You will then have these services available: monolog.logger.foo, 'monolog.logger.bar'
You can then retrieve them from the service container, or wire them in manually, e.g:
services:
App\Lib\MyService:
$fooLogger: ['#monolog.logger.foo']
You can read more here and here.
Recently I was implement single point access to the all registered loggers by MonologBundle.
And also I tried to do some better solution - and did auto-generated logger decorators. Each class decorates one object of one of the registered monolog channel.
Link to the bundle adrenalinkin/monolog-autowire-bundle
For those still struggling with this one.
In Symfony 4.3, I had, on top of that, add an alias for the specific channel, because without that, it was working only on the dev environment : when building, the Unit Tests were all failing because the custom logger was an undefined service.
monolog.logger.my_custom_logger:
alias: Psr\Log\LoggerInterface
public: true
App\Logger\MyLogger:
arguments:
$logger: '#monolog.logger.my_custom_logger'

Resources