| name | implementing-symfony-profiler-data-collectors |
| description | Implements and modifies Symfony Profiler Data Collectors with toolbar panels, Twig templates, and service registration. Use when creating, editing, or debugging data collectors for profiling, performance monitoring, or custom metrics visualization in the Web Debug Toolbar. Also use this skill when working with DataCollectorInterface, LateDataCollectorInterface, AbstractDataCollector, cloneVar(), profiler_dump(), or the data_collector service tag — even if the user doesn't explicitly mention "data collector". |
Creating Symfony Data Collectors
Purpose
Creates custom Symfony Profiler Data Collectors that integrate with the Web Debug Toolbar and Profiler panel. Data collectors capture request-specific metrics, timing data, and debugging information displayed during development.
When to Use
- Performance Profiling: Track execution time of specific subsystems (services, subsystems)
- Metrics Visualization: Display request-scoped counters, timings, or statistics in toolbar
- Debugging Tools: Capture and inspect complex objects during request lifecycle
- Event Tracking: Monitor kernel events, domain events, or message dispatches
- Integration Monitoring: Track external service calls, cache hits/misses, query counts
When NOT to Use
- Production Monitoring: Use Prometheus/Grafana, not data collectors
- Logging: Use Monolog with appropriate log levels
- Business Analytics: Use domain events and reporting infrastructure
- Error Tracking: Use Sentry or structured exception handling
Core Concepts
Class Hierarchy
DataCollectorInterface (symfony/http-kernel) extends ResetInterface
- public collect(Request, Response, ?Throwable): void
- public getName(): string
|
v
DataCollector (abstract, symfony/http-kernel)
- protected array|Data $data = []
- protected cloneVar(mixed $var): Data
- protected getCasters(): callable[]
- public reset(): void → sets $this->data = []
- public __serialize(): array
- public __unserialize(array $data): void
|
v
AbstractDataCollector (symfony/framework-bundle)
extends DataCollector
implements TemplateAwareDataCollectorInterface
- public getName(): string → returns static::class
- public static getTemplate(): ?string → returns null
Key Interfaces
| Interface | Purpose | Key Method |
|---|
DataCollectorInterface | Base contract | collect(Request, Response, ?Throwable): void |
TemplateAwareDataCollectorInterface | Links to Twig template | static getTemplate(): ?string |
LateDataCollectorInterface | Post-response collection | lateCollect(): void |
Data Flow
1. Request arrives
2. collect(Request, Response, ?Throwable) called after response
3. Data stored in $this->data (must be serializable)
4. Profile serialized to storage
5. Twig template renders data in toolbar/panel
CRITICAL: The $data property is serialized. Store only scalars, arrays, and Data objects (from cloneVar()). Non-serializable objects (entities, services) cause profiler corruption.
cloneVar() for Complex Objects
Use cloneVar() to convert non-serializable objects into Data instances:
$this->data['request_attributes'] = $this->cloneVar($request->attributes->all());
In Twig templates, render Data objects with profiler_dump():
{{ profiler_dump(collector.requestAttributes) }}
Implementation Patterns
Pattern 1: Basic Collector (Recommended)
Extend AbstractDataCollector for the simplest approach:
<?php
declare(strict_types=1);
namespace App\DataCollector;
use Override;
use Symfony\Bundle\FrameworkBundle\DataCollector\AbstractDataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
final class ExampleDataCollector extends AbstractDataCollector
{
#[Override]
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data = [
'metric_count' => 0,
'items' => [],
];
}
#[Override]
public function getName(): string
{
return 'app.example';
}
#[Override]
public static function getTemplate(): ?string
{
return 'data_collector/example.html.twig';
}
public function getMetricCount(): int
{
return $this->data['metric_count'];
}
public function getItems(): array
{
return $this->data['items'];
}
}
See examples/01-basic-data-collector.md for complete implementation.
Pattern 2: Late Data Collector
Implement LateDataCollectorInterface when data is only available after the response:
use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface;
final class MemoryDataCollector extends AbstractDataCollector implements LateDataCollectorInterface
{
#[Override]
public function lateCollect(): void
{
$this->data['peak_memory'] = memory_get_peak_usage(true);
}
}
See examples/05-late-data-collector.md for complete implementation.
Pattern 3: Event Subscriber Collector
Combine data collection with event listening to accumulate metrics across the request:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
final class EventTrackingCollector extends AbstractDataCollector implements EventSubscriberInterface
{
private array $collectedEvents = [];
#[Override]
public static function getSubscribedEvents(): array
{
return [KernelEvents::CONTROLLER => 'onController'];
}
public function onController(ControllerEvent $event): void
{
$this->collectedEvents[] = ['name' => 'controller', 'time' => microtime(true)];
}
#[Override]
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data['events'] = $this->collectedEvents;
}
}
See examples/06-event-subscriber-collector.md for complete implementation.
Service Registration
Autoconfigure (Preferred)
Classes extending AbstractDataCollector are auto-tagged via Symfony autoconfigure. No manual registration needed if services are auto-discovered.
YAML Tag (Manual)
when@dev:
services:
App\DataCollector\ExampleDataCollector:
tags:
- name: data_collector
template: 'data_collector/example.html.twig'
id: 'app.example'
#[AutoconfigureTag] Attribute
use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag;
#[AutoconfigureTag('data_collector', ['template' => 'data_collector/example.html.twig', 'id' => 'app.example'])]
final class ExampleDataCollector extends AbstractDataCollector
Dev-Only Registration
MUST register collectors only in dev environment:
- YAML: Wrap in
when@dev: block
- PHP Attribute: Use
#[When('dev')] on the class
- Autoconfigure: Ensure service file is loaded only in dev
See examples/02-service-registration.md for all registration methods.
Twig Template Structure
Templates define four blocks for the profiler UI:
toolbar Block (Web Debug Toolbar)
{% extends '@WebProfiler/Profiler/layout.html.twig' %}
{% block toolbar %}
{% set icon %}
{# SVG icon + summary value #}
{{ include('@WebProfiler/Icon/event.svg') }}
<span class="sf-toolbar-value">{{ collector.metricCount }}</span>
{% endset %}
{% set text %}
{# Hover panel content #}
<div class="sf-toolbar-info-piece">
<b>Metric Count</b>
<span>{{ collector.metricCount }}</span>
</div>
{% endset %}
{{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url }) }}
{% endblock %}
menu Block (Profiler Left Sidebar)
{% block menu %}
<span class="label">
<span class="icon">{{ include('@WebProfiler/Icon/event.svg') }}</span>
<strong>Example</strong>
</span>
{% endblock %}
panel Block (Profiler Main Content)
{% block panel %}
<h2>Example Collector</h2>
{% if collector.metricCount == 0 %}
<div class="empty">
<p>No data collected.</p>
</div>
{% else %}
<table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
{% for item in collector.items %}
<tr>
<td>{{ item.name }}</td>
<td>{{ item.value }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% endblock %}
head Block (Optional CSS/JS)
{% block head %}
{{ parent() }}
<style>
.metrics-highlight { color: var(--color-warning); font-weight: bold; }
</style>
{% endblock %}
See examples/03-toolbar-template.md and examples/04-profiler-panel-template.md for complete templates.
Best Practices
- Lightweight
collect(): Minimize work in collect — heavy processing goes in lateCollect()
- Typed Getters: Expose data through typed accessor methods, not raw
$data access
- Serializable Data Only: Store scalars, arrays,
Data (via cloneVar()) — never services or entities
- Dev-Only Registration: Data collectors MUST only be registered in
dev environment
- Passive Observer: Collectors MUST NOT modify request, response, or application state
- Status Colors: Use toolbar status classes (
sf-toolbar-status-yellow, sf-toolbar-status-red) for thresholds
- Empty States: Always handle zero-data case in templates
- Reset Implementation: Override
reset() to clear both $data and any private accumulator properties
Anti-Patterns (FORBIDDEN)
Non-Serializable Data in $data
$this->data['user'] = $request->getUser();
$this->data['user'] = $this->cloneVar($request->getUser());
$this->data['user_email'] = $request->getUser()?->getUserIdentifier();
Side Effects in collect()
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->logger->info('Collecting data');
$this->cache->delete('key');
}
public function collect(Request $request, Response $response, ?\Throwable $exception = null): void
{
$this->data['status_code'] = $response->getStatusCode();
}
Wrong Layer Placement
namespace App\Domain\Profiler;
namespace App\DataCollector;
readonly Class
final readonly class ExampleDataCollector extends AbstractDataCollector
// CORRECT - final but not readonly
final class ExampleDataCollector extends AbstractDataCollector
Production Registration
services:
App\DataCollector\ExampleDataCollector:
tags: ['data_collector']
when@dev:
services:
App\DataCollector\ExampleDataCollector:
tags: ['data_collector']
Validation Checklist
Class Structure
Data Handling
Service Registration
Twig Template
Examples
Reference Implementations
Symfony Built-in Collectors
Related Skills
No additional skills are currently available in this project.
External References
Key Principle: Data Collectors are dev-only, passive observers in the infrastructure layer. They capture request-scoped metrics into serializable $data, expose them via typed getters, and render them through Twig templates in the Web Debug Toolbar and Profiler panel. They MUST NOT have side effects or be registered in production.