Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
workflow_dispatch:

jobs:
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,22 @@ It brings `fruitcake/laravel-debugbar` with it. Nothing runs unless Laravel Debu

| Tab | Origin | Shows |
| --- | --- | --- |
| Pollora | Pollora | What answered (`Route::wp()`, the template hierarchy and its view, a Laravel route, or WordPress alone), versions, discovery, modules, theme, async actions |
| Pollora | Pollora | What answered (`Route::wp()`, the template hierarchy and its view, a Laravel route, or WordPress alone), versions, discovery, modules, theme, async actions registered and queued, WordPress constants and drop-ins |
| Doctor | Pollora | A **Run doctor** button: `pollora:doctor`'s web checks on demand, errors first |
| WP Request | WordPress | Rewrite rule, query vars, queried object, main query, true conditionals, template and hierarchy candidates |
| WP Queries | WordPress | `$wpdb` queries with time, caller, duplicates, slow ones and the main query (turns `SAVEQUERIES` on) |
| WP Queries | WordPress | `$wpdb` queries with time, full backtrace, rows, errors, duplicates, slow ones and the main query, grouped by component (core, plugin, theme…) |
| WP Hooks | WordPress | Hooks that ran, their callbacks, and those Pollora registered |
| WP HTTP | WordPress | `wp_remote_*` calls: result, time, transport, who made them |
| WP Cache | WordPress | Object cache hits and misses, transients set, OPcache |
| WP Capabilities | WordPress | `current_user_can()` checks, each distinct check once with its count |
| WP Blocks | WordPress | Blocks rendered by type with their time, Pollora's Blade blocks, block bindings |
| WP Assets | WordPress | Scripts, styles and script modules, header or footer, missing dependencies, Vite builds |
| WP Languages | WordPress | Locale and the translation files looked for |
| Timeline | WordPress | `muplugins_loaded` to `shutdown`, beside Debugbar's own measures |

REST and admin-ajax requests end with `exit`, which Laravel Debugbar never sees: this package stores them and sends the `phpdebugbar-id` header, so they appear in the bar's request list of the page that made the call.
REST and admin-ajax requests end with `exit`, which Laravel Debugbar never sees: this package stores them and sends the `phpdebugbar-id` header, so they appear in the bar's request list of the page that made the call. A `wp_redirect()` keeps its request for the page it leads to.

WordPress queries are traced through core's `log_query_custom_data` and `query` filters, so this works with any `db.php` drop-in, Pollora's included.

Configuration: `php artisan vendor:publish --tag=debugbar-pollora-config`.

Expand Down
21 changes: 21 additions & 0 deletions config/debugbar-pollora.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
| wp_queries $wpdb queries (turns SAVEQUERIES on)
| wp_hooks Hooks that ran and the callbacks Pollora registered
| wp_timeline WordPress phases on Debugbar's timeline
| wp_http HTTP calls made through wp_remote_*
| wp_cache Object cache, transients set, OPcache
| wp_capabilities current_user_can() checks, aggregated
| wp_blocks Blocks rendered and block bindings resolved
| wp_assets Scripts, styles, script modules and Vite containers
| wp_languages Locale and translation files
| doctor A button that runs pollora:doctor's web checks on demand
| bridges pollora/debugbar/* and Query Monitor's qm/* actions
|
*/
Expand All @@ -37,6 +44,13 @@
'wp_queries' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_QUERIES', true),
'wp_hooks' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_HOOKS', true),
'wp_timeline' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_TIMELINE', true),
'wp_http' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_HTTP', true),
'wp_cache' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_CACHE', true),
'wp_capabilities' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_CAPABILITIES', true),
'wp_blocks' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_BLOCKS', true),
'wp_assets' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_ASSETS', true),
'wp_languages' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_LANGUAGES', true),
'doctor' => env('DEBUGBAR_POLLORA_COLLECTORS_DOCTOR', true),
'bridges' => env('DEBUGBAR_POLLORA_COLLECTORS_BRIDGES', true),
],

Expand All @@ -49,6 +63,13 @@
'soft_limit' => (int) env('DEBUGBAR_POLLORA_WP_QUERIES_SOFT_LIMIT', 100),
// Past this many queries, the rest are left out
'hard_limit' => (int) env('DEBUGBAR_POLLORA_WP_QUERIES_HARD_LIMIT', 500),
// Full backtrace, error, rows and component for each query
'trace' => env('DEBUGBAR_POLLORA_WP_QUERIES_TRACE', true),
],

