Skip to content

Debugging

See WordPress queries, hooks, the template or route that answered and REST calls in Laravel Debugbar with pollora/debugbar, and add your own tabs.

pollora/debugbar puts WordPress and Pollora in Laravel Debugbar. Next to Laravel’s own tabs, every page shows which route or template answered it, the queries WordPress ran through $wpdb, the hooks that fired, how WordPress parsed the request, and WordPress’s phases on the timeline. REST and admin-ajax calls appear in the bar’s request list too. The aim is to make Query Monitor unnecessary in a Pollora project.

Terminal window
composer require --dev pollora/debugbar

It brings fruitcake/laravel-debugbar with it and needs Pollora 13.35.3 or later. Nothing else to register: the service provider is discovered.

It is a development dependency on purpose. composer install --no-dev leaves it out, so a production deployment never ships it.

Exactly when Laravel Debugbar does: DEBUGBAR_ENABLED, or APP_DEBUG when that is not set, and never in the production or testing environments. Turning the bar off turns everything here off, including SAVEQUERIES.

To keep Laravel’s tabs and drop Pollora’s, set DEBUGBAR_POLLORA_ENABLED=false.

Pollora’s tab comes right after Laravel’s, then the WordPress tabs, prefixed WP, then those added by plugins and packages.

TabShows
PolloraWhat answered the request — a Route::wp() route and its condition, the template hierarchy with the Blade view and the conditional that picked it, a Laravel route, or WordPress alone — then the versions, discovery (where each location’s classes came from and how long it took), modules, the theme, async actions registered and queued by this request, WordPress constants and drop-ins
DoctorA Run doctor button that runs pollora:doctor’s web checks on demand and lists them, errors first. Nothing runs with the page
WP RequestThe rewrite rule that matched, query vars, the queried object, the main query and its results, the conditional tags that are true, the template file and the candidates of each template hierarchy. In wp-admin, the admin page and screen. On a multisite, the site and network, every switch between sites and who made it, and a warning when the request ends still switched
WP QueriesThe queries WordPress ran through $wpdb, with their time, full backtrace, rows, errors, duplicates, slow ones, and the main query marked, grouped by component: core, a plugin, a theme, a module, the application. Laravel’s Queries tab keeps Eloquent’s
WP HooksThe actions that fired and how often, how many callbacks each had, and the callbacks Pollora registered, by class and method
WP Hook timingsOff by default. The slowest hook callbacks, by their own time (without the hooks they fire in turn) and in total, with their hook, priority, calls and component
WP HTTPThe calls made through wp_remote_*: result, time, transport, and who made them; a call a plugin answered in pre_http_request says so
WP CacheObject-cache hits and misses, whether the cache is persistent, the transients set and who set them, OPcache
WP CapabilitiesThe current_user_can() checks: each distinct check once, granted or refused, and how many times it was made
WP BlocksThe blocks rendered by type, with their time and nesting; Pollora’s Blade blocks marked; the block bindings that gave them values
WP AssetsScripts, styles and script modules, printed in the header or the footer, with dependencies nobody registered; the Vite build or dev server of each theme, plugin and module
WP LanguagesThe locale, and the translation files WordPress looked for, found or not
TimelineWordPress loading and the callbacks of muplugins_loaded, init, wp_loaded, template_redirect, wp_head… beside Laravel’s measures

WordPress queries are traced through two core filters, log_query_custom_data and query, rather than a wpdb class of its own: it works with Pollora’s db.php drop-in, and with any other.

A WordPress REST request or an admin-ajax call ends with exit before Laravel finishes the response, so Laravel Debugbar alone never records it. With this package, the request is stored and its id sent in the phpdebugbar-id header: a fetch() or XHR made from a page with the bar shows up in the bar’s request list, with all its tabs.

A wp_redirect() exits too: its request is kept, and the page it leads to shows both.

Stored requests are opened through _debugbar/open, and the doctor through _debugbar/pollora/doctor; Laravel Debugbar limits both to local and private addresses by default.

WordPress prints admin pages itself, without Laravel’s kernel, so Laravel Debugbar alone never shows there. With this package, the bar is printed at the bottom of admin pages and the request is stored like any other. Laravel’s tabs about a request it answered (route, views, session) are left out. The REST calls the block editor makes are listed in the bar’s request list. Set DEBUGBAR_POLLORA_ADMIN=false to keep wp-admin without the bar. The front end the Site Editor shows in its canvas gets no second bar; open that request from the bar’s request list.

Terminal window
php artisan vendor:publish --tag=debugbar-pollora-config

config/debugbar-pollora.php turns each tab on or off and sets their options. Each key also reads an environment variable:

