farikd / mongodb-profiler-bundle
Dev-only MongoDB query profiler for Symfony: web-profiler panel, on-demand explain, N+1 detection and a CLI summary — for the raw mongodb/mongodb driver.
Package info
github.com/farikd/mongodb-profiler-bundle
Type:symfony-bundle
pkg:composer/farikd/mongodb-profiler-bundle
Requires
- php: >=8.4
- ext-mongodb: *
- mongodb/mongodb: ^2.0
- symfony/config: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher: ^7.4 || ^8.0
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/twig-bundle: ^7.4 || ^8.0
- twig/twig: ^3.10
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.64
- phpstan/phpstan: ^2.0
- phpunit/phpunit: ^13.0
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/web-profiler-bundle: ^7.4 || ^8.0
- symfony/yaml: ^7.4 || ^8.0
Suggests
- symfony/web-profiler-bundle: To render the profiler panel (dev only)
README
A dev-only MongoDB query profiler for Symfony, for apps using the raw mongodb/mongodb
driver (no Doctrine ODM). It hooks the driver's own command-monitoring API, so it sees
every command issued in the process — direct collection()->find() calls, health-check
pings, anything — regardless of which repository or service issued it.
It gives you:
- A web-profiler panel listing every MongoDB command in the request: operation, collection, filter/pipeline (pretty-printed, BSON-aware), duration, and errors.
- N+1 / duplicate detection — byte-identical (operation, collection, filter) commands seen more than once in the same request, grouped and counted, with the repository call site that issued them.
- A bounded query cap (
max_queries) so a runaway loop can't blow up memory: past the cap, totals and duplicate counts stay exact, but individual rows stop being stored. - On-demand
explain— click a capturedfind/aggregate/countin the panel to re-run it withexecutionStatsand see whether it used an index (IXSCAN) or scanned the collection (COLLSCAN), plus docs-examined/docs-returned. - A CLI/messenger summary — the same counts and duplicate groups, printed at the end
of a console command or
messenger:consumerun, when opted in.
It is dev/test tooling only. With enabled: false (or outside kernel.debug, its
default), the bundle registers no services at all — a production container is
identical to one without the package installed.
Requirements
- PHP >= 8.4 with
ext-mongodb mongodb/mongodb^2.0- Symfony ^7.4 || ^8.0
Symfony 7.3 is not supported: it is end-of-life and every 7.3.x release carries unpatched security advisories, so Composer's default policy refuses to install it anyway.
symfony/web-profiler-bundle is optional — without it everything still works except the
panel (the CLI summary, in particular, does not need it).
Installation
composer require --dev farikd/mongodb-profiler-bundle
// config/bundles.php return [ // ... Farikd\MongodbProfilerBundle\MongodbProfilerBundle::class => ['dev' => true, 'test' => true], ];
Configuration reference
All keys are optional; every default is shown below.
# config/packages/mongodb_profiler.yaml mongodb_profiler: # Register the profiler services at all. Defaults to %kernel.debug%, so it is # normally left unset — set it explicitly only to force it on/off. enabled: null # Record during CLI runs (console commands, messenger workers). The web (HTTP) # path records unconditionally; CLI opts in explicitly so ordinary crons and # consumers pay nothing by default. cli: false # Per-request cap on individually stored commands. Totals and duplicate-group # counts stay exact past it; only per-row storage (and its backtrace) stops. max_queries: 2000 # Command names never captured. Replaces the built-in list wholesale — it is # NOT merged with it. See "What it captures / what it ignores" below for the # built-in default and why `ping` is deliberately absent from it. ignored_commands: - isMaster - ismaster - hello - saslStart - saslContinue - buildInfo - getMore # Extra class-name prefixes to skip when picking the "caller" frame from the # backtrace, on top of this package's own namespace and `MongoDB\`. Use this to # push the reported origin past your own repository/persistence-layer plumbing, # e.g. ['App\Infrastructure\Persistence\']. ignored_trace_prefixes: [] # Connection used to re-run a captured read with `explain`. Leaving either # unset disables the feature: the explain services — including the controller # behind the route below — are never registered, and the panel offers no # Explain button. explain: uri: null database: null
Routes
The bundle never registers routes itself — a bundle registering routes into an app
regardless of environment is exactly the kind of surprise a dev-only tool must not cause.
Import them yourself, scoped to dev/test:
# config/routes/mongodb_profiler.yaml when@dev: mongodb_profiler: resource: '@MongodbProfilerBundle/config/routes.php' when@test: mongodb_profiler: resource: '@MongodbProfilerBundle/config/routes.php'
This is what makes the panel's Explain button work; without it the panel still renders, just without that button.
Security
The explain endpoint lives under /_mongodb-profiler/. If your firewall guards
everything by default, carve out an exception the same way the standard _profiler/_wdt
paths already get one:
# config/packages/security.yaml security: firewalls: mongodb_profiler: pattern: ^/_mongodb-profiler security: false
CLI usage
Set mongodb_profiler.cli: true to also profile console commands and
messenger:consume workers — off by default, so ordinary crons and consumers pay
nothing. With it on, every command prints a summary to stdout when it terminates
(console.terminate):
MongoDB profile
================
12 command(s) in 34.56 ms
------- ------------ ------- ----------
op collection count total ms
------- ------------ ------- ----------
find videos 8 28.10
insert videos 4 6.46
------- ------------ ------- ----------
Failed commands, duplicate/N+1 groups, and a capped-query warning are appended the same
way they appear in the web panel. No config check gates the print itself: the subscriber
only records anything in CLI when cli is true, so a non-empty count already means
profiling was on, and a plain run records (and prints) nothing.
What it captures / what it ignores
Capture attaches at the MongoDB PHP driver's command-monitoring layer — the only hook
that sees every command a process issues, direct collection()->countDocuments() calls
and health-check pings included, with the driver's own measured duration for free.
A handful of connection-handshake / topology commands are ignored by default (see
ignored_commands above): isMaster/ismaster, hello, saslStart/saslContinue,
buildInfo, and getMore — internal chatter that is never something your code issued
on purpose.
ping is deliberately not in that list. An application-issued ping (e.g. a health
check that calls getDatabase()->command(['ping' => 1]) straight past its repository
layer) is exactly the driver-level capture this profiler exists to prove works — and the
PHP driver issues no background heartbeat pings of its own, so there is no noise to filter
out by excluding it.