Symfony est actuellement un framework assurément mature et puissant. Cepandant, sa documentation peut parfois sembler déroutante. Certains choses simples, ou pouvant paraître primordiales, ne semblent pas indiquées clairement.

Le fait, par exemple, de logger les erreurs jetées par Symfony à différents endroits n’est pas toujours aisé. Ce tutoriel se base sur la version 3.4 de Symfony.

J’ai récemment tenté de logger les erreurs dans une table MySQL. Mon but était donc de :

  1. Pouvoir logger diverses informations dans la base données grâce au framework PHP Monolog
  2. Automatiquement logger les erreurs jetées par Symfony en production (500, 404 …)

J’ai pour cela utilisé le bundle proposé par défaut, à savoir le Symfony Monolog Bundle, basé sur l’excellente librarie PHP Monolog.

Il est pour cela nécessaire de tout d’abord créer l’entité qui permettra d’enregistrer les logs dans la base de données.

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Swagger\Annotations as SWG;
/**
 * @ORM\Entity(repositoryClass="AppBundle\Repository\LogRepository")
 * @ORM\Table(name="log")
 * @ORM\HasLifecycleCallbacks
 */
class Log
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @ORM\Column(name="message", type="text")
     */
    private $message;

    /**
     * @ORM\Column(name="context", type="array")
     */
    private $context;

    /**
     * @ORM\Column(name="level", type="smallint")
     */
    private $level;

    /**
     * @ORM\Column(name="level_name", type="string", length=50)
     */
    private $levelName;

    /**
     * @ORM\Column(name="extra", type="array")
     */
    private $extra;

    /**
     * @ORM\Column(name="created_at", type="datetime")
     */
    private $createdAt;

    /**
     * @ORM\PrePersist
     */
    public function onPrePersist()
    {
        $this->createdAt = new \DateTime();
    }

    /**
     * Get id.
     *
     * @return int
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set message.
     *
     * @param string $message
     *
     * @return Log
     */
    public function setMessage($message)
    {
        $this->message = $message;

        return $this;
    }

    /**
     * Get message.
     *
     * @return string
     */
    public function getMessage()
    {
        return $this->message;
    }

    /**
     * Set context.
     *
     * @param array $context
     *
     * @return Log
     */
    public function setContext($context)
    {
        $this->context = $context;

        return $this;
    }

    /**
     * Get context.
     *
     * @return array
     */
    public function getContext()
    {
        return $this->context;
    }

    /**
     * Set level.
     *
     * @param int $level
     *
     * @return Log
     */
    public function setLevel($level)
    {
        $this->level = $level;

        return $this;
    }

    /**
     * Get level.
     *
     * @return int
     */
    public function getLevel()
    {
        return $this->level;
    }

    /**
     * Set levelName.
     *
     * @param string $levelName
     *
     * @return Log
     */
    public function setLevelName($levelName)
    {
        $this->levelName = $levelName;

        return $this;
    }

    /**
     * Get levelName.
     *
     * @return string
     */
    public function getLevelName()
    {
        return $this->levelName;
    }

    /**
     * Set extra.
     *
     * @param array $extra
     *
     * @return Log
     */
    public function setExtra($extra)
    {
        $this->extra = $extra;

        return $this;
    }

    /**
     * Get extra.
     *
     * @return array
     */
    public function getExtra()
    {
        return $this->extra;
    }

    /**
     * Set createdAt.
     *
     * @param \DateTime $createdAt
     *
     * @return Log
     */
    public function setCreatedAt($createdAt)
    {
        $this->createdAt = $createdAt;

        return $this;
    }

    /**
     * Get createdAt.
     *
     * @return \DateTime
     */
    public function getCreatedAt()
    {
        return $this->createdAt;
    }
}

Une fois l’entité crée, nous avons maintenant besoin du service qui permettra de persister des instances de notre entité :

<?php
// src/AppBundle/Utils/MonologDBHandler.php

namespace AppBundle\Utils;

use AppBundle\Entity\Log;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;

class MonologDBHandler extends AbstractProcessingHandler
{
    /**
     * @var EntityManagerInterface
     */
    protected $em;