'wp_capabilities' => [
// Who asked: a backtrace per distinct check
'backtrace' => env('DEBUGBAR_POLLORA_WP_CAPABILITIES_BACKTRACE', false),
],

'wp_hooks' => [
Expand Down
95 changes: 95 additions & 0 deletions resources/doctor-widget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/*
* The Doctor tab: a button that runs pollora:doctor's web checks on demand
* and lists what they found, errors first.
*/
(function () {
if (! window.PhpDebugBar || PhpDebugBar.Widgets.PolloraDoctorWidget) {
return;
}

const csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-widgets-');

class PolloraDoctorWidget extends PhpDebugBar.Widget {
get className() {
return csscls('pollora-doctor');
}

render() {
const toolbar = document.createElement('div');
toolbar.style.padding = '8px';

const button = document.createElement('button');
button.type = 'button';
button.textContent = 'Run doctor';

const status = document.createElement('span');
status.style.marginLeft = '8px';
status.textContent = 'Runs pollora:doctor\'s web checks for this site.';

const table = document.createElement('table');
table.classList.add(csscls('tablevar'));
table.style.width = '100%';

toolbar.append(button, status);
this.el.append(toolbar, table);

button.addEventListener('click', async () => {
const url = this.get('data')?.url;

if (! url) {
return;
}

button.disabled = true;
status.textContent = 'Running…';
table.innerHTML = '';

try {
const response = await fetch(url, { headers: { Accept: 'application/json' }, credentials: 'same-origin' });
const text = await response.text();
const isJson = (response.headers.get('content-type') || '').includes('json');

// A proxy or a restarting server answers with plain text or
// HTML: say what came back rather than a JSON syntax error
if (! isJson) {
throw new Error('HTTP ' + response.status + ': ' + text.replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim().slice(0, 120));
}

const body = JSON.parse(text);

if (! response.ok) {
throw new Error(body.message || 'HTTP ' + response.status);
}

const counts = {};

for (const check of body.checks) {
counts[check.status] = (counts[check.status] || 0) + 1;

const row = document.createElement('tr');
row.classList.add(csscls('item'));

const details = (check.details || []).join(' · ');

for (const value of [check.status, check.label, [check.summary, details, check.fix ? 'Fix: ' + check.fix : ''].filter(Boolean).join(' — ')]) {
const cell = document.createElement('td');
cell.classList.add(csscls('value'));
cell.textContent = value;
row.append(cell);
}

table.append(row);
}

status.textContent = Object.entries(counts).map(([name, count]) => count + ' ' + name).join(', ') || 'No web checks.';
} catch (error) {
status.textContent = 'The doctor could not run: ' + error.message;
} finally {
button.disabled = false;
}
});
}
}

PhpDebugBar.Widgets.PolloraDoctorWidget = PolloraDoctorWidget;
})();
17 changes: 16 additions & 1 deletion src/Collector.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ public function getWidgets(): array
}

