1

I need to log all my users actions with monolog. But only if the actions persist data with doctrine, insert, update or delete.

What should I do ? Could I define a generic method like "afterPersist" to log every action ?

Thx !

EDIT :

The Listener :

use Doctrine\ODM\MongoDB\Event\OnFlushEventArgs;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Security\Core\SecurityContextInterface;

class DatabaseLogger
{
    protected $logger;
    protected $security_context;
    protected $request;

    public function __construct(LoggerInterface $logger, ContainerInterface $service_container)
    {
        $this->logger = $logger;
        $this->setSecurityContext($service_container->get('security.context'));
    }

    public function setRequest(RequestStack $request_stack)
    {
        $this->request = $request_stack->getCurrentRequest();
    }

    public function setSecurityContext(SecurityContextInterface $security_context)
    {
        $this->security_context = $security_context;
    }

    public function onFlush(OnFlushEventArgs $args)
    {
        // configure this however you want
    }
}

and in service.yml

cc.listener.database_logger:
    class: Cc\HitoBundle\Listener\DatabaseLogger
    tags:
        - { name: doctrine_mongodb.odm.event_listener, event: onFlush }
        - { name: monolog.logger, channel: database_access }
    calls:
        - [ setRequest, [@request_stack] ]
    arguments: [ @logger, @service_container ]

I got an error when I add the security context :

ServiceCircularReferenceException: Circular reference detected for service "doctrine_mongodb.odm.default_document_manager", path: "doctrine_mongodb.odm.default_document_manager -> doctrine_mongodb.odm.default_connection -> doctrine_mongodb.odm.event_manager -> cc.listener.post_persist -> security.context -> security.authentication.manager -> security.user.provider.concrete.user_db".

Dan Blows
  • 20,846
  • 10
  • 65
  • 96
Cyril F
  • 1,247
  • 3
  • 16
  • 31

2 Answers2

3

Register a listener with something like:

Build a listener:

namespace Acme\MyBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;

class PersistLogger
{
       public $logger;

       public function __construct($logger)
       {
           $this->logger = $logger; 
       }

       public function postPersist(LifecycleEventArgs $args)
       {
           // configure this however you want
           $this->logger->addDebug('whatever');
       }
}

Register the listener in config.yml

acme_mybundle.eventlistener.persistlogger:
    class: Acme\MyBundle\EventListener\PersistLogger
    tags:
        - { name: doctrine.event_listener, event: postPersist }
    argument: [ @logger ]

EDIT: Injecting the security context into a doctrine listener causes a circular reference exception if you are storing your users in the database (e.g. with FOSUserBundle). This is because the security context needs to inject the entity manager so it can get users from the database, but because of the listener, the entity manager depends on the security context.

The workaround is to inject the whole service container (one of the only times doing this is justified), and get the security context from there:

namespace Acme\MyBundle\EventListener;

use Psr\Log\LoggerInterface,
    Symfony\Component\DependencyInjection\ContainerInterface,
    Symfony\Component\Security\Core\SecurityContextInterface;

protected $service_container;
protected $logger;

public function __construct(LoggerInterface $logger, ContainerInterface $service_container) 
{
    $this->service_container = $service_container;
    $this->logger = $logger;
}

public function getSecurityContext() 
{
    return $this->service_container->get('security.context');
}

and

acme_mybundle.eventlistener.persistlogger:
    class: Acme\MyBundle\EventListener\PersistLogger
    tags:
        - { name: doctrine.event_listener, event: postPersist }
    argument: [ @logger, @service_container ]
Dan Blows
  • 20,846
  • 10
  • 65
  • 96
  • Except for dot notation of services that you're not respecting ... all seems to be perfect +1 – DonCallisto Sep 24 '14 at 16:37
  • 1
    @DonCallisto updated it to dot notation so it really is perfect ;) – Dan Blows Sep 25 '14 at 10:27
  • It's a joy for my eyes :D – DonCallisto Sep 25 '14 at 10:37
  • Ok, that is what I want, but in the postPersist method, is it possible to get the user who persist the data ? And maybe extra informations like the route used ? – Cyril F Sep 30 '14 at 12:11
  • 1
    To get the route, you can inject the `request` service in the same way that you injected the `logger`. By *the user who persisted the data*, I assume you mean the currently logged in user? In that case, inject the `security context` service (`@security.context`), and get the user from there. – Dan Blows Sep 30 '14 at 13:47
  • Instead of injecting `logger`, `request` and `security context`, can I use the `service container` to get all these services ? – Cyril F Oct 01 '14 at 08:12
  • 1
    @kyrillos yes that will work. But it's a good idea to inject services instead, because it makes testing and debugging easier, since you'll know at compile time whether you have a problem. If you inject the container and use `get`, then you'll only know at runtime. You can also take advantage of type hinting to ensure you are injecting the right service. – Dan Blows Oct 01 '14 at 13:36
  • 1
    @kyrillos I couldn't find a canonical answer to why you shouldn't inject the service container, so I wrote one - http://stackoverflow.com/questions/23931321/why-injecting-whole-service-container-to-another-service-is-almost-always-a-ba/26144509#26144509. – Dan Blows Oct 01 '14 at 15:12
  • Sorry but I got an error : The definition "cc.listener.post_persist" references the service "request" which belongs to a narrower scope. Generally, it is safer to either move "cc.listener.post_persist" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "request" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error. – Cyril F Oct 02 '14 at 12:34
  • 1
    @kyrillos Here's one I answered earlier: http://stackoverflow.com/a/20502945 Basically you inject the `request_stack` and get the actual request from there. – Dan Blows Oct 02 '14 at 13:43
  • Hello, It isn't working in fact. Let see my edited question. Thx ! – Cyril F Oct 08 '14 at 13:05
  • 1
    @kyrillos Oh I forgot about that. Unfortunately there's a long-standing problem in Symfony, where injecting the security context into a Doctrine event listener causes circular references. The workaround is to inject the service container (`@service_container`) and use `$service_container->get('security.context')` instead. I'll update the answer. – Dan Blows Oct 08 '14 at 14:00
  • I still have an circular reference with my updated code !! Look above, I try to follow your instructions. – Cyril F Oct 09 '14 at 07:31
  • 1
    @kyrillos ok try that - I changed the setter to a getter. It's a kludgy fix but the alternative is really complicated. – Dan Blows Oct 09 '14 at 10:00
1

I think that you may have a look to the cookbook, there is a very nice entry that talk about Doctrine's events.

In addition, you may have a look to the method to create custom monolog chanels.

Yann Eugoné
  • 1,311
  • 1
  • 8
  • 17