How to use custom HandlerWrapper for Monolog in Symfony config.yml? - php

I would like to create a custom HandlerWrapper to use it with Monolog in my Symfony 2.8 project.
The goal is to use this CustomHandler as a filter that decides wether the wrapped/nested handler is called or not.
==============
UPDATE: The following question is already solved thanks to the answer of #Yonel. However this brought ab a new problem described below.
Extending Monolog\Handler\HandlerWrapper is no problem of course. But I am struggling to us the custom class in the Monolog config within config_prod.yml:
namespace AppBundle\Log;
use Monolog\Handler\HandlerWrapper;
class CustomHandler extends HandlerWrapper {
public function handle(array $record) {
// some custom handling/processing...
if ($this->doSomeCheck())
return $this->handler->handle($record);
else
return false; // Do not call nested handler
}
}
Config:
services:
monolog.custom_handler.service:
class: AppBundle\Log\CustomHandler
monolog:
main:
type: service
id: monolog.custom_handler.service
level: error
handler: someHandler
someHandler:
...
Problem: When running app/console cache:warmup on this config, I get the following error:
[Symfony\Component\Debug\Exception\ContextErrorException]
Catchable Fatal Error: Argument 1 passed to
Monolog\Handler\GroupHandler::__construct() must be of the type array,
none given
Well, I think the source of the problem is obvious: The service definition of monolog.custom_handler.service does not pass any argument to the class. But how can I pass someHandler as argument, as defined in the Monolog config?
==============
EDIT: New Problem
As Yonel describes in his answer, on can use the following config to pass someHandler to custom_handler:
services:
monolog.custom_handler.service:
class: AppBundle\Log\CustomHandler
arguments:
- '#monolog.handler.testHandler'
monolog.test_handler.service:
class: AppBundle\Log\TestHandler
monolog:
# Skip level checking (can be solved by adding a FingersCrossed handler)
main:
type: service
id: monolog.custom_handler.service
testHandler:
type: service
id: monolog.test_handler.service
nested: true
This works: TestHandler is now correctly passed to CustomHandler
However the goal was to let CustomHandler decide, wether TestHandler is called or not. This does not work with this config: TestHandler is called anyway (I assume directly by Monolog, as any other handler). It does not make any difference if TestHandler is passed to CustomHandler, marked as nested or not.
I implemented CustomHandler as HandlerWrapper and TestHandler as AbstractHandler. Both classes directly to a log file (not using Monolog) when their handle method is called. This way I can check if the handlers work as expected (TestHandler should only be called if CustomHandler allows it). This is not the case.
No matter if TestHandler is passed to CustomHandler, if it is marked as nested, if CustomHandler returns true or false in its handle method, etc., the result is always the same: TestHandler is called, no matter what CustomHandler decides.
How to solve this?
Handler implementations:
namespace AppBundle\Log;
use Monolog\Handler\HandlerWrapper;
class CustomHandler extends HandlerWrapper {
public function handle(array $record) {
// some custom handling/processing...
if ($this->doSomeCheck()) {
$this->directlyWriteToFileNotUsingMonolog('CustomHandler: Call TestHandler');
return $this->handler->handle($record);
else {
$this->directlyWriteToFileNotUsingMonolog('CustomHandler: Do NOT call TestHandler');
return false; // Do not call nested handler
}
}
class TestHandler extends AbstractHandler {
public function handle(array $record) {
$this->directlyWriteToFileNotUsingMonolog('TestHandler');
return false;
}
}

Related

Symfony 3.4 - Auto wire of service not working in method

I'm relatively new to Symfony, and I'm having trouble some trouble.
I'm trying to type hint a custom RequestValidator class in the method being called when the endpoint is called.
Using Symfony 3.4
However, I am getting the following error:
Controller "ApiBundle\Endpoints\Healthcheck\v1\Index::check()" requires that you provide a value for the "$request" argument. Either the argument is nullable and no null value has been provided, no default value has been provided or because there is a non optional argument after this one.
Here is my setup:
services.yml file
...
_defaults:
autowire: true
autoconfigure: true
...
routing.yml
api.Healthcheck:
path: /healthcheck
controller: ApiBundle\Endpoints\Healthcheck\v1\Index::check
defaults: { _format: json }
methods:
- GET
And then - inside the Index class, I have the following:
<?php
namespace ApiBundle\Endpoints\Healthcheck\v1;
use ApiBundle\Responses\ApiResponse;
class Index extends ApiResponse
{
public function check(HealthcheckRequest $request) {
var_dump($request);die;
}
}
When I do debug:autowiring I see my HealthcheckRequest in the list.
Further, when I do the same and try type-hint in the constructor of the Index class, it all works.
And finally, if I try and type hint the Symfony/HttpFoundation/Request, inside the check() method, it instantiates it correctly.
In summary:
Not working :
check(HealthcheckRequest $request)
Working:
__construct(HealtcheckRequest $request)
check(SymfonyRequest $request)
Am I doing something wrong? Any help is appreciated.
It's part of services.yaml already in Symfony 4, but introduced in version 3.3, so this might help:
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
ApiBundle\Endpoints\:
resource: '../../Endpoints/*'
tags: ['controller.service_arguments']

Relying on service auto-registration error on ORM entity

I am developing a Symfony 3 application. Symfony profiler logs tell me:
Relying on service auto-registration for type "App\Entity\SubDir\Category"
is deprecated since version 3.4 and won't be supported in 4.0.
Create a service named "App\Entity\SubDir\Category" instead.
Yet, this is a simple ORM bean:
/**
* #ORM\Entity
* #ORM\Table(name="category")
*/
class Category
{
...
How should I get rid of this issue? Do I really need to declare ORM entities as services in services.yaml? If yes, how?
Update
In fact, my entity is in a sub directory. I have amended my question.
In my service.yaml, I have tried:
App\:
resource: '../src/*'
exclude: '../src/{Entity,Repository,Tests,Entity/SubDir}'
...but to no avail.
Do you have any Classes under Service-auto registration which use an Entity as constructor argument?
That's where your problem comes from.
You need to ask yourself if the concerning class really is a service or just a plain object of which you always create the instance yourself.
If it is not used as a service through the container you have 2 options:
You can exclude this class also through the glob pattern like for example
AppBundle\:
resource: '...'
# you can exclude directories or files
# but if a service is unused, it's removed anyway
exclude: '../../{Entity,PathToYourNotService}'
or you can set the following parameter in your config
parameters:
container.autowiring.strict_mode: true
with this option the container won't try to create a service class with arguments that are not available as services and you will get a decisive error. This is the default setting for sf4
A good example for a class that triggers exactly this error would be a custom event class that takes an entity as payload in the constructor:
namespace AppBundle\Event;
use AppBundle\Entity\Item;
use Symfony\Component\EventDispatcher\Event;
class ItemUpdateEvent extends Event
{
const NAME = 'item.update';
protected $item;
public function __construct(Item $item)
{
$this->item = $item;
}
public function getItem()
{
return $this->item;
}
}
Now if this file isn't excluded specifically the container will try to auto register it as service. And because the Entity is excluded it can't autowire it. But in 3.4 there's this fallback which triggers this warning.
Once the strict_mode is activated the event just won't be available as service and if you tried using it as one an error would rise.

How to do advanced filtering of Monolog messages in Symfony?

I am using the MonologBundle in my Symfony 2.8 project to manage log messages. Using different Handlers it is no problem to write logs to file and to send them by e-mail at the same time.
I would like to reduce the number of messages I receive by mail. I already use the DeduplicationHandler and the FingersCrossed handler to filter by error level and to avoid duplicate messages. This works fine but is not enough.
For example I would like to reduce the number of mail about PageNotFound errors. Of course I want to be notified if /existingPage is not found, but I am not interested in messages about /.well-known/... files.
Another example are messages about errors in a third party CSV parser component. There are several known and harmless errors I am not interested in, but of course other errors are important.
This these errors/messages are generated by third party code, I cannot influence the source. I could only ignore these messages completely but this is not what I want.
I am looking for a solution to filter the messages by content. How can this be done in Monolog?
I already tried to solve this using a HandlerWrapper and discussed this issue in another question: The idea was, that the HandlerWrapper acts as filter. The HandlerWrapper is called by Monolog, it checks the message content and decides wether it should be processed or not (e.g. discard all messages including the text "./well-known/"). If a messages passes, the HandlerWrapper should simple hand it over to its nested/wrapped handler. Otherwise the message is skipped without further processing.
However this idea did not work, and the answers to the other question indicate, that a HandlerWrapper is not the right approach for this problem.
So the new/actual question is: How to create a filter for Monolog messages, that let me control wether a specific message should be process or not?
I'm not sure why using a HandlerWrapper is the wrong way to do it.
I had the same issue and figured a way how to wrap a handler in order to filter certain records.
In this answer I describe two ways to solve this, a more complex and an easy one.
(More or less) complex way
First thing I did, was to create a new class wich extends the HandlerWrapper and added some logic where I can filter records:
use Monolog\Handler\HandlerWrapper;
class CustomHandler extends HandlerWrapper
{
public function isHandling(array $record)
{
if ($this->shouldFilter($record)) {
return false;
}
return $this->handler->isHandling($record);
}
public function handle(array $record)
{
if (!$this->isHandling($record)) {
return false;
}
return $this->handler->handle($record);
}
public function handleBatch(array $records)
{
foreach ($records as $record) {
$this->handle($record);
}
}
private function shouldFilter(array $record)
{
return mt_rand(0, 1) === 1;; // add logic here
}
}
Then I created a service definition and a CompilerPass where I can wrap the GroupHandler
services.yml
CustomHandler:
class: CustomHandler
abstract: true
arguments: ['']
use Monolog\Handler\GroupHandler;
use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Definition;
use Symfony\Component\DependencyInjection\Reference;
class CustomMonologHandlerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if (!$container->hasDefinition(CustomHandler::class)) {
return;
}
$definitions = $container->getDefinitions();
foreach ($definitions as $serviceId => $definition) {
if (!$this->isValidDefinition($definition)) {
continue;
}
$cacheId = $serviceId . '.wrapper';
$container
->setDefinition($cacheId, new ChildDefinition(CustomHandler::class))
->replaceArgument(0, new Reference($cacheId . '.inner'))
->setDecoratedService($serviceId);
}
}
private function isValidDefinition(Definition $definition): bool
{
return GroupHandler::class === $definition->getClass();
}
}
As you can see I go over all definitions here and find the ones which have the GroupHandler set as their class. If this is the case, I add a new definition to the container which decorates the original handler with my CustomHandler.
Side note: At first I tried to wrap all handlers (except the CustomHandler of course :)) but due to some handlers implementing other interfaces (like the ConsoleHandler using the EventSubscriberInterface) this did not work and lead to issues I didn't want to solve in some hacky way.
Don't forget to add this compiler pass to the container in your AppBundle class
class AppBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new CustomMonologHandlerPass());
}
}
Now that everything is in place you have to group your handlers in order to make this work:
app/config(_prod|_dev).yml
monolog:
handlers:
my_group:
type: group
members: [ 'graylog' ]
graylog:
type: gelf
publisher:
id: my.publisher
level: debug
formatter: my.formatter
Easy way
We use the same CustomHandler as we did in the complex way, then we define our handlers in the config:
app/config(_prod|_dev).yml
monolog:
handlers:
graylog:
type: gelf
publisher:
id: my.publisher
level: debug
formatter: my.formatter
Decorate the handler in your services.yml with your own CustomHandler
services.yml
CustomHandler:
class: CustomHandler
decorates: monolog.handler.graylog
arguments: ['#CustomHandler.inner']
For the decorates property you have to use the format monolog.handler.$NAME_SPECIFIED_AS_KEY_IN_CONFIG, in this case it was graylog.
... and thats it
Summary
While both ways work, I used the first one, as we have several symfony projects where I need this and decorating all handlers
manually is just not what I wanted.
I hope this helps (even though I'm quite late for an answer :))
In my humble opinion, there is a simpler and clean way (maybe it was not available when the first answer was done).
I write it here because I think it can help a lot of people who need to filter their logs with a simple answer, understanding what they do:
Just implements a simple handler from AbstractHandler :
use Monolog\Handler\AbstractHandler;
/** Simple handler wrapper that filters records based on content. */
class FilterHandler extends AbstractHandler
{
/** For Monolog 3+, typehint with LogRecord instead of array */
public function isHandling(array $record): bool
{
// This function is called only with level info, so always return true if you don't want to filter on level (cf AbstractHandler phpdoc)
return true;
}
/** Handles a record. Idem, for Monolog 3+, typehint with LogRecord */
public function handle(array $record): bool
{
return $this->shouldFilter($record);
}
// Just a simple exemple (idem, Logrecord for Monolog 3+)
private function shouldFilter(array $record): bool
{
return
array_key_exists('context', $record) &&
array_key_exists('route', $record['context']) &&
'app_logout' === $record['context']['route']
;
}
}
Then in services.yaml, declare your Handler (as ordinary service) :
services:
my_filter_handler:
class: App\MyFilterNamespace\FilterHandler
public: false
And in your monolog.yaml conf, just add your handler on the top of the handlers with the type service:
monolog:
handlers:
main:
type: service
id: my_filter_handler
handler: rotating # next handler
rotating:
type: rotating_file # or whatever you need
[...]

Inject kernel.root_dir to entity constructor in Symfony2

i searh and read a lot of same question but ever i got the same error :/
I create a service:
parameters:
tbackend.report.class: T\BackendBundle\Entity\Report
services:
tbackend.entity.report:
class: %tbackend.report.class%
arguments: ["%kernel.root_dir%"]
And i has this in T\BackendBundle\Entity\Report:
public function __construct($rootDir){
$this->rootDir = $rootDir;
}
When i try to create new Report(); i receive this msg:
Warning: Missing argument 1 for T\BackendBundle\Entity\Report::__construct(), called in /var/www/test/t_study/src/T/BackendBundle/Entity/ReportRepository.php on line 62 and defined
Considerations: i know the services.yml is called, i has more services in this file and all work ok (Loginhandlers, etc), i only add one more (tbackend.entity.report)
What is wrong with that? :( I dont know if need more for know about the problem. I follow symfony2 service container guide
http://symfony.com/doc/master/book/service_container.html
Basically I try not to use DIR in the Entity when moving files
Ty
When instantiating a class, you use normal PHP. Symfony isn't some magic that hooks into the instantiating process of PHP to automatically inject things in the constructor.
If you want to get a service, you either have to inject the service in the class you need it or you have the containe rin the class (for instance, in the controller) and retrieve the service from the container.
$report = $this->container->get('tbackend.entity.report');
or: (which is a much better practice in all cases except from controllers)
class WhereINeedTheReport
{
private $report;
public function __construct(Report $report)
{
$this->report = $report;
}
}
services:
# ...
where_i_need_the_report:
class: ...
arguments: ["#tbackend.entity.report"]

Symfony2 before and after filters doesn't work with containeraware

I have multilingual site that uses xml for diffrent languages. (I know that Symfony Translations exists, but i'm implementing my own system for accessing and taking xml elements values and i wanted to test it).
I'm not going to put the entire code here for readability, the implementation is same as in this link.
I have a service under a name xml_handler. The service I fetched in the controller with $this->get('xml_handler'). But then, I started using Symfony2 best practices and started extending ContainerAware. After that, the code below, which is defined as a Before Listener, failed.
if($controller[0] instanceof LanguageInterface) {
$xmlHandler = $controller[0]->get('xml_handler');
It raises an error that there is no get() method in $controller (IndexController is the name of the controller that extends ContainerAware, but that doesn't really matter, just for clarity)
The problem arose after I stopped extending Controller but started to extend ContainerAware, as i said earlier.
So, how do i get the get() method, which is protected so obviously, trying :
$controller[0]->container->get('xml_handler')
Doesn't work. I've looked at the API but there isn't a getContainer() method.
I guess, the questing is, how do I access ContainerAware outside the controller, in my case, in the Before filter?
Thanks for the answers.
You must pass all the required dependencies to your listener constructor:
In your listener :
<?php
namespace Acme\MyBundle\EventListener;
class MyListener
{
private $xmlHandler;
public function __construct($xmlHandler)
{
$this->xmlHandler = $xmlHandler;
}
public function onKernelController(FilterControllerEvent $event)
{
$controller = $event->getController();
if (!is_array($controller)) {
return;
}
if ($controller[0] instanceof LanguageInterface) {
$this->xmlHandler->doSomething(...);
// ...
In your configuration file you have to ask the dependency injection component to provide the xml_handler service as an argument of your listener constructor (a dependency).
services:
acme_my.listener.action:
class: Acme\MyBundle\EventListener\MyListener
arguments:
- "#xml_handler"
tags:
- { name: kernel.event_listener, event: kernel.controller, method: onKernelController }

Categories