/**
* @return array<string, string>
* @return array<string, mixed>
*/
public function getAssets(): array
{
Expand All @@ -150,6 +150,21 @@ public function getAssets(): array

public function reset(): void {}

/**
* A path relative to the project, so the bar names a file someone can open.
*/
protected function relativePath(string $path): string
{
try {
$base = function_exists('base_path') ? rtrim(base_path(), '/').'/' : '';
} catch (\Throwable) {
// No application to ask: show the path as it is
return $path;
}

return $base !== '' && str_starts_with($path, $base) ? substr($path, strlen($base)) : $path;
}

/**
* What the tab shows, in the shape its widget takes.
*
Expand Down
49 changes: 48 additions & 1 deletion src/CollectorRegistrar.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,28 @@
use DebugBar\DataCollector\TimeDataCollector;
use Fruitcake\LaravelDebugbar\LaravelDebugbar;
use Illuminate\Contracts\Container\Container;
use Pollora\Debugbar\Collectors\DoctorCollector;
use Pollora\Debugbar\Collectors\PolloraCollector;
use Pollora\Debugbar\Collectors\WpAssetsCollector;
use Pollora\Debugbar\Collectors\WpBlocksCollector;
use Pollora\Debugbar\Collectors\WpCacheCollector;
use Pollora\Debugbar\Collectors\WpCapabilitiesCollector;
use Pollora\Debugbar\Collectors\WpHooksCollector;
use Pollora\Debugbar\Collectors\WpHttpCollector;
use Pollora\Debugbar\Collectors\WpLanguagesCollector;
use Pollora\Debugbar\Collectors\WpQueriesCollector;
use Pollora\Debugbar\Collectors\WpRequestCollector;
use Pollora\Debugbar\Collectors\WpTimelineCollector;
use Pollora\Debugbar\Contracts\SectionProvider;
use Pollora\Debugbar\Recording\AsyncRecorder;
use Pollora\Debugbar\Recording\BlockRecorder;
use Pollora\Debugbar\Recording\CacheRecorder;
use Pollora\Debugbar\Recording\CapabilityRecorder;
use Pollora\Debugbar\Recording\HttpRecorder;
use Pollora\Debugbar\Recording\LanguageRecorder;
use Pollora\Debugbar\Recording\QueryTracer;
use Pollora\Debugbar\Recording\RequestRecorder;
use Pollora\Debugbar\Support\Components;
use Pollora\Hook\Domain\Contract\Action;
use Pollora\Hook\Domain\Contract\Filter;

Expand Down Expand Up @@ -98,7 +113,7 @@ private function builtIns(LaravelDebugbar $debugbar): array
$collectors = [];

if ($on('pollora')) {
$collectors[] = new PolloraCollector($this->container);
$collectors[] = new PolloraCollector($this->container, $this->container->make(AsyncRecorder::class));
}

if ($on('wp_request')) {
Expand All @@ -112,13 +127,45 @@ private function builtIns(LaravelDebugbar $debugbar): array
is_numeric($threshold) ? (float) $threshold : null,
(int) $config->get('debugbar-pollora.options.wp_queries.soft_limit', 100),
(int) $config->get('debugbar-pollora.options.wp_queries.hard_limit', 500),
$this->container->make(Components::class),
$config->get('debugbar-pollora.options.wp_queries.trace', true) ? $this->container->make(QueryTracer::class) : null,
);
}

if ($on('wp_hooks')) {
$collectors[] = new WpHooksCollector($this->recorder, $this->polloraHookServices());
}

$components = $this->container->make(Components::class);

if ($on('wp_http')) {
$collectors[] = new WpHttpCollector($this->container->make(HttpRecorder::class), $components);
}

if ($on('wp_cache')) {
$collectors[] = new WpCacheCollector($this->container->make(CacheRecorder::class), $components);
}

if ($on('wp_capabilities')) {
$collectors[] = new WpCapabilitiesCollector($this->container->make(CapabilityRecorder::class), $components);
}

if ($on('wp_blocks')) {
$collectors[] = new WpBlocksCollector($this->container->make(BlockRecorder::class));
}

if ($on('wp_assets')) {
$collectors[] = new WpAssetsCollector($this->container);
}

if ($on('wp_languages')) {
$collectors[] = new WpLanguagesCollector($this->container->make(LanguageRecorder::class));
}

if ($on('doctor')) {
$collectors[] = new DoctorCollector('/'.trim((string) $config->get('debugbar.route_prefix', '_debugbar'), '/').'/pollora/doctor');
}

if ($on('wp_timeline') && $debugbar->hasCollector('time')) {
$time = $debugbar->getCollector('time');

Expand Down
82 changes: 82 additions & 0 deletions src/Collectors/DoctorCollector.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Pollora\Debugbar\Collectors;

use Pollora\Debugbar\Collector;
use Pollora\Debugbar\Origin;

/**
* The Doctor tab: a button that runs `pollora:doctor`'s web checks on demand.
*
* Collects nothing but the address to call, so the page pays nothing for it.
* Its widget is inlined in the page, since Debugbar's assets request only
* knows the collectors registered while it runs.
*/
final class DoctorCollector extends Collector
{
public function __construct(
private readonly string $url,
) {}

public function getName(): string
{
return 'pollora_doctor';
}

public function title(): string
{
return 'Doctor';
}

public function origin(): string
{
return Origin::POLLORA;
}

public function icon(): string
{
return 'bug';
}

public function position(): int
{
return 1;
}

/**
* @return array{data: array{url: string}, count: null}
*/
public function collect(): array
{
return ['data' => $this->data(), 'count' => null];
}

/**
* @return array<string, array<string, mixed>>
*/
public function getWidgets(): array
{
$widgets = parent::getWidgets();
$widgets['pollora_doctor']['widget'] = 'PhpDebugBar.Widgets.PolloraDoctorWidget';

return $widgets;
}

/**
* @return array{inline_js: array<string, string>}
*/
public function getAssets(): array
{
return ['inline_js' => ['pollora-doctor-widget' => (string) file_get_contents(__DIR__.'/../../resources/doctor-widget.js')]];
}

/**
* @return array{url: string}
*/
protected function data(): array
{
return ['url' => $this->url];
}
}
Loading
Loading