    public function __construct(EntityManagerInterface $em, $level = Logger::ERROR, $bubble = true)
    {
        $this->em = $em;
        parent::__construct($level, $bubble);
    }

    /**
     * Called when writing to our database
     * @param array $record
     */
    protected function write(array $record)
    {
        try {

            $logEntry = new Log();
            $logEntry->setMessage($record['message']);
            $logEntry->setLevel($record['level']);
            $logEntry->setLevelName($record['level_name']);

            if(is_array($record['extra']))
            {
                $logEntry->setExtra($record['extra']);
            } else {
                $logEntry->setExtra([]);
            }

            if (is_array($record['context'])) {
                $logEntry->setExtra($record['context']);
            } else {
                $logEntry->setExtra([]);
            }


            $this->em->persist($logEntry);
            $this->em->flush();
        } catch (\Exception $e) {

        }
    }

}

La prochaine étape consiste à configurer correctement ce nouveau service dans le fichier app/config/services.yml

# app/config/services.yml

# ...

services:
    # ...

    # Handler commun qui nous permettra de logger des informations diverses
    monolog.db_handler:
        class: AppBundle\Utils\MonologDBHandler
        # Les handlers de type 'services' doivent etre configurés manuellement, comme précisé ici : https://github.com/symfony/monolog-bundle/issues/116
        # Les arguments qui iront dans le constructeur
        arguments:
            - '@doctrine.orm.entity_manager'
            - !php/const Monolog\Logger::DEBUG
            # Argument 'bubble' à false : Les handlers situés après celui-ci ne seront pas appelés si le log a déjà été pris en compte
            - false

    # Handler d'erreurs, filtrant les logs dont le level est inférieur à "ERROR"
    monolog.error_db_handler:
        class: AppBundle\Utils\MonologDBHandler
        # Les arguments qui iront dans le constructeur
        arguments:
            - '@doctrine.orm.entity_manager'
            - !php/const Monolog\Logger::ERROR

    # ...

La dernière étape consiste à configure monolog de manière à ce qu’il utilise nos nouvelles déclarations de services en tant que handlers. Un point important à noter, et non précisé dans la documentation de Symfony, est que la configuration des handlers de type ‘service’ se fait directement dans leur constructeur, comme vu sur le fichier précédent.

monolog:
    use_microseconds: false
    # On définit ici de nouveaux channels
    channels: ["app-channel"]

    handlers:
        db_handler:
            # Le level du handler est configuré au niveau du service même : https://github.com/symfony/monolog-bundle/issues/116
            type: service
            id: monolog.db_handler
            channels: app-channel
    
        main:
            type: fingers_crossed
            action_level: error
            handler: grouped_handler

        # Handler de type "group" permettant de regrouper d'autres handlers
        grouped_handler:
            type: group
            members: ["nested_handler", "db_error_handler"]

        nested_handler:
            type:  stream
            path:  "php://stderr"
            # Minimum level de log
            level: info
            # On utilise un Formatter se basant sur Line Formatter afin de virer des champs si besoin
            # formatter: AppBundle\Formatter\LogFormatter

        db_error_handler:
            # Le level du handler est configuré au niveau du service même : https://github.com/symfony/monolog-bundle/issues/116
            type: service
            id: monolog.error_db_handler

Pour pouvoir utiliser notre nouveau logger, il faut mettre à jour le schéma de la base de données, en créant la table associée à l’entité ‘Log’. Lancez cette commande Symfony dans un terminal :

php bin/console doctrine:schema:update --force

Finalement, on peut vérifier que nos loggers soient opérationnels. On ajoute donc :

$logger = $this->get('monolog.logger.app-channel');
$logger->info("This one goes to app-channel!!");

dans le code d’un quelconque controller. On effectue ensuite une requête et l’on vérifie que le log ait bien été persisté en base de données.

On peut également tester le fait que les erreurs (ici de type 500) soit bien loggées aussi. On ajoute le code suivant dans le même controller :

throw new \Exception();

Et l’on ré-effectue une requête, en vérifiant ensuite que l’erreur ait bien été loggée par Symfony.