KeyDefaultEffect
collectors.wp_queriestrueAlso turns SAVEQUERIES on; off, WordPress keeps no queries
options.wp_queries.slow_threshold50Milliseconds from which a query is highlighted
options.wp_queries.soft_limit / hard_limit100 / 500Past the first, no caller is kept; past the second, queries are left out
options.wp_queries.tracetrueFull backtrace, error, rows and component for each query
options.wp_hooks.count_filtersfalseCount filters too. It listens to every hook call, so it costs on every apply_filters()
options.wp_hooks.timingsfalseTime every hook callback (WP Hook timings). Each callback is wrapped in place in $wp_filter, so code reading $wp_filter directly sees the wrapper; callbacks taking parameters by reference are not timed
options.wp_hooks.timings_limit200How many callbacks the timings tab lists
iframesfalsePrint the bar inside pages loaded in an iframe (Site Editor canvas, Customizer preview). Off, those requests are still stored and open from the bar’s request list
admin.enabledtrueShow the bar on wp-admin pages
admin.hidden_collectorsroute, views, session, livewire, inertiaLaravel Debugbar tabs left out in wp-admin
options.wp_capabilities.backtracefalseSay who made each distinct capability check
options.bridges.query_monitortrueKeep Query Monitor’s qm/* logging actions working

Every tab says where its data comes from: Pollora, WordPress, or the name you give. Tab names starting with wp_ or pollora are reserved; prefix yours with your own name.

Use actions: they need no dependency on the package, and do nothing where it is not installed.

add_action('pollora/debugbar/register', function ($bar): void {
// A tab of rows
$bar->table('acme_cart', 'Acme cart', fn (): array => acme_cart_rows(), origin: 'acme-shop');
// A tab of name => value pairs
$bar->variables('acme_info', 'Acme', fn (): array => ['mode' => 'test'], origin: 'acme-shop');
// A section in an existing tab
$bar->section('wp_request', 'Acme', fn (): array => ['Cart' => acme_cart_id()]);
});
do_action('pollora/debugbar/message', 'Cart {id} rebuilt', 'info', ['id' => $cartId]);
do_action('pollora/debugbar/start', 'acme-sync');
// …
do_action('pollora/debugbar/stop', 'acme-sync');

The closures run when the bar collects, at the end of the request.

Extend Pollora\Debugbar\Collector and tag the class:

use Pollora\Debugbar\Collector;
use Pollora\Debugbar\CollectorRegistrar;
use Pollora\Debugbar\Widget;
final class CartCollector extends Collector
{
public function getName(): string { return 'acme_cart'; }
public function title(): string { return 'Acme cart'; }
public function origin(): string { return 'acme-shop'; }
public function widget(): Widget { return Widget::Table; }
public function columns(): array { return ['qty' => 'Quantity']; }
protected function data(): array
{
return ['apple' => ['qty' => 3]];
}
}
// In a service provider's register(), when the package is installed
if (class_exists(Collector::class)) {
$this->app->tag([CartCollector::class], CollectorRegistrar::COLLECTORS_TAG);
}

widget() is Widget::Variables (the default), Widget::Table or Widget::Queries (php-debugbar’s SQL statement shape). icon() takes one of the icons php-debugbar ships, such as box, table, tags or bolt. To add a section to an existing tab instead, implement Pollora\Debugbar\Contracts\SectionProvider and tag it CollectorRegistrar::SECTIONS_TAG.

Debugbar::addCollector(), debugbar.custom_collectors, Debugbar::addMessage() and startMeasure() work as usual. Those tabs keep Debugbar’s place, among Laravel’s.

The two can run side by side while you switch. What Query Monitor shows and where it is here:

Query MonitorHere
Queries, by caller and component, duplicates, errorsWP Queries
Request, conditionals, templateWP Request, and Pollora for the route or Blade view that answered
Hooks & actionsWP Hooks
HTTP API callsWP HTTP
Transients, object cacheWP Cache
Capability checksWP Capabilities, on by default here since checks are aggregated
BlocksWP Blocks
Scripts, stylesWP Assets
LanguagesWP Languages
EnvironmentPollora (versions, constants, drop-ins)
PHP errors, doing it wrongLaravel Debugbar’s Exceptions tab; WordPress’s notices go to the wordpress log channel (WordPress Logging)
Logs (qm/debug…) and timings (qm/start, qm/stop)Still work, in Messages and the Timeline
OverviewLaravel Debugbar’s time and memory
RedirectsKept: the next page shows both requests
Admin screenWP Request, in wp-admin
MultisiteWP Request, on a multisite

Query Monitor cannot install its own db.php next to Pollora’s, so its query panel loses callers and components in a Pollora project; WP Queries has them.