Collectez vos données dans le profiler Symfony (French)

2013-08-18

  symfony    french 

Il est parfois utile (en mode debug, bien entendu) de collecter des données de services auxquels nous faisons appel. Les développeurs aimant allier l'utile à l'agréable, cet article va vous montrer comment collecter agréablement vos données dans le profiler de Symfony.

C'est parti !

Imaginons que vous ayez un service (nommé ici MyService) et que vous souhaitez collecter tous les appels faits à ce service.

Commençons par créer une classe MyDataCollector qui s'occupera de stocker vos données collectées. Cette classe étend la classe de Symfony Symfony\\Component\\HttpKernel\\DataCollector\\DataCollector.


namespace Eko\\MyBundle\\DataCollector;

use Symfony\\Component\\HttpKernel\\DataCollector\\DataCollector;
use Symfony\\Component\\HttpKernel\\DataCollector\\DataCollectorInterface;
use Symfony\\Component\\HttpFoundation\\Request;
use Symfony\\Component\\HttpFoundation\\Response;

use Eko\\MyBundle\\Service\\MyService;

/**
 * Class MyDataCollector
 *
 * This collects all methods calls that are done in my service MyService
 */
class MyDataCollector extends DataCollector implements DataCollectorInterface
{
    /**
     * @var MyService
     */
    protected $service;

    /**
     * Constructor
     *
     * @param MyService $service
     */
    public function __construct(MyService $service)
    {
        $this->service = $service;
    }

    /**
     * {@inheritdoc}
     */
    public function collect(Request $request, Response $response, \\Exception $exception = null)
    {
        $this->data = $this->service->getProfiles();
    }

    /**
     * Returns profiled data
     *
     * @return array
     */
    public function getData()
    {
        return $this->data;
    }

    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return 'eko.data_collector.my_data_collector';
    }
}

Bien ! Il nous faut désormais enregistrer ce data collector comme service et le taguer sous data_collector, comme tous les autres services qui collectent des données dans Symfony.

C'est ce que nous faisons ici :


<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="eko.data_collector.my_data_collector" class="Eko\\MyBundle\\DataCollector\\MyDataCollector">
            <tag name="data_collector" template="EkoMyBundle:Collector:my_collector" id="eko.data_collector.my_data_collector" />

            <argument type="service" id="eko.service.my_service" />
        </service>
    </services>
</container>

Notez également que nous spécifions un template au nom de tag pour et pour l'identifiant correspondant au nom de notre data collector.

Ce template va nous permettre de créer une entrée dans la toolbar du WebProfiler de Symfony ainsi qu'une entrée dans le profiler.

Pour comprendre un peu mieux, voici le template à créer :


{% extends 'WebProfilerBundle:Profiler:layout.html.twig' %}

