laminas/laminas-developer-tools
Laminas Developer Tools provides a developer toolbar and profiling collectors for Laminas MVC applications. It helps inspect requests, database queries, logs, performance metrics, and configuration during development, with integration for the Laminas ecosystem.
composer require --dev laminas/laminas-developer-tools
Do not add Laminas\DeveloperTools to config/modules.config.php (or application.config.php). Put it in the development-only module list so composer install --no-dev deployments can never autoload it:
// config/development.config.php
return [
'modules' => [
'Laminas\DeveloperTools',
],
'module_listener_options' => [
'config_glob_paths' => [
'config/autoload/{,*.}{global,local}.php',
],
],
];
If the project uses laminas/laminas-development-mode, run composer development-enable, then composer development-status to confirm development.config.php is active. Clear any config cache (data/cache/module-config-cache.* or your equivalent) after enabling.
Where to look first:
config/module.config.php — this is the authoritative list of collectors, toolbar entries, the profiler storage adapter, and the laminas-developer-tools route. Read it before overriding anything; the docs are thin and the config is the real API.src/Collector/ — each built-in collector is a small, readable template for writing your own.view/laminas-developer-tools/ — toolbar and report templates you can override via template_map.First use case: open any HTML page in dev, click the Db entry in the bottom toolbar, and you get query count and cumulative query time for that request. Click through to the stored report (/laminas-developer-tools/profiler?…) to see the timeline, the full query list with parameters, and the event/config/session panels. That single workflow — "this page took 800 ms, why?" — is 90% of the package's day-to-day value.
Environment gating is the primary pattern. The module should only ever be loaded in development. Treat the toolbar as a developer-only feature, and treat the report route as an internal endpoint that requires auth if it is reachable from a shared staging environment.
Tune the toolbar rather than accepting defaults. The toolbar is configured under laminas-developer-tools.toolbar with enabled, autoHide/auto_hide, position, and an entries map keyed by collector name (request, time, memory, db, event, config, mail, session). Turn off collectors you do not use in a given project — every collector runs on every request and the memory/time cost is real on query-heavy pages.
DB profiling. The DbCollector is driven by laminas-db's own Profiler attached to the adapter. Adapters resolved through the service manager are the happy path; an adapter you instantiate by hand, or a second named adapter, may not be instrumented unless you attach a profiler to it (or register it with the collector's adapter configuration). If query counts look suspiciously low, check that the adapter is the instrumented one before blaming the collector.
Custom collectors are the extension point. Implement Laminas\DeveloperTools\Collector\CollectorInterface (getName(), getLabel(), getDescription(), collect(MvcEvent $event)), register it as a service, and wire it into config:
final class CacheCollector implements CollectorInterface
{
private array $stats = [];
public function getName(): string { return 'cache'; }
public function getLabel(): string { return 'Cache'; }
public function getDescription(): string { return 'Cache hits/misses'; }
public function collect(MvcEvent $event): void
{
$this->stats = $event->getApplication()
->getServiceManager()
->get('My\Cache\Stats')
->snapshot();
}
public function getStats(): array { return $this->stats; }
}
// module.config.php
'service_manager' => [
'factories' => [
CacheCollector::class => Laminas\ServiceManager\Factory\InvokableFactory::class,
],
],
'laminas-developer-tools' => [
'collectors' => [
'cache' => CacheCollector::class,
],
'toolbar' => [
'entries' => [
'cache' => ['label' => 'Cache', 'tooltip' => 'Cache stats'],
],
],
],
Copy TimeCollector or MemoryCollector as the template for the toolbar entry contract; that is faster and more reliable than guessing method names. For the detail panel in the report, add or override a template under the same view namespace.
Use the EventManager for timing without writing a collector. If you only need to see how long a service or listener takes, expose it as an event and let the EventCollector surface it, or attach your own listener on MvcEvent::EVENT_FINISH that writes into a custom collector. This keeps instrumentation out of your domain code.
Custom storage. The profiler persists reports through a StorageInterface implementation (file storage by default). If you run multiple app servers or containerized dev environments, point the storage at shared storage, or accept that the report link only works on the node that served the request. Override the profiler.storage class in config if you need Redis/S3-backed profiles.
Read the report as a workflow, not a dashboard. Typical triage order: Time (where did the request go) → Db (N+1 and slow queries) → Memory (peak) → Event (listener fan-out) → Config (is the value you think is set actually set). The config panel catches more "why is this behaving differently" bugs than the profiler panels do.
text/html responses. JSON APIs, file downloads, redirects, streamed responses, and many error pages get no toolbar. This is the most common "it's not working" report. Check response content type before debugging anything else./laminas-developer-tools/...). It dumps SQL, config, session, and mail metadata. Never let the module load in production; if staging is publicly reachable, protect the route.setContent() on EVENT_FINISH after the toolbar listener, silently removes it.PHP_SAPI === 'cli' or keep the module out of those entry points entirely.composer install --no-dev is the failure mode to design against. If the module is referenced from the main modules.config.php and the package is not installed, you get a class-not-found fatal on every request. The dev-config pattern above prevents this; verify it once with a clean --no-dev install.memory_get_peak_usage tells you nothing about the DB, the web server, or OPcache. Do not chase a memory "regression" that is really just a larger config or a warmer autoloader between two requests.DbCollector only understands laminas-db. Doctrine DBAL/ORM, PDO used directly, or a custom query layer will not appear. Write a collector that reads your ORM's SQL logger or a Doctrine\DBAL\Logging\SQLLogger implementation.forward() and sub-requests produce multiple profiler tokens. The toolbar may show only one of them; use the report list to find the others.template_map is safer than editing vendor. Report and toolbar templates are ordinary view scripts in the module namespace; map your own versions in view_manager.template_map so composer update does not wipe your changes.collectors entry → toolbar.entries entry → optional template map. Missing any one of these gives you a collector that runs (or doesn't) with no visible symptom.How can I help you explore Laravel packages today?