diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..c4c4edc
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,184 @@
+name: Tests
+
+on:
+ push:
+ branches: [ main, develop ]
+ pull_request:
+ branches: [ main, develop ]
+ workflow_dispatch:
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: true
+ matrix:
+ php: [8.3, 8.4]
+
+ name: PHP ${{ matrix.php }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ extensions: dom, curl, libxml, mbstring, zip, pdo_sqlite
+ coverage: none
+
+ - name: Use the unreleased framework (until v13.35.3 is tagged; feature branch until framework#442 is merged)
+ run: composer require --no-update --no-interaction "pollora/framework:dev-feat/debugbar-extension-points as 13.35.3"
+
+ - name: Install dependencies
+ run: composer update --prefer-stable --prefer-dist --no-interaction
+
+ - name: Execute tests
+ run: vendor/bin/pest
+
+ static-analysis:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: 8.3
+ extensions: dom, curl, libxml, mbstring, zip
+ coverage: none
+
+ - name: Use the unreleased framework (until v13.35.3 is tagged; feature branch until framework#442 is merged)
+ run: composer require --no-update --no-interaction "pollora/framework:dev-feat/debugbar-extension-points as 13.35.3"
+
+ - name: Install dependencies
+ run: composer update --prefer-stable --prefer-dist --no-interaction
+
+ - name: Run PHPStan
+ run: composer phpstan:ci
+
+ - name: Check coding standards with Pint
+ run: composer lint:check
+
+ e2e:
+ name: E2E
+ runs-on: ubuntu-latest
+ needs: tests
+
+ steps:
+ - name: Checkout skeleton
+ uses: actions/checkout@v4
+ with:
+ repository: Pollora/pollora
+
+ - name: Checkout this package
+ uses: actions/checkout@v4
+ with:
+ path: packages/debugbar
+
+ - name: Setup DDEV
+ uses: ddev/github-action-setup-ddev@v1
+ with:
+ autostart: false
+
+ - name: Configure DDEV
+ run: |
+ ddev config \
+ --project-name=pollora-debugbar \
+ --project-type=wordpress \
+ --docroot=public \
+ --php-version=8.4 \
+ --webserver-type=apache-fpm \
+ --database=mariadb:10.11 \
+ --nodejs-version=22 \
+ --disable-settings-management
+
+ - name: Start DDEV
+ run: ddev start
+
+ - name: Prepare .env
+ run: |
+ cp .env.example .env
+ sed -i \
+ -e 's#^APP_URL=.*#APP_URL=https://pollora-debugbar.ddev.site#' \
+ -e 's#^APP_ENV=.*#APP_ENV=local#' \
+ -e 's#^APP_DEBUG=.*#APP_DEBUG=true#' \
+ -e 's#^DB_CONNECTION=.*#DB_CONNECTION=mysql#' \
+ -e 's/^# DB_HOST=.*/DB_HOST=db/' \
+ -e 's/^# DB_PORT=.*/DB_PORT=3306/' \
+ -e 's/^# DB_DATABASE=.*/DB_DATABASE=db/' \
+ -e 's/^# DB_USERNAME=.*/DB_USERNAME=db/' \
+ -e 's/^# DB_PASSWORD=.*/DB_PASSWORD=db/' \
+ .env
+
+ # Until v13.35.3 is tagged, the framework comes from framework#442's branch.
+ - name: Install the skeleton with this package checkout
+ run: |
+ ddev composer config repositories.debugbar '{"type": "path", "url": "packages/debugbar", "options": {"symlink": false, "versions": {"pollora/debugbar": "0.1.0"}}}'
+ ddev composer require "pollora/framework:dev-feat/debugbar-extension-points as 13.35.3" --with-all-dependencies --no-interaction --no-progress --no-scripts
+ ddev composer require --dev pollora/debugbar:0.1.0 --with-all-dependencies --no-interaction --no-progress
+
+ - name: Generate application key
+ run: ddev exec php artisan key:generate --no-interaction
+
+ - name: Install WordPress
+ run: |
+ ddev exec php artisan pollora:install --install \
+ --title="Pollora" \
+ --description="Pollora Debugbar CI" \
+ --admin-user=admin \
+ --admin-email=admin@example.com \
+ --admin-password=pollora-ci-password \
+ --locale=en_US \
+ --public=false \
+ --no-interaction
+
+ - name: Add the plugin and package fixtures
+ run: |
+ mkdir -p public/content/mu-plugins
+ cp packages/debugbar/tests/e2e/fixtures/acme-debugbar.php public/content/mu-plugins/
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: npm
+ cache-dependency-path: packages/debugbar/tests/e2e/package-lock.json
+
+ - name: Install the browser tests
+ working-directory: packages/debugbar/tests/e2e
+ run: |
+ npm ci
+ npx playwright install --with-deps chromium
+
+ - name: Trust the site's certificate
+ run: |
+ if command -v mkcert >/dev/null && [ -f "$(mkcert -CAROOT)/rootCA.pem" ]; then
+ echo "NODE_EXTRA_CA_CERTS=$(mkcert -CAROOT)/rootCA.pem" >> "$GITHUB_ENV"
+ else
+ echo "::warning::No mkcert authority on the runner; certificate checks are off for this disposable site"
+ echo "NODE_TLS_REJECT_UNAUTHORIZED=0" >> "$GITHUB_ENV"
+ fi
+
+ - name: Browser tests
+ working-directory: packages/debugbar/tests/e2e
+ env:
+ E2E_HOME_URL: https://pollora-debugbar.ddev.site
+ run: npx playwright test
+
+ - name: Upload the browser test report
+ if: failure()
+ uses: actions/upload-artifact@v4
+ with:
+ name: e2e-report
+ path: |
+ packages/debugbar/tests/e2e/playwright-report
+ packages/debugbar/tests/e2e/test-results
+ retention-days: 14
+
+ - name: Laravel log
+ if: failure()
+ run: tail -n 200 storage/logs/laravel.log || true
diff --git a/.gitignore b/.gitignore
index b95361f..00eced4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,6 @@ composer.lock
patches.lock.json
.idea/
.phpunit.result.cache
+tests/e2e/node_modules/
+tests/e2e/playwright-report/
+tests/e2e/test-results/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0286e3f
--- /dev/null
+++ b/README.md
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+# Pollora Debugbar
+
+Puts WordPress and [Pollora](https://pollora.dev) in [Laravel Debugbar](https://github.com/fruitcake/laravel-debugbar): the `$wpdb` queries next to Eloquent's, the hooks that ran, how WordPress parsed the request, which route or template answered it, and WordPress's phases on the timeline. The long-term goal is to make Query Monitor unnecessary in a Pollora project.
+
+## Installation
+
+```bash
+composer require --dev pollora/debugbar
+```
+
+It brings `fruitcake/laravel-debugbar` with it. Nothing runs unless Laravel Debugbar is enabled for the request (`DEBUGBAR_ENABLED`, or `APP_DEBUG`; never in production or testing), and as a dev dependency it is not installed by `composer install --no-dev`.
+
+## Tabs
+
+| 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 |
+| 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 Hooks | WordPress | Hooks that ran, their callbacks, and those Pollora registered |
+| 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.
+
+Configuration: `php artisan vendor:publish --tag=debugbar-pollora-config`.
+
+## Adding your own data
+
+Tabs from Pollora, WordPress and third parties are told apart: Pollora's tab comes first, WordPress's are prefixed `WP`, and everyone else's come last. Names starting with `wp_` or `pollora` are reserved.
+
+**From a WordPress plugin or theme**, with no dependency on this package (without it, nothing fires the action):
+
+```php
+add_action('pollora/debugbar/register', function ($bar): void {
+ $bar->table('acme_cart', 'Acme cart', fn (): array => acme_cart_rows(), origin: 'acme-shop');
+ $bar->variables('acme_info', 'Acme', fn (): array => ['mode' => 'test'], origin: 'acme-shop');
+ $bar->section('wp_request', 'Acme', fn (): array => ['Cart' => acme_cart_id()]);
+});
+
+do_action('pollora/debugbar/message', 'Cart rebuilt', 'info', ['items' => 3]);
+do_action('pollora/debugbar/start', 'acme-sync');
+do_action('pollora/debugbar/stop', 'acme-sync');
+```
+
+Query Monitor's `qm/debug` … `qm/emergency`, `qm/start` and `qm/stop` actions keep working too.
+
+**From a package or module**, extend `Pollora\Debugbar\Collector` and tag it:
+
+```php
+final class CartCollector extends \Pollora\Debugbar\Collector
+{
+ public function getName(): string { return 'acme_cart'; }
+ public function title(): string { return 'Acme cart'; }
+ public function origin(): string { return 'acme-shop'; }
+ protected function data(): array { return ['items' => 3]; }
+}
+
+$this->app->tag([CartCollector::class], \Pollora\Debugbar\CollectorRegistrar::COLLECTORS_TAG);
+```
+
+`widget()` picks `Widget::Variables`, `Widget::Table` (with `columns()`) or `Widget::Queries`. A `Pollora\Debugbar\Contracts\SectionProvider` tagged `pollora.debugbar.sections` adds a section to an existing tab.
+
+**With Laravel Debugbar alone**, `Debugbar::addCollector()` and `debugbar.custom_collectors` work as usual.
+
+## License
+
+MIT. See [LICENSE](LICENSE).
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..2c7d322
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,82 @@
+{
+ "name": "pollora/debugbar",
+ "description": "Laravel Debugbar for Pollora: WordPress queries, hooks, the template hierarchy and Pollora's internals in the debug bar",
+ "type": "library",
+ "license": "MIT",
+ "keywords": [
+ "wordpress",
+ "laravel",
+ "debugbar",
+ "pollora",
+ "query-monitor"
+ ],
+ "authors": [
+ {
+ "name": "Amphibee",
+ "email": "contact@amphibee.fr"
+ }
+ ],
+ "require": {
+ "php": "^8.3",
+ "fruitcake/laravel-debugbar": "^4.4",
+ "pollora/framework": "^13.35.3"
+ },
+ "require-dev": {
+ "brain/monkey": "^2.7",
+ "laravel/pint": "^1.22",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^11.1",
+ "pestphp/pest": "^3.8",
+ "php-stubs/wordpress-stubs": "^7.1",
+ "phpstan/phpstan": "^2.1",
+ "szepeviktor/phpstan-wordpress": "^2.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "Pollora\\Debugbar\\": "src/"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Pollora\\Debugbar\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Pollora\\Debugbar\\DebugbarServiceProvider"
+ ]
+ }
+ },
+ "config": {
+ "sort-packages": true,
+ "allow-plugins": {
+ "cweagans/composer-patches": true,
+ "pestphp/pest-plugin": true,
+ "phpstan/extension-installer": true,
+ "pollora/helper-overrider": true,
+ "wikimedia/composer-merge-plugin": true
+ }
+ },
+ "scripts": {
+ "test": [
+ "@test:unit",
+ "@phpstan",
+ "@lint:check"
+ ],
+ "test:unit": "pest",
+ "phpstan": "phpstan analyse --memory-limit=1G",
+ "phpstan:ci": "phpstan analyse --memory-limit=1G --no-progress",
+ "lint": "pint",
+ "lint:check": "pint --test"
+ },
+ "scripts-descriptions": {
+ "test": "Run the whole quality gate as CI does",
+ "test:unit": "Run the Pest test suite",
+ "phpstan": "Run PHPStan static analysis",
+ "lint": "Apply Laravel Pint code style fixes",
+ "lint:check": "Verify code style without writing"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
+}
diff --git a/config/debugbar-pollora.php b/config/debugbar-pollora.php
new file mode 100644
index 0000000..51b2371
--- /dev/null
+++ b/config/debugbar-pollora.php
@@ -0,0 +1,64 @@
+ env('DEBUGBAR_POLLORA_ENABLED', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Tabs
+ |--------------------------------------------------------------------------
+ |
+ | pollora What answered the request, discovery, modules, theme, async
+ | wp_request Rewrite rule, query vars, main query, conditionals, templates
+ | 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
+ | bridges pollora/debugbar/* and Query Monitor's qm/* actions
+ |
+ */
+
+ 'collectors' => [
+ 'pollora' => env('DEBUGBAR_POLLORA_COLLECTORS_POLLORA', true),
+ 'wp_request' => env('DEBUGBAR_POLLORA_COLLECTORS_WP_REQUEST', true),
+ '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),
+ 'bridges' => env('DEBUGBAR_POLLORA_COLLECTORS_BRIDGES', true),
+ ],
+
+ 'options' => [
+
+ 'wp_queries' => [
+ // Milliseconds from which a query is highlighted; null for never
+ 'slow_threshold' => env('DEBUGBAR_POLLORA_WP_QUERIES_SLOW_THRESHOLD', 50),
+ // Past this many queries, no backtrace is kept
+ '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),
+ ],
+
+ 'wp_hooks' => [
+ // Count filters too: an `all` listener runs on every apply_filters()
+ 'count_filters' => env('DEBUGBAR_POLLORA_WP_HOOKS_COUNT_FILTERS', false),
+ ],
+
+ 'bridges' => [
+ // Keep Query Monitor's qm/* logging actions working
+ 'query_monitor' => env('DEBUGBAR_POLLORA_BRIDGES_QUERY_MONITOR', true),
+ ],
+ ],
+];
diff --git a/phpstan.neon b/phpstan.neon
new file mode 100644
index 0000000..65bc89d
--- /dev/null
+++ b/phpstan.neon
@@ -0,0 +1,12 @@
+includes:
+ - vendor/szepeviktor/phpstan-wordpress/extension.neon
+
+parameters:
+ level: 6
+ paths:
+ - src
+ ignoreErrors:
+ # Pollora's services are read defensively, so that a tab survives an
+ # older framework: method_exists() guards PHPStan cannot narrow on.
+ - '#Call to function method_exists\(\) with .* will always evaluate to true#'
+ reportUnmatchedIgnoredErrors: false
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..1d0f0ba
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,20 @@
+
+
+
+
+ tests/Unit
+
+
+ tests/Feature
+
+
+
+
+ src
+
+
+
diff --git a/pint.json b/pint.json
new file mode 100644
index 0000000..c03d7ed
--- /dev/null
+++ b/pint.json
@@ -0,0 +1,33 @@
+{
+ "preset": "laravel",
+ "rules": {
+ "declare_strict_types": true,
+ "ordered_imports": {
+ "sort_algorithm": "alpha"
+ },
+ "single_quote": true,
+ "array_syntax": {
+ "syntax": "short"
+ },
+ "ordered_class_elements": {
+ "order": [
+ "use_trait",
+ "case",
+ "constant_public",
+ "constant_protected",
+ "constant_private",
+ "property_public",
+ "property_protected",
+ "property_private",
+ "construct",
+ "method_public",
+ "method_protected",
+ "method_private"
+ ]
+ },
+ "no_unused_imports": true
+ },
+ "exclude": [
+ "vendor"
+ ]
+}
\ No newline at end of file
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..39a2b6e
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,6 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:base"
+ ]
+}
diff --git a/src/Activation.php b/src/Activation.php
new file mode 100644
index 0000000..97d9f7f
--- /dev/null
+++ b/src/Activation.php
@@ -0,0 +1,45 @@
+runningInConsole()) {
+ return false;
+ }
+
+ $config = $app->make('config');
+
+ if (! $config->get('debugbar-pollora.enabled', true)) {
+ return false;
+ }
+
+ if (! LaravelDebugbar::canBeEnabled()) {
+ return false;
+ }
+
+ // Debugbar merges its config in its own register(), which may come
+ // after this one: read its environment default when the key is not
+ // there yet.
+ $enabled = $config->has('debugbar.enabled')
+ ? value($config->get('debugbar.enabled'))
+ : env('DEBUGBAR_ENABLED');
+
+ return (bool) ($enabled ?? $config->get('app.debug'));
+ }
+}
diff --git a/src/Bridges/MessageBridge.php b/src/Bridges/MessageBridge.php
new file mode 100644
index 0000000..cd98885
--- /dev/null
+++ b/src/Bridges/MessageBridge.php
@@ -0,0 +1,104 @@
+message(...), 10, 3);
+ add_action('pollora/debugbar/start', $this->start(...), 10, 2);
+ add_action('pollora/debugbar/stop', $this->stop(...), 10, 1);
+
+ if (! $queryMonitor) {
+ return;
+ }
+
+ foreach (self::LEVELS as $level) {
+ add_action("qm/{$level}", fn (mixed $message, array $context = []) => $this->message($message, $level, $context), 10, 2);
+ }
+
+ add_action('qm/start', fn (string $name) => $this->start($name), 10, 1);
+ add_action('qm/stop', $this->stop(...), 10, 1);
+ }
+
+ /**
+ * @param array $context
+ */
+ public function message(mixed $message, string $level = 'info', array $context = []): void
+ {
+ $debugbar = ($this->debugbar)();
+
+ if (! $debugbar instanceof LaravelDebugbar) {
+ return;
+ }
+
+ if ($message instanceof \Throwable) {
+ $debugbar->addThrowable($message);
+
+ return;
+ }
+
+ if ($message instanceof \WP_Error) {
+ $message = 'WP_Error: '.$message->get_error_message();
+ }
+
+ $debugbar->addMessage(is_string($message) ? $this->interpolate($message, $context) : $message, $level, $context);
+ }
+
+ public function start(string $name, ?string $label = null): void
+ {
+ ($this->debugbar)()?->startMeasure($name, $label ?? $name, 'wordpress', 'WordPress');
+ }
+
+ public function stop(string $name): void
+ {
+ ($this->debugbar)()?->stopMeasure($name);
+ }
+
+ /**
+ * PSR-3 placeholders (`{user}`) filled from the context, as Query Monitor does.
+ *
+ * @param array $context
+ */
+ private function interpolate(string $message, array $context): string
+ {
+ $replacements = [];
+
+ foreach ($context as $key => $value) {
+ if (is_scalar($value) || $value instanceof \Stringable) {
+ $replacements['{'.$key.'}'] = (string) $value;
+ }
+ }
+
+ return strtr($message, $replacements);
+ }
+}
diff --git a/src/Collector.php b/src/Collector.php
new file mode 100644
index 0000000..e07d535
--- /dev/null
+++ b/src/Collector.php
@@ -0,0 +1,198 @@
+}>
+ */
+ private array $sections = [];
+
+ /**
+ * The tab's title in the bar.
+ */
+ abstract public function title(): string;
+
+ /**
+ * Who the data comes from: Origin::POLLORA, Origin::WORDPRESS, or the
+ * owner's own name for a third party (`acme-shop`).
+ */
+ abstract public function origin(): string;
+
+ /**
+ * The widget the data is shown with.
+ */
+ public function widget(): Widget
+ {
+ return Widget::Variables;
+ }
+
+ /**
+ * The tab's icon: one of the icons php-debugbar ships (`box`, `database`,
+ * `link`, `leaf`, `search`, `table`, `tags`, `clock`, `bolt`, `flag`…;
+ * see its resources/icons.css). Any other name shows an empty square.
+ */
+ public function icon(): string
+ {
+ return 'box';
+ }
+
+ /**
+ * The table's columns, keyed by the row field they show (Table widget only).
+ *
+ * @return array
+ */
+ public function columns(): array
+ {
+ return [];
+ }
+
+ /**
+ * Where the tab sits among the tabs of its origin, from 0.
+ */
+ public function position(): int
+ {
+ return 0;
+ }
+
+ /**
+ * Append a section of name => value pairs to this tab (Variables widget).
+ *
+ * @param \Closure(): array $values
+ */
+ public function addSection(string $title, \Closure $values): void
+ {
+ $this->sections[] = ['title' => $title, 'values' => $values];
+ }
+
+ /**
+ * @return array
+ */
+ public function collect(): array
+ {
+ $data = $this->data();
+
+ return match ($this->widget()) {
+ Widget::Variables => $this->variables($data),
+ Widget::Table => [
+ 'data' => ['data' => $data, 'key_map' => $this->columns()],
+ 'count' => count($data),
+ ],
+ Widget::Queries => $data,
+ };
+ }
+
+ /**
+ * @return array>
+ */
+ public function getWidgets(): array
+ {
+ $name = $this->getName();
+ $queries = $this->widget() === Widget::Queries;
+
+ return [
+ $name => [
+ 'icon' => $this->icon(),
+ 'title' => $this->title(),
+ 'tooltip' => Origin::label($this->origin()),
+ 'widget' => match ($this->widget()) {
+ Widget::Variables => match (true) {
+ $this->isJsonVarDumperUsed() => 'PhpDebugBar.Widgets.JsonVariableListWidget',
+ $this->isHtmlVarDumperUsed() => 'PhpDebugBar.Widgets.HtmlVariableListWidget',
+ default => 'PhpDebugBar.Widgets.VariableListWidget',
+ },
+ Widget::Table => 'PhpDebugBar.Widgets.TableVariableListWidget',
+ Widget::Queries => 'PhpDebugBar.Widgets.SQLQueriesWidget',
+ },
+ 'map' => $queries ? $name : "{$name}.data",
+ 'default' => '{}',
+ 'order' => Origin::band($this->origin()) + $this->position(),
+ ],
+ "{$name}:badge" => [
+ 'map' => $queries ? "{$name}.nb_statements" : "{$name}.count",
+ 'default' => 'null',
+ ],
+ ];
+ }
+
+ /**
+ * @return array
+ */
+ public function getAssets(): array
+ {
+ if ($this->widget() !== Widget::Queries) {
+ return [];
+ }
+
+ return [
+ 'css' => 'widgets/sqlqueries/widget.css',
+ 'js' => 'widgets/sqlqueries/widget.js',
+ ];
+ }
+
+ public function reset(): void {}
+
+ /**
+ * What the tab shows, in the shape its widget takes.
+ *
+ * Variables: name => value. Table: row key => [column => value].
+ * Queries: php-debugbar's SQL statement structure.
+ *
+ * @return array
+ */
+ abstract protected function data(): array;
+
+ /**
+ * Name => value pairs as the variable list widgets render them.
+ *
+ * The JSON and HTML widgets write string values as HTML (the HTML one its
+ * keys too), so plain strings are escaped; arrays and objects go through
+ * the var dumper.
+ *
+ * @param array $values
+ * @return array{data: array, count: int|null}
+ */
+ private function variables(array $values): array
+ {
+ foreach ($this->sections as $section) {
+ foreach (($section['values'])() as $key => $value) {
+ $values["{$section['title']} › {$key}"] = $value;
+ }
+ }
+
+ $html = $this->isHtmlVarDumperUsed();
+ $escape = $html || $this->isJsonVarDumperUsed();
+ $formatted = [];
+
+ foreach ($values as $key => $value) {
+ $label = $html ? htmlspecialchars((string) $key) : (string) $key;
+
+ $formatted[$label] = match (true) {
+ is_string($value), is_int($value), is_float($value) => $escape ? htmlspecialchars((string) $value) : (string) $value,
+ is_bool($value) => $value ? 'yes' : 'no',
+ $value === null => '—',
+ default => $this->getDataFormatter()->formatVar($value),
+ };
+ }
+
+ return ['data' => $formatted, 'count' => null];
+ }
+}
diff --git a/src/CollectorRegistrar.php b/src/CollectorRegistrar.php
new file mode 100644
index 0000000..e5fb241
--- /dev/null
+++ b/src/CollectorRegistrar.php
@@ -0,0 +1,171 @@
+registered) {
+ return;
+ }
+
+ $this->registered = true;
+
+ foreach ($this->builtIns($debugbar) as $collector) {
+ $this->add($debugbar, $collector);
+ }
+
+ foreach ($this->container->tagged(self::COLLECTORS_TAG) as $collector) {
+ if ($collector instanceof DataCollectorInterface) {
+ $this->add($debugbar, $collector);
+ }
+ }
+
+ $registry = new Registry;
+
+ if (function_exists('do_action')) {
+ do_action('pollora/debugbar/register', $registry);
+ }
+
+ foreach ($registry->collectors() as $collector) {
+ $this->add($debugbar, $collector);
+ }
+
+ foreach ($this->container->tagged(self::SECTIONS_TAG) as $provider) {
+ if ($provider instanceof SectionProvider) {
+ $this->section($debugbar, $provider->tab(), $provider->title(), $provider->values(...));
+ }
+ }
+
+ foreach ($registry->sections() as $section) {
+ $this->section($debugbar, $section['tab'], $section['title'], $section['values']);
+ }
+ }
+
+ public function isRegistered(): bool
+ {
+ return $this->registered;
+ }
+
+ /**
+ * @return list
+ */
+ private function builtIns(LaravelDebugbar $debugbar): array
+ {
+ $config = $this->container->make('config');
+ $on = static fn (string $name): bool => (bool) $config->get("debugbar-pollora.collectors.{$name}", true);
+ $collectors = [];
+
+ if ($on('pollora')) {
+ $collectors[] = new PolloraCollector($this->container);
+ }
+
+ if ($on('wp_request')) {
+ $collectors[] = new WpRequestCollector($this->recorder);
+ }
+
+ if ($on('wp_queries')) {
+ $threshold = $config->get('debugbar-pollora.options.wp_queries.slow_threshold', 50);
+
+ $collectors[] = new WpQueriesCollector(
+ 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),
+ );
+ }
+
+ if ($on('wp_hooks')) {
+ $collectors[] = new WpHooksCollector($this->recorder, $this->polloraHookServices());
+ }
+
+ if ($on('wp_timeline') && $debugbar->hasCollector('time')) {
+ $time = $debugbar->getCollector('time');
+
+ if ($time instanceof TimeDataCollector) {
+ $collectors[] = new WpTimelineCollector($this->recorder, $time);
+ }
+ }
+
+ return $collectors;
+ }
+
+ /**
+ * @return list