From 67e915bf75a906420dd95d8b0e41c78bd157839a Mon Sep 17 00:00:00 2001 From: Derrick Austin Date: Wed, 23 Sep 2026 21:38:44 -0500 Subject: [PATCH] Support Laravel Debugbar 4 Move to fruitcake/laravel-debugbar ^4.2, which supports Laravel 11, 12 and 13 on PHP 8.2+. - Update namespaces from Barryvdh\Debugbar to Fruitcake\LaravelDebugbar. - Debugbar 4 injects from a RequestHandled listener, so InjectDebugbar handles the response itself and only for users allowed to see the bar. Store-only requests are collected without response headers, and the bar is disabled for every other request, including responses that never reach the middleware. - Drop the SymfonyHttpDriver wiring and the JavascriptRenderer subclass; add Winter's stylesheet with the renderer's addAssets(). - Add the return types php-debugbar 3 requires and use getDataFormatter()->formatVar(). - Turn off the models and route collectors in register(), since Debugbar 4 registers collectors before the plugin boots. - Bind Illuminate\Log\Logger to the default log channel; Storm aliases it to the LogManager. --- Plugin.php | 68 ++++++++++-------- classes/JavascriptRenderer.php | 19 ----- classes/ServiceProvider.php | 54 ++++++++++++++- classes/WinterDebugbar.php | 25 ++++--- collectors/BackendCollector.php | 6 +- collectors/CmsCollector.php | 8 +-- collectors/ComponentsCollector.php | 10 +-- composer.json | 6 +- middleware/InjectDebugbar.php | 92 +++++++++++++++++++++---- middleware/InterpretsAjaxExceptions.php | 2 +- twig/extension/Debug.php | 2 +- twig/extension/Stopwatch.php | 2 +- 12 files changed, 204 insertions(+), 90 deletions(-) delete mode 100644 classes/JavascriptRenderer.php diff --git a/Plugin.php b/Plugin.php index 537cc53..e366981 100644 --- a/Plugin.php +++ b/Plugin.php @@ -2,9 +2,8 @@ use Backend\Classes\Controller as BackendController; use Backend\Models\UserRole; -use Barryvdh\Debugbar\Facades\Debugbar; -use Barryvdh\Debugbar\LaravelDebugbar; -use Barryvdh\Debugbar\SymfonyHttpDriver; +use Fruitcake\LaravelDebugbar\Facades\Debugbar; +use Fruitcake\LaravelDebugbar\LaravelDebugbar; use Cms\Classes\Controller as CmsController; use Cms\Classes\Layout; use Cms\Classes\Page; @@ -12,7 +11,6 @@ use Event; use Illuminate\Contracts\Http\Kernel as HttpKernelContract; use Illuminate\Foundation\AliasLoader; -use Illuminate\Session\SessionManager; use System\Classes\CombineAssets; use System\Classes\PluginBase; use Twig\Extension\ProfilerExtension; @@ -33,6 +31,11 @@ class Plugin extends PluginBase */ public $elevated = true; + /** + * @var bool Whether Winter's models collector replaces Laravel Debugbar's. + */ + protected $useWinterModelsCollector = false; + /** * Returns information about this plugin. * @return array @@ -57,20 +60,29 @@ public function register() // Provide the winter.debugbar config under the debugbar namespace Config::set('debugbar', Config::get('winter.debugbar::config')); + /* + * Laravel Debugbar 4 registers its collectors when its service provider boots, before this + * plugin boots, so turn off the collectors that Winter replaces here. + */ + if (Config::get('debugbar.collectors.models', true)) { + $this->useWinterModelsCollector = true; + Config::set('debugbar.collectors.models', false); + } + if (!$this->app->runningInBackend() && Config::get('debugbar.collectors.cms', true)) { + // The CMS collector presents the route information instead + Config::set('debugbar.collectors.route', false); + } + // Register the service provider $this->app->register(\Winter\Debugbar\Classes\ServiceProvider::class); // Replace the LaravelDebugbar with the WinterDebugbar + // Laravel Debugbar 4 configures its own HTTP driver (LaravelHttpDriver), so we only + // need to swap in Winter's Debugbar subclass for the custom JavascriptRenderer styling. + // Resolve through the container so the constructor (Application $app, Request $request) + // dependencies are injected (Laravel Debugbar 4 requires both). $this->app->singleton(LaravelDebugbar::class, function ($app) { - $debugbar = new WinterDebugbar($app); - - if ($app->bound(SessionManager::class)) { - $sessionManager = $app->make(SessionManager::class); - $httpDriver = new SymfonyHttpDriver($sessionManager); - $debugbar->setHttpDriver($httpDriver); - } - - return $debugbar; + return $app->make(WinterDebugbar::class); }); // Register alias @@ -114,14 +126,13 @@ public function boot() */ public function addGlobalCollectors() { - if (Config::get('debugbar.collectors.models', true)) { - // Disable original models collector because it will be replaced - Config::set('debugbar.collectors.models', false); - - /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugBar */ - $debugBar = $this->app->make(\Barryvdh\Debugbar\LaravelDebugbar::class); + if ($this->useWinterModelsCollector) { + /** @var LaravelDebugbar $debugBar */ + $debugBar = $this->app->make(LaravelDebugbar::class); $modelsCollector = $this->app->make(ModelsCollector::class); - $debugBar->addCollector($modelsCollector); + if (!$debugBar->hasCollector($modelsCollector->getName())) { + $debugBar->addCollector($modelsCollector); + } } } @@ -130,13 +141,10 @@ public function addGlobalCollectors() */ public function addFrontendCollectors() { - /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugBar */ - $debugBar = $this->app->make(\Barryvdh\Debugbar\LaravelDebugbar::class); + /** @var LaravelDebugbar $debugBar */ + $debugBar = $this->app->make(LaravelDebugbar::class); if (Config::get('debugbar.collectors.cms', true)) { - // Disable route collector as the CMS collector presents this info instead - Config::set('debugbar.collectors.route', false); - Event::listen('cms.page.beforeDisplay', function (CmsController $controller, $url, ?Page $page) use ($debugBar) { if ($page) { $collector = new CmsCollector($controller, $url, $page); @@ -164,8 +172,8 @@ public function addFrontendCollectors() */ public function addBackendCollectors() { - /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugBar */ - $debugBar = $this->app->make(\Barryvdh\Debugbar\LaravelDebugbar::class); + /** @var LaravelDebugbar $debugBar */ + $debugBar = $this->app->make(LaravelDebugbar::class); if (Config::get('debugbar.collectors.backend', true)) { Event::listen('backend.page.beforeDisplay', function (BackendController $controller, $action, array $params) use ($debugBar) { @@ -183,7 +191,7 @@ public function addBackendCollectors() protected function registerCmsTwigExtensions() { $profile = new Profile; - $debugBar = $this->app->make(\Barryvdh\Debugbar\LaravelDebugbar::class); + $debugBar = $this->app->make(LaravelDebugbar::class); Event::listen('cms.page.beforeDisplay', function ($controller, $url, $page) use ($profile, $debugBar) { $twig = $controller->getTwig(); @@ -197,9 +205,11 @@ protected function registerCmsTwigExtensions() } }); + // php-debugbar 2.x (shipped with Laravel Debugbar 4) no longer bundles a Twig profile + // collector, so only register one when the bridge class is actually available. if (class_exists(\DebugBar\Bridge\NamespacedTwigProfileCollector::class)) { $debugBar->addCollector(new \DebugBar\Bridge\NamespacedTwigProfileCollector($profile)); - } else { + } elseif (class_exists(\DebugBar\Bridge\TwigProfileCollector::class)) { $debugBar->addCollector(new \DebugBar\Bridge\TwigProfileCollector($profile)); } } diff --git a/classes/JavascriptRenderer.php b/classes/JavascriptRenderer.php deleted file mode 100644 index 036410c..0000000 --- a/classes/JavascriptRenderer.php +++ /dev/null @@ -1,19 +0,0 @@ -cssFiles['winter'] = __DIR__ . '/../assets/css/debugbar.css'; - } -} diff --git a/classes/ServiceProvider.php b/classes/ServiceProvider.php index 374fd0d..44e2117 100644 --- a/classes/ServiceProvider.php +++ b/classes/ServiceProvider.php @@ -1,7 +1,11 @@ listen(RequestHandled::class, function (RequestHandled $event): void { + if ( + $this->app->resolved(LaravelDebugbar::class) + && !$event->request->attributes->get(InjectDebugbar::HANDLED) + ) { + $this->app->make(LaravelDebugbar::class)->disable(); + } + }); + + parent::boot($events); + + $this->registerMiddleware(InjectDebugbar::class); + } + + /** + * Register the service provider. + * + * Debugbar's log collectors type-hint Illuminate\Log\Logger. Storm aliases that class to the + * "log" service, which is a LogManager, so bind it to the default channel's Logger instead. + */ + public function register(): void + { + parent::register(); + + $this->app->bind(Logger::class, fn ($app) => $app->make('log')->driver()); + } + /** * Register the Debugbar Middleware * * @param string $middleware */ - protected function registerMiddleware($middleware) + protected function registerMiddleware(string $middleware): void { $kernel = $this->app[Kernel::class]; - $kernel->pushMiddleware(InjectDebugbar::class); + $kernel->pushMiddleware($middleware); } } diff --git a/classes/WinterDebugbar.php b/classes/WinterDebugbar.php index 2a7c25a..78c960d 100644 --- a/classes/WinterDebugbar.php +++ b/classes/WinterDebugbar.php @@ -1,21 +1,28 @@ jsRenderer === null) { - $this->jsRenderer = new JavascriptRenderer($this, $baseUrl, $basePath); + $alreadyBuilt = $this->jsRenderer !== null; + + $renderer = parent::getJavascriptRenderer($baseUrl, $basePath); + + if (!$alreadyBuilt) { + $renderer->addAssets(cssFiles: ['debugbar.css'], basePath: __DIR__ . '/../assets/css'); } - return $this->jsRenderer; + + return $renderer; } } diff --git a/collectors/BackendCollector.php b/collectors/BackendCollector.php index 5b6429b..27e3c4f 100644 --- a/collectors/BackendCollector.php +++ b/collectors/BackendCollector.php @@ -25,7 +25,7 @@ public function __construct(Controller $controller, $action, array $params = []) /** * {@inheritDoc} */ - public function collect() + public function collect(): array { $ajaxHandler = $this->controller->getAjaxHandler(); @@ -100,7 +100,7 @@ protected function getReflector($handler) /** * {@inheritDoc} */ - public function getName() + public function getName(): string { return 'backend'; } @@ -108,7 +108,7 @@ public function getName() /** * {@inheritDoc} */ - public function getWidgets() + public function getWidgets(): array { return [ 'route' => [ diff --git a/collectors/CmsCollector.php b/collectors/CmsCollector.php index 1523c33..b315d45 100644 --- a/collectors/CmsCollector.php +++ b/collectors/CmsCollector.php @@ -26,7 +26,7 @@ public function __construct(Controller $controller, $url, Page $page) /** * {@inheritDoc} */ - public function collect() + public function collect(): array { $ajaxHandler = $this->controller->getAjaxHandler(); @@ -47,7 +47,7 @@ public function collect() } foreach ($this->page->toArray() as $key => $value) { - $result[$key] = is_scalar($value) ? $value : $this->formatVar($value); + $result[$key] = is_scalar($value) ? $value : $this->getDataFormatter()->formatVar($value); } return $result; @@ -103,7 +103,7 @@ protected function getReflector($handler) /** * {@inheritDoc} */ - public function getName() + public function getName(): string { return 'cms'; } @@ -111,7 +111,7 @@ public function getName() /** * {@inheritDoc} */ - public function getWidgets() + public function getWidgets(): array { return [ 'route' => [ diff --git a/collectors/ComponentsCollector.php b/collectors/ComponentsCollector.php index ca9d333..795b939 100644 --- a/collectors/ComponentsCollector.php +++ b/collectors/ComponentsCollector.php @@ -28,17 +28,17 @@ public function __construct(Controller $controller, Page $page, Layout $layout) /** * {@inheritDoc} */ - public function collect() + public function collect(): array { /** @var ComponentBase[]|object $components */ $components = []; foreach ($this->layout->components as $alias => $componentObj) { - $components[$alias] = $this->formatVar($this->makeComponentDetails($componentObj)); + $components[$alias] = $this->getDataFormatter()->formatVar($this->makeComponentDetails($componentObj)); } foreach ($this->page->components as $alias => $componentObj) { - $components[$alias] = $this->formatVar($this->makeComponentDetails($componentObj)); + $components[$alias] = $this->getDataFormatter()->formatVar($this->makeComponentDetails($componentObj)); } return $components; @@ -58,7 +58,7 @@ protected function makeComponentDetails($componentObj): array /** * {@inheritDoc} */ - public function getName() + public function getName(): string { return 'components'; } @@ -66,7 +66,7 @@ public function getName() /** * {@inheritDoc} */ - public function getWidgets() + public function getWidgets(): array { return [ 'components' => [ diff --git a/composer.json b/composer.json index bf72003..e5b3056 100644 --- a/composer.json +++ b/composer.json @@ -23,9 +23,9 @@ "source": "https://github.com/wintercms/wn-debugbar-plugin" }, "require": { - "php": ">=7.0", + "php": ">=8.2", "composer/installers": "~1.0", - "barryvdh/laravel-debugbar": "^3.15.0" + "fruitcake/laravel-debugbar": "^4.2" }, "replace": { "rainlab/debugbar-plugin": ">=1.0.5" @@ -34,7 +34,7 @@ "installer-name": "debugbar", "laravel": { "dont-discover": [ - "barryvdh/laravel-debugbar" + "fruitcake/laravel-debugbar" ] } } diff --git a/middleware/InjectDebugbar.php b/middleware/InjectDebugbar.php index 4e6ab64..75b92b0 100644 --- a/middleware/InjectDebugbar.php +++ b/middleware/InjectDebugbar.php @@ -3,14 +3,57 @@ namespace Winter\Debugbar\Middleware; use Backend\Facades\BackendAuth; -use Barryvdh\Debugbar\Middleware\InjectDebugbar as BaseMiddleware; use Closure; +use Fruitcake\LaravelDebugbar\LaravelDebugbar; +use Illuminate\Contracts\Debug\ExceptionHandler; +use Illuminate\Foundation\Application; use Symfony\Component\HttpFoundation\Request; use Throwable; use Winter\Storm\Support\Facades\Config; -class InjectDebugbar extends BaseMiddleware +/** + * Injects the Debugbar into the response, gated by the winter.debugbar permissions. + * + * Laravel Debugbar 4 removed its own InjectDebugbar middleware in favour of a + * RequestHandled event listener (registered by the base ServiceProvider) that injects + * the Debugbar unconditionally. This middleware replicates the legacy behaviour: it boots + * the Debugbar, restricts request storage and injection to authorised users, and performs + * the response handling itself via LaravelDebugbar::handleResponse(). Because handleResponse() + * flags the response as modified, the base event listener becomes a no-op for this request. + */ +class InjectDebugbar { + /** + * Request attribute set once this middleware has applied the permission checks. + */ + public const HANDLED = 'winter.debugbar.handled'; + + /** + * The Laravel Application + * + * @var Application + */ + protected $app; + + /** + * The Debugbar instance + * + * @var LaravelDebugbar + */ + protected $debugbar; + + /** + * Create a new middleware instance. + * + * @param Application $app + * @param LaravelDebugbar $debugbar + */ + public function __construct(Application $app, LaravelDebugbar $debugbar) + { + $this->app = $app; + $this->debugbar = $debugbar; + } + /** * Handle an incoming request. * @@ -20,17 +63,23 @@ class InjectDebugbar extends BaseMiddleware */ public function handle($request, Closure $next) { - if (!$this->debugbar->isEnabled() || $this->inExceptArray($request)) { + if (!$this->debugbar->isEnabled() || $this->debugbar->requestIsExcluded($request)) { return $next($request); } $this->debugbar->boot(); + // Render any downstream exception into a response so the Debugbar is still injected on + // error pages. Laravel Debugbar 4 dropped its own InjectDebugbar middleware (which wrapped + // the request in this try/catch via handleException()), so replicate it here rather than + // letting the exception bypass handleResponse() below. try { /** @var \Illuminate\Http\Response $response */ $response = $next($request); } catch (Throwable $e) { - $response = $this->handleException($request, $e); + $handler = $this->app->make(ExceptionHandler::class); + $handler->report($e); + $response = $handler->render($request, $e); } // Database table might not exist yet @@ -40,8 +89,14 @@ public function handle($request, Closure $next) $user = null; } - if ((!$user || !$user->hasAccess('winter.debugbar.access_stored_requests')) && - !Config::get('winter.debugbar::store_all_requests', false)) { + $request->attributes->set(static::HANDLED, true); + + $canStore = ($user && $user->hasAccess('winter.debugbar.access_stored_requests')) + || Config::get('winter.debugbar::store_all_requests', false); + $canView = ($user && $user->hasAccess('winter.debugbar.access_debugbar')) + || Config::get('winter.debugbar::allow_public_access', false); + + if (!$canStore) { // Disable stored requests // Note: this will completely disable storing requests from any users // without the required permission. If that functionality is desired again @@ -49,12 +104,25 @@ public function handle($request, Closure $next) $this->debugbar->setStorage(null); } - // Modify the response to add the Debugbar if allowed - if ( - ($user && $user->hasAccess('winter.debugbar.access_debugbar')) || - Config::get('winter.debugbar::allow_public_access', false) - ) { - $this->debugbar->modifyResponse($request, $response); + if ($canView) { + // Add the Debugbar to the response. This flags the response as modified, so the base + // ServiceProvider's RequestHandled listener skips it. + $this->debugbar->handleResponse($request, $response); + + return $response; + } + + // The user may not see the Debugbar: store the request if permitted, but never send its + // data in the response headers, and disable the Debugbar so the base RequestHandled and + // Terminating listeners leave this request alone. + try { + if ($canStore) { + $this->debugbar->collect(); + } + } catch (Throwable $e) { + $this->app['log']->error('Debugbar exception: ' . $e->getMessage(), ['exception' => $e]); + } finally { + $this->debugbar->disable(); } return $response; diff --git a/middleware/InterpretsAjaxExceptions.php b/middleware/InterpretsAjaxExceptions.php index f4a858a..961a38d 100644 --- a/middleware/InterpretsAjaxExceptions.php +++ b/middleware/InterpretsAjaxExceptions.php @@ -38,7 +38,7 @@ public function __construct(Application $app) */ public function handle($request, Closure $next) { - /** @var \Barryvdh\Debugbar\LaravelDebugbar $debugbar */ + /** @var \Fruitcake\LaravelDebugbar\LaravelDebugbar $debugbar */ $debugbar = $this->app['debugbar']; try { diff --git a/twig/extension/Debug.php b/twig/extension/Debug.php index d9ebd57..39e2709 100644 --- a/twig/extension/Debug.php +++ b/twig/extension/Debug.php @@ -13,7 +13,7 @@ class Debug extends TwigExtension { /** - * @var \Barryvdh\Debugbar\LaravelDebugbar + * @var \Fruitcake\LaravelDebugbar\LaravelDebugbar */ protected $debugbar; diff --git a/twig/extension/Stopwatch.php b/twig/extension/Stopwatch.php index e886d44..e8c0988 100644 --- a/twig/extension/Stopwatch.php +++ b/twig/extension/Stopwatch.php @@ -13,7 +13,7 @@ class Stopwatch extends TwigExtension { /** - * @var \Barryvdh\Debugbar\LaravelDebugbar + * @var \Fruitcake\LaravelDebugbar\LaravelDebugbar */ protected $debugbar;