{% block toolbar %}
    <div class="sf-toolbar-block">
        <div class="sf-toolbar-info">
            <div class="sf-toolbar-info-piece">
                <b>Total calls</b>
                <span class="sf-toolbar-status sf-toolbar-status-green">{{ data.collector|length }}</span>
            </div>
        </div>
        <div class="sf-toolbar-icon">
            <a href="{{ path('_profiler', { 'token': token, 'panel': name }) }}">
                {#<span class="icon"><img src="data:image/png;base64,..." alt="" /></span>#}
                <span class="sf-toolbar-status">{{ data.collector|length }}</span>
            </a>
        </div>
    </div>
{% endblock %}

{% block menu %}
    <span class="label">
        {#<span class="icon"><img src="data:image/png;base64,..." alt="" /></span>#}
        <strong>MyService</strong>
        <span class="count">
            <span>{{ collector.data|length }}</span>
        </span>
    </span>
{% endblock %}

{% block panel %}
    <h2>MyService calls detail</h2>

    {% if collector.data|length == 0 %}
        <p><em>No calls profiled.</em></p>
    {% else %}
        <table>
            <tr>
                <th>#</th>
                <th>Query</th>
                <th>Duration</th>
                <th>Memory usage</th>
            </tr>

            {% for call in collector.data %}
                <tr>
                    <td>{{ loop.index }}</td>
                    <td>{{ call.query }}</td>
                    <td>{{ call.duration }} ms</td>
                    <td>{{ call.memory_end }}</td>
                </tr>
            {% endfor %}
        </table>
    {% endif %}
{% endblock %}

Ce template définit plusieurs blocks utilisés à différents endroits dans le profiler (toolbar, menu du profiler, block de détail, ...). Je passe rapidement sur cette partie car elle semble plutôt claire. Juste une petite particularité : si vous souhaitez ajouter des icônes à vos éléments du menu, encodez-les en base64.

Nous avons tout, maintenant, collectons les données !

Voici notre service Eko\\MyBundle\\MyService:


namespace Eko\\MyBundle\\Service;

use Symfony\\Component\\Stopwatch\\Stopwatch;
use Symfony\\Component\\Stopwatch\\StopwatchEvent;

/**
 * Class MyService
 *
 * This is my amazing service
 */
class MyService
{
    /**
     * @var array $profiles Profiled data
     */
    protected $profiles = array();

    /**
     * @var Stopwatch $stopwatch Symfony profiler Stopwatch service
     */
    protected $stopwatch;

    /**
     * @var integer
     */
    protected $counter = 1;

    /**
     * Constructor
     *
     * @param Stopwatch $stopwatch Symfony profiler stopwatch service
     */
    public function __construct(Stopwatch $stopwatch = null)
    {
        $this->stopwatch = $stopwatch;
    }

    /**
     * Returns profiled data
     *
     * @return array
     */
    public function getProfiles()
    {
        return $this->profiles;
    }

    /**
     * My call action
     *
     * @param string $query My call query
     */
    public function call($query)
    {
        $event = $this->startProfiling($query);

        // Here is my call query
        $result = $this->process(); // this methods process my fake call

        $this->stopProfiling($event, $result);
    }

    /**
     * Starts profiling
     *
     * @param string $query Query text
     *
     * @return StopwatchEvent
     */
    protected function startProfiling($query)
    {
        if ($this->stopwatch instanceof Stopwatch) {
            $this->profiles[$this->counter] = array(
                'query'        => urldecode($query),
                'duration'     => null,
                'memory_start' => memory_get_usage(true),
                'memory_end'   => null,
                'memory_peak'  => null,
            );

            return $this->stopwatch->start($query);
        }
    }

    /**
     * Stops the profiling
     *
     * @param StopwatchEvent $event A stopwatchEvent instance
     */
    protected function stopProfiling(StopwatchEvent $event = null)
    {
        if ($this->stopwatch instanceof Stopwatch) {
            $event->stop();

            $values = array(
                'duration'    => $event->getDuration(),
                'memory_end'  => memory_get_usage(true),
                'memory_peak' => memory_get_peak_usage(true),
            );

            $this->profiles[$this->counter] = array_merge($this->profiles[$this->counter], $values);

            $this->counter++;
        }
    }
}

Notre service dispose donc d'une méthode call() qui fera donc appel au composant Stopwatch via les méthodes startProfiling() et stopProfiling() pour collecter les données, qui seront stockées dans la propriété profiles.

Maintenant, il faut injecter dans notre service le composant Stopwatch de Symfony mais uniquement dans le cas ou le mode debug est actif.

Ainsi, dans notre fichier DependencyInjection\\EkoMyExtension.php nous ajoutons uniquement la définition du composant lorsque le paramètre kernel.debug est présent.


namespace Eko\\MyBundle\\DependencyInjection;

use Symfony\\Component\\DependencyInjection\\ContainerBuilder;
use Symfony\\Component\\Config\\FileLocator;
use Symfony\\Component\\HttpKernel\\DependencyInjection\\Extension;
use Symfony\\Component\\DependencyInjection\\Loader;
use Symfony\\Component\\DependencyInjection\\Loader\\XmlFileLoader;
use Symfony\\Component\\DependencyInjection\\Reference;

/**
 * This is the class that loads and manages your bundle configuration
 *
 * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html}
 */
class EkoMyExtension extends Extension
{
    /**
     * {@inheritDoc}
     */
    public function load(array $configs, ContainerBuilder $container)
    {
        $configuration = new Configuration();
        $config = $this->processConfiguration($configuration, $configs);

        $loader = new Loader\\XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
        $loader->load('service.xml');

        $this->loadProfilerCollector($container, $loader);
    }

    /**
     * Loads profiler collector for correct environments
     *
     * @param ContainerBuilder $container Symfony dependency injection container
     * @param XmlFileLoader    $loader    XML file loader
     */
    protected function loadProfilerCollector(ContainerBuilder $container, XmlFileLoader $loader)
    {
        if ($container->getParameter('kernel.debug')) {
            $loader->load('collector.xml');

            $serviceDefinition = $container->getDefinition('eko.service.my_service');
            $serviceDefinition->addArgument(new Reference('debug.stopwatch'));

            $container->setDefinition('eko.service.my_service', $serviceDefinition);
        }
    }
}

Voilà, vous collectez désormais vos données dans le profiler Symfony ! Il faut avouer que ce n'est pas si sorcier que ça.

Plus d'informations

→ La documentation Symfony est toujours excellente et est là pour vous si vous avez des interrogations supplémentaires : http://symfony.com/fr/doc/current/cookbook/profiler/data_collector.html

Comments