Collect your data using the Symfony profiler (English)

2013-08-18

  symfony    english 

This is often useful to collect custom services data. Developers magnet combine business with pleasure, this post will show you how to collect data with please using the great Symfony profiler.

Let's go!

Imagine a custom service (here named MyService) and you want to collect all requests made to this service.

Start by creating a data collector MyDataCollector which will process collected data. This custom data collector class extends the Symfony one 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';
    }
}

Well! We now need to register this custom data collector as a service and tag it under data_collector as all symfony data collectors services.

That's what we do here:


<?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>

Please note that we provide a template argument to the tag corresponding to our data collector name.

This custom template will allow us to personalize the Symfony WebProfiler toolbar entry and also the profiler menu displays.

To understand more, here is our custom template:


{% 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 %}

This template defined multiple blocks used by the profiler (toolbar, profiler page menu, detail block, ...). This is quite clearly, I think. Just one little thing if you want to add a custom icon, the source must be base64 encoded.

Alright, now, collect your data!

Here is our custom 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++;
        }
    }
}

Our custom service has a call() method. This is the method that will call the Stopwatch component via startProfiling() and stopProfiling() to collect data. Collected data will be stored in the profiles class property.

Now, just inject into our custom service the Stopwatch component. Only when debug mode is enabled in Symfony.

Thus, in our bundle file DependencyInjection\\EkoMyExtension.php we will add the Stopwatch service argument only when kernel.debug exists.


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

That's it, your custom service calls are now collected in the Symfony profiler! Admit it, this is not rocket science.

More information

→ The Symfony documentation is always really clear and complete to find information so do not hesitate to visit http://symfony.com/en/doc/current/cookbook/profiler/data_collector.html

Comments