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.
Related
I migrate symfony 3 to symfony 4.3 and my custom bundle moved to: src/Bundles/FooBundle.
Strucutre FooBundle:
- DependencyInjection
- Configuration.php
- FooExtension.php
- Event
- Model
- Resources
- config
- services.yml
- Service
- Twig
- Exception
- FooBundle.php
And files
Bundles/FooBundle/Resources/config/servies.yaml
services:
foo:
class: App\Bundles\FooBundle\Service\Foo
arguments:
- '%foo.argument1%'
- '%foo.argument2%'
- '%foo.argument3%'
foo.listener.example:
class: App\Bundles\FooBundle\Event\Listener\ExampleListener
arguments: ['#annotations.reader']
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
foo.filter:
class: App\Bundles\FooBundle\Filter\FilterConverter
tags:
- { name: request.param_converter }
Bundles/FooBundle/DependencyInjection/Configuration.php
<?php
namespace App\Bundles\FooBundle\DependencyInjection;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
class Configuration implements ConfigurationInterface
{
/**
* {#inheritdoc}
*/
public function getConfigTreeBuilder()
{
$treeBuilder = new TreeBuilder('foo');
$treeBuilder->getRootNode()
->children()
->scalarNode('argument1')->isRequired()->end()
->scalarNode('argument2')->isRequired()->end()
->scalarNode('argument3')->isRequired()->end()
->end();
return $treeBuilder;
}
}
Bundles/FooBundle/DependencyInjection/FooExtension.php
<?php
namespace App\Bundles\FooBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Loader;
/**
* This is the class that loads and manages your bundle configuration.
*
* #link http://symfony.com/doc/current/cookbook/bundles/extension.html
*/
class FooExtension extends Extension
{
/**
* {#inheritdoc}
*/
public function load(array $configs, ContainerBuilder $container)
{
$configuration = new Configuration();
$config = $this->processConfiguration($configuration, $configs);
$container->setParameter('foo.argument1', $config['page_field_name']);
$container->setParameter('foo.argument2', $config['per_page_field_name']);
$container->setParameter('foo.argument3', $config['sort_field_name']);
$loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yml');
}
}
And bundle registration: config/bundles.php
App\Bundles\FooBundle\FooBundle::class => ['all' => true],
configuration package:
config/packages/foo.yam
foo:
argument1: test1
argument2: test2
argument3: test3
Done... Run app and error:
Cannot autowire service "App\Bundles\FooBundle\Service\Foo": argument "$argument1" of method "__construct()" has no type-hint, you should configure its value explicitly.
But, when I add conf in config/services.yaml:
App\Bundles\FooBundle\Service\Foo:
arguments:
- '%foo.argument1%'
- '%foo.argument2%'
- '%foo.argument3%'
That working...
Question: why is the bundle service not working?
I guess the problem is that the default configuration in symfony add autowireing to all classes under the App namespace.
Check the configuration under config/services.yml:
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.
# 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}'
You can remove this lines:
App\:
resource: '../src/*'
exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
And the problem should disappear.
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, ...)
I try to override Exception Controller with Symfony 3
I followed their example here http://symfony.com/doc/current/controller/error_pages.html but it doesn't work
So first I have created my error page in app/Ressources/TwigBundle.../error404.html.twig
And it works without the controller overriding.
Second, I have added in services.yml, under services:
frontBundle\Controller\CustomExceptionController:
public: true
arguments:
$debug: '%kernel.debug%'
Third in my Controller folder, I have created a CustomExceptionController.php
Inside, I have put (I want to override findtemple() for example)
namespace frontBundle\Controller;
use Symfony\Bundle\TwigBundle\Controller\ExceptionController;
use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference;
use Symfony\Component\HttpFoundation\Request;
class CustomExceptionController extends ExceptionController
{
/**
* #param Request $request
* #param string $format
* #param int $code An HTTP response status code
* #param bool $showException
*
* #return TemplateReferenceInterface
*/
protected function findTemplate(Request $request, $format, $code,
$showException)
{
}
}
However it doesn't work. This new controller is not taken into consideration and the findTemplate() is not overrided.
Did I miss something?
There are really not a lot of help about that with Symfony 3...
Thank you so much
If you need override the controller try this:
# app/config/services.yml
services:
_defaults:
# ... be sure autowiring is enabled
autowire: true
# ...
AppBundle\Controller\CustomExceptionController:
public: true
arguments:
$debug: '%kernel.debug%'
and:
# app/config/config.yml
twig:
exception_controller: AppBundle:Exception:showException
Link
Regards
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'
I have the following class:
namespace AppBundle\EventListener;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use FOS\UserBundle\Event\UserEvent;
use FOS\UserBundle\FOSUserEvents;
use FOS\UserBundle\Event\FormEvent;
class UserRegistrationListener implements EventSubscriberInterface
{
protected $logger;
public function __construct($logger)
{
$this->logger = $logger;
}
public static function getSubscribedEvents()
{
return array(
FOSUserEvents::REGISTRATION_INITIALIZE => 'onRegistrationInit',
);
}
/**
* take action when registration is initialized
* set the username to a unique id
* #param \FOS\UserBundle\Event\FormEvent $event
*/
public function onRegistrationInit(UserEvent $userevent)
{
$this->logger->info("Log Something");
$user = $userevent->getUser();
$user->setUsername(uniqid());
}
}
and I have been trying for hours to log something with monolog from within it but have had no luck.
I have read much of the documentation and I believe I need to somehow 'Inject' monolog as a service. What I have read however does not seem to be clear to me.
Some details:
#config_dev.yml
monolog:
channels: [chris]
handlers:
mylog:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%_chris.log"
channels: chris
formatter: monolog.my_line_formatter
.
#services.yml
services:
monolog.my_line_formatter:
class: Monolog\Formatter\LineFormatter
arguments: [~, ~, true]
app.user_registration:
class: AppBundle\EventListener\UserRegistrationListener
arguments: [#logger] ## changed to [#monolog.logger.chris] to us custom channel
tags:
- { name: kernel.event_subscriber }
What do I have to do to get Monolog working with my formatter inside this class?
UPDATE:
#griotteau I have done what you have posted in your answer but I still get an error:
CRITICAL - Uncaught PHP Exception Symfony\Component\Debug\Exception\ContextErrorException: "Warning: Missing argument 1 for AppBundle\EventListener\UserRegistrationListener::__construct(), called in ...filepath...\app\cache\dev\appDevDebugProjectContainer.php on line 384 and defined" at ...filepath...\src\AppBundle\EventListener\UserRegistrationListener.php line 18
SOLVED ERROR I already had a service with the same class (not shown in ym question). #griotteau 's answer is correct.
You can pass arguments when you declare your service
In services.yml :
app.user_registration:
class: AppBundle\EventListener\UserRegistrationListener
arguments: [#logger]
tags:
- { name: kernel.event_subscriber }
In your class, add a constructor :
protected $logger;
public function __construct($logger)
{
$this->logger = $logger;
}
So when you want to add a log :
$this->logger->info(...);