Symfony autowiring monolog channels - symfony

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'

Related

Symfony autowiring Event Listener

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, ...)

Symfony - Setup the TokenController with LexikJWTAuthenticationBundle

I'm using LexikJWTAuthenticationBundle https://github.com/lexik/LexikJWTAuthenticationBundle/blob/master/Resources/doc/index.md#installation
I'm setting up my controller to get a Token:
class TokenController extends AbstractController
{
/**
* #Route("/api/token", name="token", methods={"POST"})
* #param Request $request
* #param JWTEncoderInterface $JWTEncoder
* #return JsonResponse
* #throws \Lexik\Bundle\JWTAuthenticationBundle\Exception\JWTEncodeFailureException
*/
public function token(Request $request, JWTEncoderInterface $JWTEncoder)
{
$user = $this->getDoctrine()->getRepository(User::class)->findOneBy([
'email' => $request->getUser(),
]);
if (!$user) {
throw $this->createNotFoundException('User Not Found');
}
$isValid = $this->get('security.password_encoder')
->isPasswordValid($user, $request->getPassword());
if (!$isValid) {
throw new BadCredentialsException();
}
$token = $JWTEncoder->encode([
'email' => $user->getEmail(),
'exp' => time() + 3600 // 1 hour expiration
]);
return new JsonResponse(['token' => $token]);
}
}
But I have this error:
Service "security.password_encoder" not found: even though it exists
in the app's container, the container inside
"App\Controller\TokenController" is a smaller service locator that
only knows about the "doctrine", "form.factory", "http_kernel",
"parameter_bag", "request_stack", "router",
"security.authorization_checker", "security.csrf.token_manager",
"security.token_storage", "serializer", "session" and "twig" services.
Unless you need extra laziness, try using dependency injection
instead. Otherwise, you need to declare it using
"TokenController::getSubscribedServices()".
I have use Dependency injection and here is my service conf
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: false # 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.
# 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']
Where is the problem?
You are extending from AbstractController, using this controller, services that you access using $this->get() will be limited. To access the password encoder service you can inject Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface into your controller action or through the controller class constructor.
private $passwordEncoder;
public function __construct(UserPasswordEncoderInterface $passwordEncoder)
{
$this->passwordEncoder = $passwordEncoder;
}
...
$this->passwordEncoder->isPasswordValid()
...
Or you can extend from Symfony\Bundle\FrameworkBundle\Controller\Controller for full container access. $this->get('security.password_encoder') should work with this.

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;
}
}

Dependency on a non-existent service "templating"

I tried to use
# app/config/services.yml
services:
project.controller.some:
class: Project\SomeBundle\Controller\SomeController
arguments: ['#templating']
and
namespace Project\SomeBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
class SomeController
{
private $templating;
public function __construct(EngineInterface $templating)
{
$this->templating = $templating;
}
public function indexAction()
{
return $this->templating->render(
'SomeBundle::template.html.twig',
array(
)
);
}
}
in Symfony 4 flex. Now I get the error
ServiceNotFoundException
The service "project.controller.some" has a dependency on a non-existent service "templating".
Please tell me how to solve this. My composer.json already contains "symfony/templating": "^4.0" but this seems not to be enough.
Symfony 4 doesn't include Twig by default, so you need to install it first:
composer require twig
should do the trick. Also, with service autowiring in Symfony 4 you don't need to manually declare it in the services.yml.
Another solution is to add the configuration under framework as explained in the doc
# app/config/packages/framework.yaml
framework:
# ...
templating: { engines: ['twig'] }
With Symfony 4 you can also use new DI features (already available since Symfony 3.3):
_defaults
class named service
They will simplify all to:
# app/config/services.yml
services:
_defaults:
autowired: true
Project\SomeBundle\Controller\SomeController: ~
If you want to know more with real before/after examples, read How to refactor to new Dependency Injection features in Symfony 3.3

How to have a global variable coming from db in symfony template?

How can I have a global variable in symfony template?
I did read this
but I prefer to fetch parameter from database, I think this service will be loaded on startup before it can fetch anything from db. Is it possible to do a trick to do so?
EDIT: Update in 2019 with Symfony 3.4+ syntax.
Create a Twig extension where you inject the Entity Manager:
Fuz/AppBundle/Twig/Extension/DatabaseGlobalsExtension.php
<?php
namespace Fuz\AppBundle\Twig\Extension;
use Doctrine\ORM\EntityManager;
use Twig\Extension\AbstractExtension;
use Twig\Extension\GlobalsInterface;
class DatabaseGlobalsExtension extends AbstractExtension implements GlobalsInterface
{
protected $em;
public function __construct(EntityManager $em)
{
$this->em = $em;
}
public function getGlobals()
{
return [
'myVariable' => $this->em->getRepository(FuzAppBundle\Entity::class)->getSomeData(),
];
}
}
Register your extension in your Fuz/AppBundle/Resources/config/services.yml:
services:
_defaults:
autowire: true
autoconfigure: true
Fuz\AppBundle\Twig\Extension\DatabaseGlobalsExtension: ~
Now you can do the requests you want using the entity manager.
Don't forget to replace paths and namespaces to match with your application.
As of this day, the class signature has changed. You must implement \ Twig_Extension_GlobalsInterface, without it, your globals won't show up.
class MyTwigExtension extends \Twig_Extension implements Twig_Extension_GlobalsInterface
{ }
Bye!
you can register a twig extension
services:
twig_extension:
class: Acme\DemoBundle\Extension\TwigExtension
arguments: [#doctrine]
tags:
- { name: twig.extension }
And then in the TwigExtension you can do as follows:
class TwigExtension extends \Twig_Extension
{
public function getGlobals() {
return array(
// your key => values to make global
);
}
}
So you could get a value from the database in this TwigExtension and pass it to the template with the getGlobals() function
Stay away from global variables.
Instead make a custom twig extension then inject the database connection as a parameter.
Something like:
services:
acme.twig.acme_extension:
class: Acme\DemoBundle\Twig\AcmeExtension
arguments: [#doctrine.dbal.default_connection]
tags:
- { name: twig.extension }
Details:
http://symfony.com/doc/current/cookbook/templating/twig_extension.html

Resources