zim32/xhprof-profiler-bundle
Symfony bundle that integrates XHProf profiling into the Web Debug Toolbar and Profiler, helping you capture and inspect function call graphs, timings, and performance data to identify bottlenecks in PHP applications.
This is a Symfony bundle, so map your Laravel instincts before you start: a bundle is a package plus a ServiceProvider, config/packages/*.yaml is config/*.php, and the Symfony Web Profiler toolbar is the analog of Laravel Telescope/Debugbar. The bundle does one thing: it registers an XHProf data collector so a per-request function-level profile shows up as a panel in the debug toolbar and profiler.
Prerequisites, in order:
Install the PHP extension, not just the package. This bundle only presents data; it does not collect it. You need the open-source tideways_xhprof PECL extension:
pecl install tideways_xhprof
# then add extension=tideways_xhprof.so to your php.ini
php -m | grep tideways_xhprof # must print a line
Do not confuse this with the commercial tideways extension (SaaS APM) — that one is a different extension and will not satisfy this bundle.
Require it as a dev dependency:
composer require --dev zim32/xhprof-profiler-bundle
With Symfony Flex it should be registered automatically in config/bundles.php. If not, add the bundle class from the package's src/ namespace manually — and only inside the dev/test arrays.
Confirm the config root. Because this package is small and thinly documented, dump the reference before guessing:
php bin/console config:dump-reference | grep -i xhprof
Then check the bundle's DependencyInjection/Configuration.php in vendor/zim32/ for the exact key, default flags, and enable switch.
Where to look first: the package README (short), then DependencyInjection/Configuration.php for options, then whatever EventSubscriber/DataCollector class exists — that tells you exactly when tideways_xhprof_enable() fires and where results are stored. Don't trust blog posts; this package is too obscure for them to be accurate.
First use case: open a slow page in dev with the toolbar visible, click the XHProf panel, and read the function table — inclusive vs. exclusive wall time, CPU time, and memory per function. You are looking for the function that owns the most exclusive time, not the one at the top of the inclusive list (that's always the kernel).
Dev-only gating is the whole pattern. Never enable this in production: the extension adds meaningful overhead and the panel exposes internal call structure. Keep the bundle registration in config/bundles.php limited to ['dev', 'test' => true] and, if the bundle has an enabled flag, drive it from %kernel.debug% or an env var in config/packages/dev/.
Always-on in dev vs. on-demand. The low-friction mode is to let it profile every request in dev — you get a panel on whatever page is slow without re-running anything. If the overhead bothers you or profiles get noisy, gate collection behind a header or query param in a small kernel-request listener that calls tideways_xhprof_enable(...) yourself and skips the bundle's automatic start.
Use it as a complement, not a replacement. Symfony's built-in panels already give you the request timeline, DB queries, and Twig render costs. XHProf's value is the call tree: which internal function inside your service is burning CPU, whether array_merge in a loop or an accidental O(n²) is the culprit. Read the timeline panel first to find the slow phase, then XHProf to find the function.
Before/after comparison is the workflow. Profile the endpoint, note inclusive CPU/wall time for the suspect function, change the code, re-profile, compare the same row. Treat the numbers as relative, not absolute — they vary run to run. If you need durable storage and side-by-side diffs across many runs, this bundle is the wrong tool; reach for XHGui or a profiler that persists runs.
CLI profiling needs a manual shim. The bundle is built around the HTTP kernel and the profiler, so console commands generally won't get a panel. Wrap the work yourself:
tideways_xhprof_enable(TIDEWAYS_XHPROF_FLAGS_CPU | TIDEWAYS_XHPROF_FLAGS_MEMORY);
// ... run the command logic ...
$data = tideways_xhprof_disable();
file_put_contents('/tmp/profile.json', json_encode($data));
Then feed the JSON into a viewer (or tideways_xhprof's callgraph tooling). This is usually the highest-value use of XHProf for queue workers and import scripts, and it's the part the bundle doesn't cover.
Override the panel if the default view is thin. Symfony lets you override bundle templates by placing a file at templates/bundles/<BundleName>/.... If the shipped panel lacks a column you want (peak memory, call counts), copy the template and extend it. Likewise, you can decorate the bundle's data collector service in config/services.yaml to post-process or filter the raw data before rendering.
tideways_xhprof_enable is undefined, you get a fatal error in the listener or, worse, an empty panel with no explanation. First debugging step is always php -m | grep tideways.framework.profiler.collect: false (common on staging-ish dev setups) or the web_profiler toolbar is disabled, the collector may never run. Check both before blaming the bundle.tideways vs tideways_xhprof. Two different PECL extensions. Installing the commercial one does not give you these functions, and having both loaded can produce confusing function tables. Confirm which .so is in php -m.test env, functional tests get slower and memory-heavy profiles they don't need. Either scope it to dev only or disable collection in config/packages/test/.config:dump-reference and the composer.json constraints over any external source. If your Symfony major version isn't in the constraint but the code is trivial, composer require --dev --ignore-platform-reqs or a local fork is a reasonable escape hatch — just pin it.dev, pin a version in composer.lock, and if you need real extensions (custom metrics, persistence), fork rather than wait.tideways_xhprof_disable() output is large. On a heavy request it can be tens of MB of arrays. If you're dumping to JSON for later analysis, gzip it and prune to functions above a time threshold; otherwise you'll fill /tmp fast.How can I help you explore Laravel packages today?