From ba3959c07e43c178076f72faebd2abcda5898077 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:49:26 +0400 Subject: [PATCH 01/10] [#539] Fill the debugbar events tab when the toolbar renders Debugger::render() writes the registered event listeners into the events tab itself, once per render, and PrepareRequestStage no longer writes to the debugger. --- .../Stages/Runtime/PrepareRequestStage.php | 18 +---- src/Debugger/Debugger.php | 5 +- .../Runtime/PrepareRequestStageTest.php | 30 +------- tests/Unit/Debugger/DebuggerTest.php | 74 +++++++++++++++++++ 4 files changed, 80 insertions(+), 47 deletions(-) diff --git a/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php index bb55dcab..f60bda19 100644 --- a/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php @@ -17,15 +17,13 @@ use Quantum\Lang\Factories\LangFactory; use Quantum\Di\Exceptions\DiException; use Quantum\ResourceCache\ViewCache; -use Quantum\Debugger\Debugger; use Quantum\App\AppContext; use ReflectionException; /** * Class PrepareRequestStage * - * Prepares a matched request: loads the language, logs debug info and sets up the view cache. - * The order of these steps is kept from the former WebAppAdapter flow. + * Prepares a matched request: loads the language and sets up the view cache. * * @implements StageInterface * @package Quantum\App @@ -44,23 +42,9 @@ public function process(AppContext $context): void { LangFactory::get(); - $this->logDebugInfo(); - $this->setupViewCache($context); } - /** - * @throws DiException|ReflectionException - */ - private function logDebugInfo(): void - { - $debugBar = debugbar(); - - if ($debugBar->isEnabled()) { - $debugBar->addToStoreCell(Debugger::EVENTS, 'info', event()->getRegistered()); - } - } - /** * @throws ConfigException|DiException|ReflectionException */ diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index 558063b6..d8c156af 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -142,7 +142,7 @@ public function resetStore(): void } /** - * Renders the debug bar + * Renders the debug bar, with the event listeners registered at this point in the events tab * @throws DebugBarException|DiException|ReflectionException */ public function render(): string @@ -151,6 +151,9 @@ public function render(): string return ''; } + $this->clearStoreCell(self::EVENTS); + $this->addToStoreCell(self::EVENTS, 'info', event()->getRegistered()); + foreach ([self::MESSAGES, self::QUERIES, self::ROUTES, self::EVENTS, self::MAILS] as $tab) { $this->createTab($tab); } diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php index 1d677b4d..76b40536 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php @@ -41,38 +41,10 @@ public function testPrepareRequestStageLoadsLanguage(): void $this->assertTrue(config()->has('lang')); } - public function testPrepareRequestStageLogsRegisteredEventsToDebugger(): void + public function testPrepareRequestStageWritesNothingToTheDebugger(): void { config()->set('app.debug', true); - $listener = function (): void { - }; - - event()->listen('test.event', $listener); - - (new PrepareRequestStage())->process($this->webContext); - - $cell = debugbar()->getStoreCell(Debugger::EVENTS); - - $this->assertCount(1, $cell); - $this->assertSame(['test.event' => [$listener]], $cell[0]['info']); - } - - public function testPrepareRequestStageLogsNothingToDebuggerWithoutEventListeners(): void - { - config()->set('app.debug', true); - - $this->assertSame([], event()->getRegistered()); - - (new PrepareRequestStage())->process($this->webContext); - - $this->assertSame([], debugbar()->getStoreCell(Debugger::EVENTS)); - } - - public function testPrepareRequestStageSkipsDebuggerLoggingWhenDebugDisabled(): void - { - config()->set('app.debug', false); - event()->listen('test.event', function (): void { }); diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index 4e963ba0..461665ce 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -26,6 +26,7 @@ public function setUp(): void parent::setUp(); $this->debuggerStore = new DebuggerStore(); + $this->debuggerStore->flush(); $this->debugBarMock = Mockery::mock(DebugBar::class); @@ -162,4 +163,77 @@ public function testDebugbarRenderReturnsEmptyWhenDisabled(): void $this->assertEmpty($output); } + + public function testDebugbarRenderWritesTheRegisteredEventListenersToTheEventsTab(): void + { + config()->set('app.debug', true); + + $listener = function (): void { + }; + + event()->listen('test.event', $listener); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertSame([['info' => ['test.event' => [$listener]]]], $this->debuggerStore->get(Debugger::EVENTS)); + } + + public function testDebugbarRenderWritesNothingToTheEventsTabWithoutEventListeners(): void + { + config()->set('app.debug', true); + + $this->assertSame([], event()->getRegistered()); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); + } + + public function testDebugbarRenderWritesNothingToTheEventsTabWhenDisabled(): void + { + config()->set('app.debug', false); + + event()->listen('test.event', function (): void { + }); + + $this->debugger->render(); + + $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); + } + + public function testDebugbarRenderListsTheEventListenersOnceWhenRenderedTwice(): void + { + config()->set('app.debug', true); + + event()->listen('test.event', function (): void { + }); + + $this->expectRendering(); + + $this->debugger->render(); + $this->debugger->render(); + + $this->assertCount(1, $this->debuggerStore->get(Debugger::EVENTS)); + } + + /** + * Lets render() run against the mocked DebugBar: every tab gets a collector that accepts any message + */ + private function expectRendering(): void + { + $rendererMock = Mockery::mock(JavascriptRenderer::class); + $rendererMock->shouldReceive('setBaseUrl')->andReturnSelf(); + $rendererMock->shouldReceive('addAssets')->andReturnSelf(); + $rendererMock->shouldReceive('renderHead')->andReturn(''); + $rendererMock->shouldReceive('render')->andReturn(''); + + $this->debugBarMock->shouldReceive('getJavascriptRenderer')->andReturn($rendererMock); + $this->debugBarMock->shouldReceive('hasCollector')->andReturn(true); + $this->debugBarMock->shouldReceive('getCollector') + ->andReturn(Mockery::mock(MessagesCollector::class)->shouldIgnoreMissing()); + } } From e3882228c4e68133705c9d3a49073a9650bc9403 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:56:23 +0400 Subject: [PATCH 02/10] [#539] Remove InitDebuggerStage from the web boot The debugger store creates a tab on its first write, so the web boot no longer initializes it, and the App package no longer references the Debugger package. --- .../Web/Stages/Boot/InitDebuggerStage.php | 27 ------------------- src/App/Adapters/Web/WebAppAdapter.php | 2 -- .../App/Adapters/Web/WebAppAdapterTest.php | 20 ++++++++++++++ 3 files changed, 20 insertions(+), 29 deletions(-) delete mode 100644 src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php diff --git a/src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php b/src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php deleted file mode 100644 index 3c84e6f6..00000000 --- a/src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @package Quantum\App - */ -class InitDebuggerStage implements StageInterface -{ - public function process(AppContext $context): void - { - debugbar()->initStore(); - } -} diff --git a/src/App/Adapters/Web/WebAppAdapter.php b/src/App/Adapters/Web/WebAppAdapter.php index 747c7191..8511924a 100644 --- a/src/App/Adapters/Web/WebAppAdapter.php +++ b/src/App/Adapters/Web/WebAppAdapter.php @@ -16,7 +16,6 @@ use Quantum\App\Adapters\Web\Stages\Runtime\PrepareRequestStage; use Quantum\App\Adapters\Web\Stages\Runtime\SendResponseStage; use Quantum\App\Adapters\Web\Stages\Runtime\ResolveRouteStage; -use Quantum\App\Adapters\Web\Stages\Boot\InitDebuggerStage; use Quantum\App\Adapters\Web\Stages\Boot\LoadModulesStage; use Quantum\App\Adapters\Web\Stages\Boot\InitHttpStage; use Quantum\App\Stages\Boot\SetupErrorHandlerStage; @@ -49,7 +48,6 @@ public function boot(): void new LoadAppConfigStage(), new SetupErrorHandlerStage(), new InitHttpStage(), - new InitDebuggerStage(), new LoadModulesStage(), ]); diff --git a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php index 98f54570..2b42f588 100644 --- a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php +++ b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php @@ -10,6 +10,7 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\Http\Enums\ContentType; use Quantum\Http\Enums\StatusCode; +use Quantum\Debugger\DebuggerStore; use Quantum\Router\MatchedRoute; use Quantum\Debugger\Debugger; use Quantum\Router\Route; @@ -366,6 +367,25 @@ public function testWebAppAdapterStartDispatchesTheSendAfterEventOnceTheRequestI $this->assertNull($uriSeenAfterSend); } + public function testWebAppAdapterLeavesTheDebuggerStoreEmptyUntilTheToolbarRenders(): void + { + $store = new DebuggerStore(); + $store->flush(); + + $adapter = new WebAppAdapter($this->createContext()); + $adapter->boot(); + + config()->set('app.debug', true); + request()->create('GET', '/test/am/tests'); + + ob_start(); + $adapter->start(); + ob_end_clean(); + + $this->assertFalse($store->has(Debugger::EVENTS)); + $this->assertSame([], $store->all()); + } + public function testWebAppAdapterConstructorDoesNotBoot(): void { $this->clearAppContext(); From 2d27c142e5d854e686b070078452b3a1793b358f Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:17:29 +0400 Subject: [PATCH 03/10] [#539] Add the changelog entry for taking the debugbar out of the app flow --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b794c02..4a093bf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,9 @@ The format is based on Keep a Changelog. - Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533) ### Changed +- **BREAKING:** Took the debugbar out of the app flow (#539): the App package no longer references the Debugger package. + - Removed `InitDebuggerStage` (`Quantum\App\Stages\InitDebuggerStage` in 3.0.3) from the web boot. The debugger store creates a tab on its first write; code that wants the tabs up front can call `debugbar()->initStore()`. + - `Debugger::render()` fills the `events` tab with the event listeners registered when the toolbar renders, instead of `PrepareRequestStage` writing them while a matched request is prepared. Listeners added later in the request, for example in a controller, are listed too. - **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #538, #575, #576, #579, #583, #589): - `AppInterface` gains `boot()`. The adapter constructors only store the context, and `AppFactory` calls `boot()` right after creating the adapter, so `AppFactory::create(...)->start()` is unchanged. Code that creates an adapter directly must call `boot()` before `start()`. - Removed the `AppAdapter` base class and the `WebAppTrait` and `ConsoleAppTrait` traits: `WebAppAdapter` and `ConsoleAppAdapter` implement `AppInterface` directly and moved to `Quantum\App\Adapters\Web` and `Quantum\App\Adapters\Console`. The stages are grouped by the adapter that runs them: the boot stages both adapters run in `Quantum\App\Stages\Boot`, the others in `Quantum\App\Adapters\Web\Stages\Boot`, `...\Web\Stages\Runtime`, `...\Console\Stages\Boot` and `...\Console\Stages\Runtime`. Code that goes through `AppFactory` is unaffected; code that references these classes directly must update its imports. From 3bbcd831e3f26b52d4f2796b1d3163145a4ea6c5 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:40:51 +0400 Subject: [PATCH 04/10] [#539] Fill the debugbar routes tab when the toolbar renders and drop the Debugger from the View --- src/Debugger/Debugger.php | 47 +++++++++++- src/View/Factories/ViewFactory.php | 1 - src/View/View.php | 37 ++++----- tests/Unit/Debugger/DebuggerTest.php | 109 ++++++++++++++++++++++++++- tests/Unit/View/ViewTest.php | 26 +++++++ 5 files changed, 193 insertions(+), 27 deletions(-) diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index d8c156af..a9bfa494 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -15,11 +15,15 @@ use DebugBar\DataCollector\MessagesCollector; use DebugBar\DataCollector\PhpInfoCollector; use DebugBar\DataCollector\MemoryCollector; +use Quantum\Config\Exceptions\ConfigException; +use Quantum\App\Exceptions\BaseException; +use Quantum\View\Factories\ViewFactory; use Quantum\Di\Exceptions\DiException; use DebugBar\JavascriptRenderer; use DebugBar\DebugBarException; use ReflectionException; use DebugBar\DebugBar; +use Quantum\Di\Di; /** * Class Debugger @@ -142,8 +146,9 @@ public function resetStore(): void } /** - * Renders the debug bar, with the event listeners registered at this point in the events tab - * @throws DebugBarException|DiException|ReflectionException + * Renders the debug bar. The events and routes tabs are filled here, with the event listeners, + * the matched route and the rendered view as they are at this point. + * @throws DebugBarException|DiException|ReflectionException|ConfigException|BaseException */ public function render(): string { @@ -154,6 +159,9 @@ public function render(): string $this->clearStoreCell(self::EVENTS); $this->addToStoreCell(self::EVENTS, 'info', event()->getRegistered()); + $this->clearStoreCell(self::ROUTES); + $this->addToStoreCell(self::ROUTES, 'info', $this->routeInfo()); + foreach ([self::MESSAGES, self::QUERIES, self::ROUTES, self::EVENTS, self::MAILS] as $tab) { $this->createTab($tab); } @@ -163,6 +171,41 @@ public function render(): string return $renderer->renderHead() . $renderer->render(); } + /** + * The matched route and the rendered view, without the values that are not set + * @return array + * @throws DiException|ReflectionException|ConfigException|BaseException + */ + protected function routeInfo(): array + { + $info = []; + + $matchedRoute = request()->getMatchedRoute(); + + if ($matchedRoute !== null) { + $route = $matchedRoute->getRoute(); + + $info = [ + 'Methods' => implode(', ', $route->getMethods()), + 'Pattern' => $route->getPattern(), + 'Module' => $route->getModule(), + 'Controller' => $route->getController(), + 'Action' => $route->getClosure() !== null ? 'Closure' : $route->getAction(), + 'Name' => $route->getName(), + 'Group' => $route->getGroup(), + 'Prefix' => $route->getPrefix(), + 'Middlewares' => $route->getMiddlewares(), + 'Params' => $matchedRoute->getParams(), + ]; + } + + if (Di::isRegistered(ViewFactory::class) && ViewFactory::get()->getViewFile() !== null) { + $info['View'] = request()->getCurrentModule() . '/Views/' . ViewFactory::get()->getViewFile(); + } + + return array_filter($info, fn ($value): bool => $value !== null && $value !== []); + } + /** * Creates a tab * @throws DebugBarException diff --git a/src/View/Factories/ViewFactory.php b/src/View/Factories/ViewFactory.php index b154a7c4..fa592d4e 100644 --- a/src/View/Factories/ViewFactory.php +++ b/src/View/Factories/ViewFactory.php @@ -53,7 +53,6 @@ public function resolve(): View $this->instance = new View( RendererFactory::get(), asset(), - debugbar(), Di::get(ViewCache::class) ); } diff --git a/src/View/View.php b/src/View/View.php index bf160dd2..7945dbeb 100644 --- a/src/View/View.php +++ b/src/View/View.php @@ -19,10 +19,8 @@ use Quantum\Di\Exceptions\DiException; use Quantum\ResourceCache\ViewCache; use Quantum\Asset\AssetManager; -use Quantum\Debugger\Debugger; use Quantum\Renderer\Renderer; use ReflectionException; -use Psr\Log\LogLevel; /** * Class View @@ -34,8 +32,6 @@ class View private AssetManager $assetManager; - private Debugger $debugger; - private ViewCache $viewCache; /** @@ -43,6 +39,11 @@ class View */ private ?string $layoutFile = null; + /** + * Last view file rendered with render() + */ + private ?string $viewFile = null; + /** * Rendered view */ @@ -63,12 +64,10 @@ class View public function __construct( Renderer $renderer, AssetManager $assetManager, - Debugger $debugger, ViewCache $viewCache ) { $this->renderer = $renderer; $this->assetManager = $assetManager; - $this->debugger = $debugger; $this->viewCache = $viewCache; } @@ -171,15 +170,12 @@ public function render(string $viewFile, array $params = []): ?string } $this->viewContent = $this->renderFile($viewFile); + $this->viewFile = $viewFile; if ($this->assets !== []) { $this->assetManager->register($this->assets); } - if ($this->debugger->isEnabled()) { - $this->updateDebugger($viewFile); - } - $layoutContent = $this->renderFile($layoutFile); if ($this->viewCache->isEnabled()) { @@ -207,6 +203,14 @@ public function renderPartial(string $viewFile, array $params = []): string return $this->renderFile($viewFile); } + /** + * Gets the last view rendered with render(), or null before the first one + */ + public function getViewFile(): ?string + { + return $this->viewFile; + } + /** * Gets the rendered view content. * @throws ViewException @@ -268,17 +272,4 @@ private function sanitizeHtml(string $value): string { return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } - - /** - * @throws ReflectionException|DiException - */ - private function updateDebugger(string $viewFile): void - { - $routesCell = $this->debugger->getStoreCell(Debugger::ROUTES); - $currentData = current($routesCell)[LogLevel::INFO] ?? []; - $additionalData = ['View' => request()->getCurrentModule() . '/Views/' . $viewFile]; - $mergedData = array_merge($currentData, $additionalData); - $this->debugger->clearStoreCell(Debugger::ROUTES); - $this->debugger->addToStoreCell(Debugger::ROUTES, LogLevel::INFO, $mergedData); - } } diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index 461665ce..eca8255e 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -4,10 +4,13 @@ use DebugBar\DataCollector\MessagesCollector; use DebugBar\DataCollector\PhpInfoCollector; +use Quantum\View\Factories\ViewFactory; use Quantum\Debugger\DebuggerStore; use Quantum\Tests\Unit\AppTestCase; use DebugBar\JavascriptRenderer; +use Quantum\Router\MatchedRoute; use Quantum\Debugger\Debugger; +use Quantum\Router\Route; use DebugBar\DebugBar; use Psr\Log\LogLevel; use Mockery; @@ -193,16 +196,120 @@ public function testDebugbarRenderWritesNothingToTheEventsTabWithoutEventListene $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); } - public function testDebugbarRenderWritesNothingToTheEventsTabWhenDisabled(): void + public function testDebugbarRenderWritesNothingWhenDisabled(): void { config()->set('app.debug', false); event()->listen('test.event', function (): void { }); + request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); + $this->debugger->render(); $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); + $this->assertSame([], $this->debuggerStore->get(Debugger::ROUTES)); + } + + public function testDebugbarRenderWritesTheMatchedRouteToTheRoutesTab(): void + { + config()->set('app.debug', true); + + $route = (new Route(['GET', 'POST'], 'posts/[id=:num]', 'PostController', 'show')) + ->module('Web') + ->name('post') + ->group('blog') + ->prefix('en') + ->addMiddlewares(['Auth']); + + request()->setMatchedRoute(new MatchedRoute($route, ['id' => '5'])); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertSame([['info' => [ + 'Methods' => 'GET, POST', + 'Pattern' => 'posts/[id=:num]', + 'Module' => 'Web', + 'Controller' => 'PostController', + 'Action' => 'show', + 'Name' => 'post', + 'Group' => 'blog', + 'Prefix' => 'en', + 'Middlewares' => ['Auth'], + 'Params' => ['id' => '5'], + ]]], $this->debuggerStore->get(Debugger::ROUTES)); + } + + public function testDebugbarRenderWritesAClosureRouteToTheRoutesTab(): void + { + config()->set('app.debug', true); + + $route = (new Route(['GET'], 'ping', null, null, function (): void { + }))->module('Api'); + + request()->setMatchedRoute(new MatchedRoute($route, [])); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertSame([['info' => [ + 'Methods' => 'GET', + 'Pattern' => 'ping', + 'Module' => 'Api', + 'Action' => 'Closure', + ]]], $this->debuggerStore->get(Debugger::ROUTES)); + } + + public function testDebugbarRenderAddsTheRenderedViewToTheRoutesTab(): void + { + config()->set('app.debug', true); + + request()->create('GET', '/test'); + request()->setMatchedRoute(new MatchedRoute((new Route(['GET'], 'test', 'SomeController', 'test'))->module('Test'), [])); + + $view = ViewFactory::get(); + $view->setLayout('layout'); + $view->render('index'); + + $this->expectRendering(); + + $this->debugger->render(); + + $routes = $this->debuggerStore->get(Debugger::ROUTES); + + $this->assertCount(1, $routes); + $this->assertSame('Test/Views/index', $routes[0]['info']['View']); + $this->assertSame('SomeController', $routes[0]['info']['Controller']); + } + + public function testDebugbarRenderWritesNothingToTheRoutesTabWithoutARouteOrAView(): void + { + config()->set('app.debug', true); + + $this->assertNull(request()->getMatchedRoute()); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertSame([], $this->debuggerStore->get(Debugger::ROUTES)); + } + + public function testDebugbarRenderListsTheMatchedRouteOnceWhenRenderedTwice(): void + { + config()->set('app.debug', true); + + request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); + + $this->expectRendering(); + + $this->debugger->render(); + $this->debugger->render(); + + $this->assertCount(1, $this->debuggerStore->get(Debugger::ROUTES)); } public function testDebugbarRenderListsTheEventListenersOnceWhenRenderedTwice(): void diff --git a/tests/Unit/View/ViewTest.php b/tests/Unit/View/ViewTest.php index b252e2cb..1e5d6c4a 100644 --- a/tests/Unit/View/ViewTest.php +++ b/tests/Unit/View/ViewTest.php @@ -6,6 +6,7 @@ use Quantum\View\Factories\ViewFactory; use Quantum\Tests\Unit\AppTestCase; use Quantum\Router\MatchedRoute; +use Quantum\Debugger\Debugger; use Quantum\View\RawParam; use Quantum\Router\Route; use Quantum\View\View; @@ -100,6 +101,31 @@ public function testSetGetRawParams(): void $this->assertEquals('

Monkey people

', $this->view->getParam('chapter_two')); } + public function testGetViewFileIsNullBeforeAViewIsRendered(): void + { + $this->assertNull($this->view->getViewFile()); + } + + public function testGetViewFileReturnsTheLastRenderedView(): void + { + $this->view->setLayout('layout'); + + $this->view->render('index'); + + $this->assertSame('index', $this->view->getViewFile()); + } + + public function testRenderWritesNothingToTheDebugger(): void + { + config()->set('app.debug', true); + + $this->view->setLayout('layout'); + + $this->view->render('index'); + + $this->assertSame([], debugbar()->getStoreCell(Debugger::ROUTES)); + } + public function testRenderWithoutLayout(): void { $this->expectException(ViewException::class); From 6ecc4668603e03aeadb94ec07d866bebf60b3c95 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:58:11 +0400 Subject: [PATCH 05/10] [#539] Fill the debugbar queries tab when the toolbar renders --- src/Debugger/Debugger.php | 25 +++++++- tests/Unit/Debugger/DebuggerTest.php | 91 ++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 2 deletions(-) diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index a9bfa494..59ba94be 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -10,6 +10,7 @@ namespace Quantum\Debugger; +use Quantum\Database\Contracts\RelationalInterface; use DebugBar\DataCollector\RequestDataCollector; use DebugBar\DataCollector\TimeDataCollector; use DebugBar\DataCollector\MessagesCollector; @@ -21,6 +22,7 @@ use Quantum\Di\Exceptions\DiException; use DebugBar\JavascriptRenderer; use DebugBar\DebugBarException; +use Quantum\Database\Database; use ReflectionException; use DebugBar\DebugBar; use Quantum\Di\Di; @@ -146,8 +148,8 @@ public function resetStore(): void } /** - * Renders the debug bar. The events and routes tabs are filled here, with the event listeners, - * the matched route and the rendered view as they are at this point. + * Renders the debug bar. The events, routes and queries tabs are filled here, with the event listeners, + * the matched route, the rendered view and the query log as they are at this point. * @throws DebugBarException|DiException|ReflectionException|ConfigException|BaseException */ public function render(): string @@ -162,6 +164,11 @@ public function render(): string $this->clearStoreCell(self::ROUTES); $this->addToStoreCell(self::ROUTES, 'info', $this->routeInfo()); + $this->clearStoreCell(self::QUERIES); + foreach ($this->queryLog() as $query) { + $this->addToStoreCell(self::QUERIES, 'info', $query); + } + foreach ([self::MESSAGES, self::QUERIES, self::ROUTES, self::EVENTS, self::MAILS] as $tab) { $this->createTab($tab); } @@ -206,6 +213,20 @@ protected function routeInfo(): array return array_filter($info, fn ($value): bool => $value !== null && $value !== []); } + /** + * The queries run so far, only when the request used the database and its adapter keeps a query log + * @return array + * @throws DiException|ReflectionException + */ + protected function queryLog(): array + { + if (!Di::isRegistered(Database::class) || !is_subclass_of(db()->getOrmClass(), RelationalInterface::class)) { + return []; + } + + return Database::queryLog(); + } + /** * Creates a tab * @throws DebugBarException diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index eca8255e..dd4e647c 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -2,6 +2,8 @@ namespace Quantum\Tests\Unit\Debugger; +use Quantum\Database\Adapters\Sleekdb\SleekDbal; +use Quantum\Database\Adapters\Idiorm\IdiormDbal; use DebugBar\DataCollector\MessagesCollector; use DebugBar\DataCollector\PhpInfoCollector; use Quantum\View\Factories\ViewFactory; @@ -9,10 +11,13 @@ use Quantum\Tests\Unit\AppTestCase; use DebugBar\JavascriptRenderer; use Quantum\Router\MatchedRoute; +use Quantum\Database\Database; use Quantum\Debugger\Debugger; use Quantum\Router\Route; +use Quantum\Config\Setup; use DebugBar\DebugBar; use Psr\Log\LogLevel; +use Quantum\Di\Di; use Mockery; class DebuggerTest extends AppTestCase @@ -205,10 +210,15 @@ public function testDebugbarRenderWritesNothingWhenDisabled(): void request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); + $this->connectSqlite(); + + Database::query('SELECT 1'); + $this->debugger->render(); $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); $this->assertSame([], $this->debuggerStore->get(Debugger::ROUTES)); + $this->assertSame([], $this->debuggerStore->get(Debugger::QUERIES)); } public function testDebugbarRenderWritesTheMatchedRouteToTheRoutesTab(): void @@ -312,6 +322,71 @@ public function testDebugbarRenderListsTheMatchedRouteOnceWhenRenderedTwice(): v $this->assertCount(1, $this->debuggerStore->get(Debugger::ROUTES)); } + public function testDebugbarRenderWritesTheQueriesOfTheRequestToTheQueriesTab(): void + { + config()->set('app.debug', true); + + $this->connectSqlite(); + + Database::query('SELECT 1'); + + $this->expectRendering(); + + $this->debugger->render(); + + $queries = $this->debuggerStore->get(Debugger::QUERIES); + + $this->assertCount(count(Database::queryLog()), $queries); + $this->assertSame(['info' => 'SELECT 1'], end($queries)); + } + + public function testDebugbarRenderLeavesTheDatabaseUnusedWhenTheRequestDidNotUseIt(): void + { + config()->set('app.debug', true); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertFalse(Di::isRegistered(Database::class)); + $this->assertSame([], $this->debuggerStore->get(Debugger::QUERIES)); + } + + public function testDebugbarRenderWritesNothingToTheQueriesTabForAnAdapterWithoutAQueryLog(): void + { + config()->set('app.debug', true); + + if (!config()->has('database')) { + config()->import(new Setup('config', 'database')); + } + + config()->set('database.default', 'sleekdb'); + + $this->assertSame(SleekDbal::class, db()->getOrmClass()); + + $this->expectRendering(); + + $this->debugger->render(); + + $this->assertSame([], $this->debuggerStore->get(Debugger::QUERIES)); + } + + public function testDebugbarRenderListsTheQueriesOnceWhenRenderedTwice(): void + { + config()->set('app.debug', true); + + $this->connectSqlite(); + + Database::query('SELECT 1'); + + $this->expectRendering(); + + $this->debugger->render(); + $this->debugger->render(); + + $this->assertCount(count(Database::queryLog()), $this->debuggerStore->get(Debugger::QUERIES)); + } + public function testDebugbarRenderListsTheEventListenersOnceWhenRenderedTwice(): void { config()->set('app.debug', true); @@ -327,6 +402,22 @@ public function testDebugbarRenderListsTheEventListenersOnceWhenRenderedTwice(): $this->assertCount(1, $this->debuggerStore->get(Debugger::EVENTS)); } + /** + * Connects the in-memory SQLite database again, so the connection logs queries with the current app.debug + */ + private function connectSqlite(): void + { + if (!config()->has('database')) { + config()->import(new Setup('config', 'database')); + } + + config()->set('database.default', 'sqlite'); + + IdiormDbal::disconnect(); + + db(); + } + /** * Lets render() run against the mocked DebugBar: every tab gets a collector that accepts any message */ From 069e3aa41bd94ac4fa7eb911031f1c05e84bf0b0 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:11:07 +0400 Subject: [PATCH 06/10] [#539] Remove the tests that only prove the debugger is gone --- .../Runtime/PrepareRequestStageTest.php | 13 ------------ .../App/Adapters/Web/WebAppAdapterTest.php | 20 ------------------- tests/Unit/View/ViewTest.php | 12 ----------- 3 files changed, 45 deletions(-) diff --git a/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php index 76b40536..08665af7 100644 --- a/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php @@ -7,7 +7,6 @@ use Quantum\ResourceCache\ViewCache; use Quantum\App\Adapters\Web\WebContext; use Quantum\Router\MatchedRoute; -use Quantum\Debugger\Debugger; use Quantum\Di\DiContainer; use Quantum\App\AppContext; use Quantum\Router\Route; @@ -41,18 +40,6 @@ public function testPrepareRequestStageLoadsLanguage(): void $this->assertTrue(config()->has('lang')); } - public function testPrepareRequestStageWritesNothingToTheDebugger(): void - { - config()->set('app.debug', true); - - event()->listen('test.event', function (): void { - }); - - (new PrepareRequestStage())->process($this->webContext); - - $this->assertSame([], debugbar()->getStoreCell(Debugger::EVENTS)); - } - public function testPrepareRequestStageRegistersViewCache(): void { $this->assertFalse(Di::isRegistered(ViewCache::class)); diff --git a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php index 2b42f588..98f54570 100644 --- a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php +++ b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php @@ -10,7 +10,6 @@ use Quantum\Tests\Unit\AppTestCase; use Quantum\Http\Enums\ContentType; use Quantum\Http\Enums\StatusCode; -use Quantum\Debugger\DebuggerStore; use Quantum\Router\MatchedRoute; use Quantum\Debugger\Debugger; use Quantum\Router\Route; @@ -367,25 +366,6 @@ public function testWebAppAdapterStartDispatchesTheSendAfterEventOnceTheRequestI $this->assertNull($uriSeenAfterSend); } - public function testWebAppAdapterLeavesTheDebuggerStoreEmptyUntilTheToolbarRenders(): void - { - $store = new DebuggerStore(); - $store->flush(); - - $adapter = new WebAppAdapter($this->createContext()); - $adapter->boot(); - - config()->set('app.debug', true); - request()->create('GET', '/test/am/tests'); - - ob_start(); - $adapter->start(); - ob_end_clean(); - - $this->assertFalse($store->has(Debugger::EVENTS)); - $this->assertSame([], $store->all()); - } - public function testWebAppAdapterConstructorDoesNotBoot(): void { $this->clearAppContext(); diff --git a/tests/Unit/View/ViewTest.php b/tests/Unit/View/ViewTest.php index 1e5d6c4a..9b3ce6c7 100644 --- a/tests/Unit/View/ViewTest.php +++ b/tests/Unit/View/ViewTest.php @@ -6,7 +6,6 @@ use Quantum\View\Factories\ViewFactory; use Quantum\Tests\Unit\AppTestCase; use Quantum\Router\MatchedRoute; -use Quantum\Debugger\Debugger; use Quantum\View\RawParam; use Quantum\Router\Route; use Quantum\View\View; @@ -115,17 +114,6 @@ public function testGetViewFileReturnsTheLastRenderedView(): void $this->assertSame('index', $this->view->getViewFile()); } - public function testRenderWritesNothingToTheDebugger(): void - { - config()->set('app.debug', true); - - $this->view->setLayout('layout'); - - $this->view->render('index'); - - $this->assertSame([], debugbar()->getStoreCell(Debugger::ROUTES)); - } - public function testRenderWithoutLayout(): void { $this->expectException(ViewException::class); From 84710f18cdf7e3bd3c8e73c3ff0fee220c6a5510 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:40:46 +0400 Subject: [PATCH 07/10] [#539] Keep log messages in the logger and pass collected data straight to the debugbar tabs --- src/Debugger/Debugger.php | 60 ++++-- src/Logger/Adapters/MessageAdapter.php | 23 ++- tests/Unit/Debugger/DebuggerTest.php | 183 ++++++++---------- .../Logger/Adapters/MessageAdapterTest.php | 28 +-- .../Helpers/LoggerHelperFunctionsTest.php | 48 +---- tests/Unit/Logger/LoggerTest.php | 35 +--- 6 files changed, 154 insertions(+), 223 deletions(-) diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index 59ba94be..8e0ab008 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -17,6 +17,7 @@ use DebugBar\DataCollector\PhpInfoCollector; use DebugBar\DataCollector\MemoryCollector; use Quantum\Config\Exceptions\ConfigException; +use Quantum\Logger\Adapters\MessageAdapter; use Quantum\App\Exceptions\BaseException; use Quantum\View\Factories\ViewFactory; use Quantum\Di\Exceptions\DiException; @@ -148,8 +149,8 @@ public function resetStore(): void } /** - * Renders the debug bar. The events, routes and queries tabs are filled here, with the event listeners, - * the matched route, the rendered view and the query log as they are at this point. + * Renders the debug bar. Each tab shows its stored entries, followed by what is collected + * from the packages at this point. * @throws DebugBarException|DiException|ReflectionException|ConfigException|BaseException */ public function render(): string @@ -158,19 +159,8 @@ public function render(): string return ''; } - $this->clearStoreCell(self::EVENTS); - $this->addToStoreCell(self::EVENTS, 'info', event()->getRegistered()); - - $this->clearStoreCell(self::ROUTES); - $this->addToStoreCell(self::ROUTES, 'info', $this->routeInfo()); - - $this->clearStoreCell(self::QUERIES); - foreach ($this->queryLog() as $query) { - $this->addToStoreCell(self::QUERIES, 'info', $query); - } - - foreach ([self::MESSAGES, self::QUERIES, self::ROUTES, self::EVENTS, self::MAILS] as $tab) { - $this->createTab($tab); + foreach ($this->collect() as $tab => $entries) { + $this->createTab($tab, $entries); } $renderer = $this->getRenderer(); @@ -178,6 +168,37 @@ public function render(): string return $renderer->renderHead() . $renderer->render(); } + /** + * Collects the entries of each tab from the packages, as they are now + * @return array>> + * @throws DiException|ReflectionException|ConfigException|BaseException + */ + protected function collect(): array + { + $routeInfo = $this->routeInfo(); + $listeners = event()->getRegistered(); + + return [ + self::MESSAGES => $this->logMessages(), + self::QUERIES => array_map(fn ($query): array => ['info' => $query], $this->queryLog()), + self::ROUTES => $routeInfo !== [] ? [['info' => $routeInfo]] : [], + self::EVENTS => $listeners !== [] ? [['info' => $listeners]] : [], + self::MAILS => [], + ]; + } + + /** + * The messages of the debug-mode logger + * @return array> + * @throws DiException|ReflectionException|ConfigException|BaseException + */ + protected function logMessages(): array + { + $adapter = logger()->getAdapter(); + + return $adapter instanceof MessageAdapter ? $adapter->getMessages() : []; + } + /** * The matched route and the rendered view, without the values that are not set * @return array @@ -228,16 +249,15 @@ protected function queryLog(): array } /** - * Creates a tab + * Creates a tab with its stored entries, followed by the collected ones + * @param array> $collected * @throws DebugBarException */ - protected function createTab(string $type): void + protected function createTab(string $type, array $collected): void { $collector = $this->resolveMessagesCollector($type); - $messages = $this->store->get($type); - - foreach ($messages as $message) { + foreach (array_merge($this->store->get($type), $collected) as $message) { $fn = key($message); $collector->log($fn, $message[$fn]); } diff --git a/src/Logger/Adapters/MessageAdapter.php b/src/Logger/Adapters/MessageAdapter.php index 6c22da04..fd641073 100644 --- a/src/Logger/Adapters/MessageAdapter.php +++ b/src/Logger/Adapters/MessageAdapter.php @@ -11,7 +11,6 @@ namespace Quantum\Logger\Adapters; use Quantum\Logger\Contracts\ReportableInterface; -use Quantum\Debugger\Debugger; use Stringable; /** @@ -20,15 +19,23 @@ */ class MessageAdapter implements ReportableInterface { + /** + * Reported messages, as [level => message] + * @var array> + */ + private array $messages = []; + public function report(string $level, string|Stringable $message, ?array $context = []): void { - $tab = $context['tab'] ?? Debugger::MESSAGES; - - $debugger = debugbar(); - - if ($debugger->isEnabled()) { - $debugger->addToStoreCell($tab, $level, $message); - } + $this->messages[] = [$level => $message]; } + /** + * Gets the reported messages in the order they were reported + * @return array> + */ + public function getMessages(): array + { + return $this->messages; + } } diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index dd4e647c..eb6e1865 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -155,7 +155,7 @@ public function testDebugbarRenderReplaysArrayPayloadsThroughLog(): void $method = new \ReflectionMethod($this->debugger, 'createTab'); $method->setAccessible(true); - $method->invoke($this->debugger, Debugger::ROUTES); + $method->invoke($this->debugger, Debugger::ROUTES, []); $this->assertEquals( ['info' => ['Route' => '/welcome']], @@ -172,56 +172,77 @@ public function testDebugbarRenderReturnsEmptyWhenDisabled(): void $this->assertEmpty($output); } - public function testDebugbarRenderWritesTheRegisteredEventListenersToTheEventsTab(): void + public function testDebugbarRenderPassesNothingToTheTabsWhenDisabled(): void { - config()->set('app.debug', true); + config()->set('app.debug', false); - $listener = function (): void { - }; + event()->listen('test.event', function (): void { + }); - event()->listen('test.event', $listener); + request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); - $this->expectRendering(); + info('Logged message'); - $this->debugger->render(); + $this->connectSqlite(); + + Database::query('SELECT 1'); - $this->assertSame([['info' => ['test.event' => [$listener]]]], $this->debuggerStore->get(Debugger::EVENTS)); + $this->assertSame([], $this->renderTabs()); } - public function testDebugbarRenderWritesNothingToTheEventsTabWithoutEventListeners(): void + public function testDebugbarRenderPassesTheStoredEntriesBeforeTheCollectedOnes(): void { config()->set('app.debug', true); - $this->assertSame([], event()->getRegistered()); + $this->debugger->addToStoreCell(Debugger::MESSAGES, LogLevel::INFO, 'Written directly'); - $this->expectRendering(); + info('Logged message'); - $this->debugger->render(); + $tabs = $this->renderTabs(); - $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); + $this->assertSame([['info' => 'Written directly'], ['info' => 'Logged message']], $tabs[Debugger::MESSAGES]); } - public function testDebugbarRenderWritesNothingWhenDisabled(): void + public function testDebugbarRenderLeavesTheStoreUnchanged(): void { - config()->set('app.debug', false); + config()->set('app.debug', true); + + $this->debugger->addToStoreCell(Debugger::MESSAGES, LogLevel::INFO, 'Written directly'); + + info('Logged message'); event()->listen('test.event', function (): void { }); - request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); + $this->renderTabs(); - $this->connectSqlite(); + $this->assertSame([Debugger::MESSAGES => [['info' => 'Written directly']]], $this->debuggerStore->all()); + } - Database::query('SELECT 1'); + public function testDebugbarRenderListsTheRegisteredEventListenersInTheEventsTab(): void + { + config()->set('app.debug', true); - $this->debugger->render(); + $listener = function (): void { + }; + + event()->listen('test.event', $listener); - $this->assertSame([], $this->debuggerStore->get(Debugger::EVENTS)); - $this->assertSame([], $this->debuggerStore->get(Debugger::ROUTES)); - $this->assertSame([], $this->debuggerStore->get(Debugger::QUERIES)); + $tabs = $this->renderTabs(); + + $this->assertSame([['info' => ['test.event' => [$listener]]]], $tabs[Debugger::EVENTS]); } - public function testDebugbarRenderWritesTheMatchedRouteToTheRoutesTab(): void + public function testDebugbarRenderPassesNothingToTheEventsTabWithoutEventListeners(): void + { + config()->set('app.debug', true); + + $this->assertSame([], event()->getRegistered()); + + $this->assertArrayNotHasKey(Debugger::EVENTS, $this->renderTabs()); + } + + public function testDebugbarRenderListsTheMatchedRouteInTheRoutesTab(): void { config()->set('app.debug', true); @@ -234,9 +255,7 @@ public function testDebugbarRenderWritesTheMatchedRouteToTheRoutesTab(): void request()->setMatchedRoute(new MatchedRoute($route, ['id' => '5'])); - $this->expectRendering(); - - $this->debugger->render(); + $tabs = $this->renderTabs(); $this->assertSame([['info' => [ 'Methods' => 'GET, POST', @@ -249,10 +268,10 @@ public function testDebugbarRenderWritesTheMatchedRouteToTheRoutesTab(): void 'Prefix' => 'en', 'Middlewares' => ['Auth'], 'Params' => ['id' => '5'], - ]]], $this->debuggerStore->get(Debugger::ROUTES)); + ]]], $tabs[Debugger::ROUTES]); } - public function testDebugbarRenderWritesAClosureRouteToTheRoutesTab(): void + public function testDebugbarRenderListsAClosureRouteInTheRoutesTab(): void { config()->set('app.debug', true); @@ -261,16 +280,14 @@ public function testDebugbarRenderWritesAClosureRouteToTheRoutesTab(): void request()->setMatchedRoute(new MatchedRoute($route, [])); - $this->expectRendering(); - - $this->debugger->render(); + $tabs = $this->renderTabs(); $this->assertSame([['info' => [ 'Methods' => 'GET', 'Pattern' => 'ping', 'Module' => 'Api', 'Action' => 'Closure', - ]]], $this->debuggerStore->get(Debugger::ROUTES)); + ]]], $tabs[Debugger::ROUTES]); } public function testDebugbarRenderAddsTheRenderedViewToTheRoutesTab(): void @@ -284,45 +301,23 @@ public function testDebugbarRenderAddsTheRenderedViewToTheRoutesTab(): void $view->setLayout('layout'); $view->render('index'); - $this->expectRendering(); - - $this->debugger->render(); - - $routes = $this->debuggerStore->get(Debugger::ROUTES); + $routes = $this->renderTabs()[Debugger::ROUTES]; $this->assertCount(1, $routes); $this->assertSame('Test/Views/index', $routes[0]['info']['View']); $this->assertSame('SomeController', $routes[0]['info']['Controller']); } - public function testDebugbarRenderWritesNothingToTheRoutesTabWithoutARouteOrAView(): void + public function testDebugbarRenderPassesNothingToTheRoutesTabWithoutARouteOrAView(): void { config()->set('app.debug', true); $this->assertNull(request()->getMatchedRoute()); - $this->expectRendering(); - - $this->debugger->render(); - - $this->assertSame([], $this->debuggerStore->get(Debugger::ROUTES)); - } - - public function testDebugbarRenderListsTheMatchedRouteOnceWhenRenderedTwice(): void - { - config()->set('app.debug', true); - - request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); - - $this->expectRendering(); - - $this->debugger->render(); - $this->debugger->render(); - - $this->assertCount(1, $this->debuggerStore->get(Debugger::ROUTES)); + $this->assertArrayNotHasKey(Debugger::ROUTES, $this->renderTabs()); } - public function testDebugbarRenderWritesTheQueriesOfTheRequestToTheQueriesTab(): void + public function testDebugbarRenderListsTheQueriesOfTheRequestInTheQueriesTab(): void { config()->set('app.debug', true); @@ -330,11 +325,7 @@ public function testDebugbarRenderWritesTheQueriesOfTheRequestToTheQueriesTab(): Database::query('SELECT 1'); - $this->expectRendering(); - - $this->debugger->render(); - - $queries = $this->debuggerStore->get(Debugger::QUERIES); + $queries = $this->renderTabs()[Debugger::QUERIES]; $this->assertCount(count(Database::queryLog()), $queries); $this->assertSame(['info' => 'SELECT 1'], end($queries)); @@ -344,15 +335,13 @@ public function testDebugbarRenderLeavesTheDatabaseUnusedWhenTheRequestDidNotUse { config()->set('app.debug', true); - $this->expectRendering(); - - $this->debugger->render(); + $tabs = $this->renderTabs(); $this->assertFalse(Di::isRegistered(Database::class)); - $this->assertSame([], $this->debuggerStore->get(Debugger::QUERIES)); + $this->assertArrayNotHasKey(Debugger::QUERIES, $tabs); } - public function testDebugbarRenderWritesNothingToTheQueriesTabForAnAdapterWithoutAQueryLog(): void + public function testDebugbarRenderPassesNothingToTheQueriesTabForAnAdapterWithoutAQueryLog(): void { config()->set('app.debug', true); @@ -364,42 +353,19 @@ public function testDebugbarRenderWritesNothingToTheQueriesTabForAnAdapterWithou $this->assertSame(SleekDbal::class, db()->getOrmClass()); - $this->expectRendering(); - - $this->debugger->render(); - - $this->assertSame([], $this->debuggerStore->get(Debugger::QUERIES)); + $this->assertArrayNotHasKey(Debugger::QUERIES, $this->renderTabs()); } - public function testDebugbarRenderListsTheQueriesOnceWhenRenderedTwice(): void + public function testDebugbarRenderListsTheLoggedMessagesInTheMessagesTab(): void { config()->set('app.debug', true); - $this->connectSqlite(); - - Database::query('SELECT 1'); + info('First message'); + warning('Second message'); - $this->expectRendering(); + $tabs = $this->renderTabs(); - $this->debugger->render(); - $this->debugger->render(); - - $this->assertCount(count(Database::queryLog()), $this->debuggerStore->get(Debugger::QUERIES)); - } - - public function testDebugbarRenderListsTheEventListenersOnceWhenRenderedTwice(): void - { - config()->set('app.debug', true); - - event()->listen('test.event', function (): void { - }); - - $this->expectRendering(); - - $this->debugger->render(); - $this->debugger->render(); - - $this->assertCount(1, $this->debuggerStore->get(Debugger::EVENTS)); + $this->assertSame([['info' => 'First message'], ['warning' => 'Second message']], $tabs[Debugger::MESSAGES]); } /** @@ -419,10 +385,13 @@ private function connectSqlite(): void } /** - * Lets render() run against the mocked DebugBar: every tab gets a collector that accepts any message + * Renders against the mocked DebugBar and returns what each tab's collector received, as [tab => [[level => data], ...]] + * @return array>> */ - private function expectRendering(): void + private function renderTabs(): array { + $logged = []; + $rendererMock = Mockery::mock(JavascriptRenderer::class); $rendererMock->shouldReceive('setBaseUrl')->andReturnSelf(); $rendererMock->shouldReceive('addAssets')->andReturnSelf(); @@ -431,7 +400,17 @@ private function expectRendering(): void $this->debugBarMock->shouldReceive('getJavascriptRenderer')->andReturn($rendererMock); $this->debugBarMock->shouldReceive('hasCollector')->andReturn(true); - $this->debugBarMock->shouldReceive('getCollector') - ->andReturn(Mockery::mock(MessagesCollector::class)->shouldIgnoreMissing()); + $this->debugBarMock->shouldReceive('getCollector')->andReturnUsing(function (string $tab) use (&$logged) { + $collector = Mockery::mock(MessagesCollector::class); + $collector->shouldReceive('log')->andReturnUsing(function ($level, $data) use (&$logged, $tab): void { + $logged[$tab][] = [$level => $data]; + }); + + return $collector; + }); + + $this->debugger->render(); + + return $logged; } } diff --git a/tests/Unit/Logger/Adapters/MessageAdapterTest.php b/tests/Unit/Logger/Adapters/MessageAdapterTest.php index 65cb2c50..92837868 100644 --- a/tests/Unit/Logger/Adapters/MessageAdapterTest.php +++ b/tests/Unit/Logger/Adapters/MessageAdapterTest.php @@ -4,40 +4,28 @@ use Quantum\Logger\Adapters\MessageAdapter; use Quantum\Tests\Unit\AppTestCase; -use Quantum\Debugger\Debugger; -use Quantum\Di\Di; class MessageAdapterTest extends AppTestCase { private MessageAdapter $adapter; - private Debugger $debugger; public function setUp(): void { parent::setUp(); - if (!Di::isRegistered(Debugger::class)) { - Di::register(Debugger::class); - } - - $this->debugger = Di::get(Debugger::class); - - $this->debugger->initStore(); - $this->adapter = new MessageAdapter(); } - public function testReportAddsMessageToDebuggerStore(): void + public function testGetMessagesIsEmptyBeforeAnyReport(): void { - $level = 'info'; - $message = 'Test message'; - - $this->adapter->report($level, $message); - - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); + $this->assertSame([], $this->adapter->getMessages()); + } - $this->assertArrayHasKey($level, $storedMessages[0]); + public function testReportKeepsTheMessagesInOrder(): void + { + $this->adapter->report('info', 'Test message'); + $this->adapter->report('error', 'Error message'); - $this->assertEquals($message, $storedMessages[0][$level]); + $this->assertSame([['info' => 'Test message'], ['error' => 'Error message']], $this->adapter->getMessages()); } } diff --git a/tests/Unit/Logger/Helpers/LoggerHelperFunctionsTest.php b/tests/Unit/Logger/Helpers/LoggerHelperFunctionsTest.php index 9bfa5010..657c9b8f 100644 --- a/tests/Unit/Logger/Helpers/LoggerHelperFunctionsTest.php +++ b/tests/Unit/Logger/Helpers/LoggerHelperFunctionsTest.php @@ -8,33 +8,15 @@ use Quantum\Logger\Adapters\DailyAdapter; use Quantum\Logger\Enums\LoggerType; use Quantum\Tests\Unit\AppTestCase; -use Quantum\Debugger\Debugger; use Quantum\Logger\Logger; -use Quantum\Di\Di; class LoggerHelperFunctionsTest extends AppTestCase { - private Debugger $debugger; - public function setUp(): void { parent::setUp(); config()->set('app.debug', true); - - if (!Di::isRegistered(Debugger::class)) { - Di::register(Debugger::class); - } - - $this->debugger = Di::get(Debugger::class); - - $this->debugger->resetStore(); - } - - public function tearDown(): void - { - $this->debugger->resetStore(); - parent::tearDown(); } public function testLoggerHelperGetDefaultLoggerAdapter(): void @@ -94,54 +76,34 @@ public function testErrorHelper(): void { error('Fatal Error'); - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); - - $this->assertArrayHasKey('error', $storedMessages[0]); - - $this->assertEquals('Fatal Error', $storedMessages[0]['error']); + $this->assertSame([['error' => 'Fatal Error']], logger()->getAdapter()->getMessages()); } public function testWarningHelper(): void { warning('Warning!!'); - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); - - $this->assertArrayHasKey('warning', $storedMessages[0]); - - $this->assertEquals('Warning!!', $storedMessages[0]['warning']); + $this->assertSame([['warning' => 'Warning!!']], logger()->getAdapter()->getMessages()); } public function testNoticeHelper(): void { notice('Simple Notice'); - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); - - $this->assertArrayHasKey('notice', $storedMessages[0]); - - $this->assertEquals('Simple Notice', $storedMessages[0]['notice']); + $this->assertSame([['notice' => 'Simple Notice']], logger()->getAdapter()->getMessages()); } public function testInfoHelper(): void { info('For your information'); - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); - - $this->assertArrayHasKey('info', $storedMessages[0]); - - $this->assertEquals('For your information', $storedMessages[0]['info']); + $this->assertSame([['info' => 'For your information']], logger()->getAdapter()->getMessages()); } public function testDebugHelper(): void { debug('Debugging!!'); - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); - - $this->assertArrayHasKey('debug', $storedMessages[0]); - - $this->assertEquals('Debugging!!', $storedMessages[0]['debug']); + $this->assertSame([['debug' => 'Debugging!!']], logger()->getAdapter()->getMessages()); } } diff --git a/tests/Unit/Logger/LoggerTest.php b/tests/Unit/Logger/LoggerTest.php index 49b18ca2..d88542d9 100644 --- a/tests/Unit/Logger/LoggerTest.php +++ b/tests/Unit/Logger/LoggerTest.php @@ -5,28 +5,17 @@ use Quantum\Logger\Contracts\ReportableInterface; use Quantum\Logger\Adapters\MessageAdapter; use Quantum\Tests\Unit\AppTestCase; -use Quantum\Debugger\Debugger; use Quantum\Logger\Logger; -use Quantum\Di\Di; class LoggerTest extends AppTestCase { private Logger $logger; private MessageAdapter $adapter; - private Debugger $debugger; public function setUp(): void { parent::setUp(); - if (!Di::isRegistered(Debugger::class)) { - Di::register(Debugger::class); - } - - $this->debugger = Di::get(Debugger::class); - - $this->debugger->initStore(); - $this->adapter = new MessageAdapter(); $this->logger = new Logger($this->adapter); @@ -41,28 +30,14 @@ public function testLoggerGetAdapter(): void $this->assertInstanceOf(ReportableInterface::class, $this->logger->getAdapter()); } - public function testLoggerLogAddsMessageToDebuggerStore(): void + public function testLoggerLogReportsTheMessageToTheAdapter(): void { - $levelInfo = 'info'; - $messageInfo = 'Test message'; - - $levelError = 'error'; - $messageError = 'Error message'; - - $context = []; - - $this->logger->log($levelInfo, $messageInfo, $context); - - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); - - $this->assertArrayHasKey($levelInfo, $storedMessages[0]); - $this->assertEquals($messageInfo, $storedMessages[0][$levelInfo]); + $this->logger->log('info', 'Test message', []); - $this->logger->log($levelError, $messageError, $context); + $this->assertSame([['info' => 'Test message']], $this->adapter->getMessages()); - $storedMessages = $this->debugger->getStoreCell(Debugger::MESSAGES); + $this->logger->log('error', 'Error message', []); - $this->assertArrayHasKey($levelError, $storedMessages[1]); - $this->assertEquals($messageError, $storedMessages[1][$levelError]); + $this->assertSame([['info' => 'Test message'], ['error' => 'Error message']], $this->adapter->getMessages()); } } From 1332ee3da5ea2c573b3b297e56c8b1b5535e4a70 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:12:04 +0400 Subject: [PATCH 08/10] [#539] Keep the mailer transport log in the mailer and read it into the debugbar mails tab --- src/Debugger/Debugger.php | 17 ++++++++- src/Mailer/Adapters/SmtpAdapter.php | 5 ++- src/Mailer/Contracts/MailerInterface.php | 6 ++++ src/Mailer/Mailer.php | 1 + src/Mailer/Traits/MailerTrait.php | 18 ++++++++-- tests/Unit/Debugger/DebuggerTest.php | 22 ++++++++++++ .../Adapters/MailerAdapterTraitTest.php | 36 +++++++++++++++++++ .../Unit/Mailer/Adapters/SmtpAdapterTest.php | 23 ++++++++++++ 8 files changed, 122 insertions(+), 6 deletions(-) diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index 8e0ab008..a9608923 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -18,6 +18,7 @@ use DebugBar\DataCollector\MemoryCollector; use Quantum\Config\Exceptions\ConfigException; use Quantum\Logger\Adapters\MessageAdapter; +use Quantum\Mailer\Factories\MailerFactory; use Quantum\App\Exceptions\BaseException; use Quantum\View\Factories\ViewFactory; use Quantum\Di\Exceptions\DiException; @@ -183,10 +184,24 @@ protected function collect(): array self::QUERIES => array_map(fn ($query): array => ['info' => $query], $this->queryLog()), self::ROUTES => $routeInfo !== [] ? [['info' => $routeInfo]] : [], self::EVENTS => $listeners !== [] ? [['info' => $listeners]] : [], - self::MAILS => [], + self::MAILS => $this->mailLog(), ]; } + /** + * The transport log of the mailer, only when the request used it + * @return array> + * @throws DiException|ReflectionException|ConfigException|BaseException + */ + protected function mailLog(): array + { + if (!Di::isRegistered(MailerFactory::class)) { + return []; + } + + return array_map(fn (string $line): array => ['warning' => $line], mailer()->getAdapter()->getTransportLog()); + } + /** * The messages of the debug-mode logger * @return array> diff --git a/src/Mailer/Adapters/SmtpAdapter.php b/src/Mailer/Adapters/SmtpAdapter.php index 4f2c6dff..0aa4d9d6 100644 --- a/src/Mailer/Adapters/SmtpAdapter.php +++ b/src/Mailer/Adapters/SmtpAdapter.php @@ -14,7 +14,6 @@ use Quantum\Di\Exceptions\DiException; use Quantum\Mailer\Traits\MailerTrait; use PHPMailer\PHPMailer\PHPMailer; -use Quantum\Debugger\Debugger; use PHPMailer\PHPMailer\SMTP; use ReflectionException; use Exception; @@ -44,11 +43,11 @@ public function __construct(array $params) $this->setupSmtp($params); - if (config()->get('debug')) { + if (is_debug_mode()) { $this->mailer->SMTPDebug = SMTP::DEBUG_SERVER; $this->mailer->Debugoutput = function (string $message): void { - warning($message, ['tab' => Debugger::MAILS]); + $this->transportLog[] = $message; }; } } diff --git a/src/Mailer/Contracts/MailerInterface.php b/src/Mailer/Contracts/MailerInterface.php index e20caa91..4284d2c7 100644 --- a/src/Mailer/Contracts/MailerInterface.php +++ b/src/Mailer/Contracts/MailerInterface.php @@ -74,4 +74,10 @@ public function getBody(); * Sends an email */ public function send(): bool; + + /** + * Gets the transport log: the errors of failed sends and, where the transport reports it, its exchange with the server + * @return array + */ + public function getTransportLog(): array; } diff --git a/src/Mailer/Mailer.php b/src/Mailer/Mailer.php index a7372d66..fca07254 100644 --- a/src/Mailer/Mailer.php +++ b/src/Mailer/Mailer.php @@ -28,6 +28,7 @@ * @method MailerInterface setBody($message) * @method array|string getBody() * @method bool send() + * @method array getTransportLog() */ class Mailer { diff --git a/src/Mailer/Traits/MailerTrait.php b/src/Mailer/Traits/MailerTrait.php index 17a2b14b..fe381be1 100644 --- a/src/Mailer/Traits/MailerTrait.php +++ b/src/Mailer/Traits/MailerTrait.php @@ -12,7 +12,6 @@ use Quantum\Mailer\Contracts\MailerInterface; use Quantum\Di\Exceptions\DiException; -use Quantum\Debugger\Debugger; use Quantum\Mailer\MailTrap; use ReflectionException; use Quantum\Di\Di; @@ -84,6 +83,12 @@ trait MailerTrait */ protected array $stringAttachments = []; + /** + * Transport log + * @var array + */ + private array $transportLog = []; + abstract protected function prepare(): void; abstract protected function sendEmail(): bool; @@ -226,13 +231,22 @@ public function send(): bool if (!$sent) { $errors = $this->getTransportErrors(); if (!empty($errors)) { - warning(implode(', ', $errors), ['tab' => Debugger::MAILS]); + $this->transportLog = array_merge($this->transportLog, $errors); + warning(implode(', ', $errors)); } } return $sent; } + /** + * @inheritDoc + */ + public function getTransportLog(): array + { + return $this->transportLog; + } + /** * Gets the message ID * @throws Exception diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index eb6e1865..2ed83d69 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -5,6 +5,7 @@ use Quantum\Database\Adapters\Sleekdb\SleekDbal; use Quantum\Database\Adapters\Idiorm\IdiormDbal; use DebugBar\DataCollector\MessagesCollector; +use Quantum\Mailer\Factories\MailerFactory; use DebugBar\DataCollector\PhpInfoCollector; use Quantum\View\Factories\ViewFactory; use Quantum\Debugger\DebuggerStore; @@ -368,6 +369,27 @@ public function testDebugbarRenderListsTheLoggedMessagesInTheMessagesTab(): void $this->assertSame([['info' => 'First message'], ['warning' => 'Second message']], $tabs[Debugger::MESSAGES]); } + public function testDebugbarRenderListsTheTransportLogOfTheMailerInTheMailsTab(): void + { + config()->set('app.debug', true); + + $this->setPrivateProperty(mailer()->getAdapter(), 'transportLog', ['SERVER -> CLIENT: 220 ready', 'SERVER -> CLIENT: 535 Authentication failed']); + + $tabs = $this->renderTabs(); + + $this->assertSame([['warning' => 'SERVER -> CLIENT: 220 ready'], ['warning' => 'SERVER -> CLIENT: 535 Authentication failed']], $tabs[Debugger::MAILS]); + } + + public function testDebugbarRenderLeavesTheMailerUnbuiltWhenTheRequestDidNotUseIt(): void + { + config()->set('app.debug', true); + + $tabs = $this->renderTabs(); + + $this->assertFalse(Di::isRegistered(MailerFactory::class)); + $this->assertArrayNotHasKey(Debugger::MAILS, $tabs); + } + /** * Connects the in-memory SQLite database again, so the connection logs queries with the current app.debug */ diff --git a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php index 59db521d..dbfba7a6 100644 --- a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php +++ b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php @@ -3,7 +3,9 @@ namespace Quantum\Tests\Unit\Mailer\Adapters; use Quantum\Mailer\Adapters\SendinblueAdapter; +use Quantum\Mailer\Contracts\MailerInterface; use Quantum\Mailer\Adapters\SmtpAdapter; +use Quantum\Mailer\Traits\MailerTrait; use Quantum\Tests\Unit\AppTestCase; use Quantum\Config\Setup; @@ -92,4 +94,38 @@ public function testMailerTraitGetMessageId(): void $this->assertIsString($adapter->getMessageId()); } + + public function testMailerTraitGetTransportLogIsEmptyBeforeASend(): void + { + $this->assertSame([], $this->adapter->getTransportLog()); + } + + public function testMailerTraitSendKeepsTheTransportErrorsOfAFailedSendAndLogsThem(): void + { + config()->set('app.debug', true); + config()->set('mailer.mail_trap', false); + + $adapter = new class () implements MailerInterface { + use MailerTrait; + + protected function prepare(): void + { + } + + protected function sendEmail(): bool + { + return false; + } + + protected function getTransportErrors(): array + { + return ['Connection refused']; + } + }; + + $this->assertFalse($adapter->send()); + + $this->assertSame(['Connection refused'], $adapter->getTransportLog()); + $this->assertSame([['warning' => 'Connection refused']], logger()->getAdapter()->getMessages()); + } } diff --git a/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php b/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php index ff1c582c..30e86f42 100644 --- a/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php +++ b/tests/Unit/Mailer/Adapters/SmtpAdapterTest.php @@ -5,6 +5,7 @@ use Quantum\Tests\Unit\Mailer\MailerTestCase; use Quantum\Mailer\Contracts\MailerInterface; use Quantum\Mailer\Adapters\SmtpAdapter; +use PHPMailer\PHPMailer\SMTP; use Quantum\Config\Setup; class SmtpAdapterTest extends MailerTestCase @@ -100,6 +101,28 @@ public function testSmtpAdapterSetGetStringAttachments(): void $this->assertEquals('document.txt', current($attachments)['filename']); } + public function testSmtpAdapterKeepsTheSmtpExchangeInDebugMode(): void + { + config()->set('app.debug', true); + + $adapter = new SmtpAdapter(config()->get('mailer.smtp')); + + $debugOutput = $this->getPrivateProperty($adapter, 'mailer')->Debugoutput; + + $debugOutput('CLIENT -> SERVER: EHLO localhost', 2); + + $this->assertSame(['CLIENT -> SERVER: EHLO localhost'], $adapter->getTransportLog()); + } + + public function testSmtpAdapterKeepsNoSmtpExchangeOutsideDebugMode(): void + { + config()->set('app.debug', false); + + $adapter = new SmtpAdapter(config()->get('mailer.smtp')); + + $this->assertSame(SMTP::DEBUG_OFF, $this->getPrivateProperty($adapter, 'mailer')->SMTPDebug); + } + public function testSmtpAdapterSend(): void { $this->adapter->setFrom('john@hotmail.com', 'John Doe'); From a2100550b6d0b0f62283ccf919a57fc6f5701d55 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:21:07 +0400 Subject: [PATCH 09/10] [#539] Describe taking the debugbar out of the app flow and the packages in the changelog --- CHANGELOG.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a093bf4..0dbb8405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,9 +12,12 @@ The format is based on Keep a Changelog. - Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533) ### Changed -- **BREAKING:** Took the debugbar out of the app flow (#539): the App package no longer references the Debugger package. +- **BREAKING:** Took the debugbar out of the app flow and out of the packages that feed it (#539): no package outside `Quantum\Debugger` references it. Each package keeps its own state, and `Debugger::render()` reads it when the toolbar renders: every tab shows the entries written with `debugbar()->addToStoreCell()`, followed by the event listeners, the matched route and the rendered view, the query log, the log messages and the mailer transport log as they are at that point. The debugger store keeps only those direct writes, and the debugbar builds no database connection or mailer the request did not use. - Removed `InitDebuggerStage` (`Quantum\App\Stages\InitDebuggerStage` in 3.0.3) from the web boot. The debugger store creates a tab on its first write; code that wants the tabs up front can call `debugbar()->initStore()`. - - `Debugger::render()` fills the `events` tab with the event listeners registered when the toolbar renders, instead of `PrepareRequestStage` writing them while a matched request is prepared. Listeners added later in the request, for example in a controller, are listed too. + - The `events` tab lists the listeners registered when the toolbar renders, so listeners added later in the request, for example in a controller, are listed too. + - `View::__construct()` no longer takes a `Debugger`: it is `(Renderer, AssetManager, ViewCache)`. Code that gets the view through `view()` or `ViewFactory` is unaffected. Added `View::getViewFile()`, the last view rendered with `render()`. + - `MessageAdapter`, the logger in debug mode, keeps its messages in memory (`getMessages()`) instead of writing them into the debugger store, and no longer reads `$context['tab']`: a message logged with a tab is listed in the `messages` tab, and `debugbar()->getStoreCell('messages')` returns only direct writes. + - `MailerInterface` gains `getTransportLog()`, implemented in `MailerTrait`: the errors of failed sends and, for SMTP in debug mode, the exchange with the server. A custom adapter that implements the interface without the trait must add it. A failed send is still logged as a warning. - **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #538, #575, #576, #579, #583, #589): - `AppInterface` gains `boot()`. The adapter constructors only store the context, and `AppFactory` calls `boot()` right after creating the adapter, so `AppFactory::create(...)->start()` is unchanged. Code that creates an adapter directly must call `boot()` before `start()`. - Removed the `AppAdapter` base class and the `WebAppTrait` and `ConsoleAppTrait` traits: `WebAppAdapter` and `ConsoleAppAdapter` implement `AppInterface` directly and moved to `Quantum\App\Adapters\Web` and `Quantum\App\Adapters\Console`. The stages are grouped by the adapter that runs them: the boot stages both adapters run in `Quantum\App\Stages\Boot`, the others in `Quantum\App\Adapters\Web\Stages\Boot`, `...\Web\Stages\Runtime`, `...\Console\Stages\Boot` and `...\Console\Stages\Runtime`. Code that goes through `AppFactory` is unaffected; code that references these classes directly must update its imports. @@ -49,6 +52,9 @@ The format is based on Keep a Changelog. - The page not found response is JSON for every client that asks for JSON, not only when the `Accept` header is exactly `application/json`: clients such as axios (`application/json, text/plain, */*`), a `charset` parameter, `+json` types and AJAX requests used to get the HTML page. `page_not_found_response()` decides with `Request::expectsJson()` and takes an optional request (#581) - `core:env` now looks for the existing `.env` in the application base directory, so running it from another working directory asks for the confirmation instead of silently overwriting the file (#575) - The file rate limiter creates the missing parent folders of its `path` (for example `cache/data` when `cache/` does not exist), instead of failing every request that goes through the rate limit middleware with `mkdir(): No such file or directory` (#585) +- The debugbar `routes` tab shows the matched route again (methods, pattern, module, controller, action or `Closure`, name, group, prefix, middlewares and params) next to the rendered view; since 3.0.0 it showed only the view path (#539) +- The debugbar `queries` tab lists the queries the request ran; it has been empty since 2.6.0 (#539) +- `SmtpAdapter` records the SMTP exchange in debug mode: it checked `config()->get('debug')`, which is never set, instead of `app.debug`, so the exchange never reached the `mails` tab (#539) ## [3.0.3] - 2026-07-10 From 3f468245f90098578b4513eb2a4e8e8d3bc9d3d0 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 19:48:56 +0400 Subject: [PATCH 10/10] [#539] Read the query log only from a started and connected database, and tidy the Debugger --- src/Debugger/Debugger.php | 24 +++++++++++++++---- tests/Unit/Debugger/DebuggerTest.php | 35 ++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index a9608923..3c612470 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -12,11 +12,12 @@ use Quantum\Database\Contracts\RelationalInterface; use DebugBar\DataCollector\RequestDataCollector; +use Quantum\Config\Exceptions\ConfigException; use DebugBar\DataCollector\TimeDataCollector; use DebugBar\DataCollector\MessagesCollector; +use Quantum\Database\Contracts\DbalInterface; use DebugBar\DataCollector\PhpInfoCollector; use DebugBar\DataCollector\MemoryCollector; -use Quantum\Config\Exceptions\ConfigException; use Quantum\Logger\Adapters\MessageAdapter; use Quantum\Mailer\Factories\MailerFactory; use Quantum\App\Exceptions\BaseException; @@ -242,21 +243,34 @@ protected function routeInfo(): array ]; } - if (Di::isRegistered(ViewFactory::class) && ViewFactory::get()->getViewFile() !== null) { - $info['View'] = request()->getCurrentModule() . '/Views/' . ViewFactory::get()->getViewFile(); + $viewFile = Di::isRegistered(ViewFactory::class) ? ViewFactory::get()->getViewFile() : null; + + if ($viewFile !== null) { + $info['View'] = request()->getCurrentModule() . '/Views/' . $viewFile; } return array_filter($info, fn ($value): bool => $value !== null && $value !== []); } /** - * The queries run so far, only when the request used the database and its adapter keeps a query log + * The queries run so far, only when the request started the database, it is still connected + * and its adapter keeps a query log * @return array * @throws DiException|ReflectionException */ protected function queryLog(): array { - if (!Di::isRegistered(Database::class) || !is_subclass_of(db()->getOrmClass(), RelationalInterface::class)) { + if (!Di::has(Database::class)) { + return []; + } + + $ormClass = db()->getOrmClass(); + + if (!is_subclass_of($ormClass, DbalInterface::class) || !$ormClass::getConnection()) { + return []; + } + + if (!is_subclass_of($ormClass, RelationalInterface::class)) { return []; } diff --git a/tests/Unit/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index 2ed83d69..d44eea98 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -4,6 +4,7 @@ use Quantum\Database\Adapters\Sleekdb\SleekDbal; use Quantum\Database\Adapters\Idiorm\IdiormDbal; +use Quantum\Database\Exceptions\DatabaseException; use DebugBar\DataCollector\MessagesCollector; use Quantum\Mailer\Factories\MailerFactory; use DebugBar\DataCollector\PhpInfoCollector; @@ -357,6 +358,40 @@ public function testDebugbarRenderPassesNothingToTheQueriesTabForAnAdapterWithou $this->assertArrayNotHasKey(Debugger::QUERIES, $this->renderTabs()); } + public function testDebugbarRenderPassesNothingToTheQueriesTabAfterTheDatabaseDisconnected(): void + { + config()->set('app.debug', true); + + $this->connectSqlite(); + + Database::query('SELECT 1'); + + IdiormDbal::disconnect(); + + $this->assertArrayNotHasKey(Debugger::QUERIES, $this->renderTabs()); + } + + public function testDebugbarRenderPassesNothingToTheQueriesTabWhenTheDatabaseFailedToStart(): void + { + config()->set('app.debug', true); + + if (!config()->has('database')) { + config()->import(new Setup('config', 'database')); + } + + config()->set('database.default', 'unknown'); + + try { + db(); + $this->fail('db() should throw for an unknown adapter'); + } catch (DatabaseException $e) { + } + + $this->assertTrue(Di::isRegistered(Database::class)); + + $this->assertArrayNotHasKey(Debugger::QUERIES, $this->renderTabs()); + } + public function testDebugbarRenderListsTheLoggedMessagesInTheMessagesTab(): void { config()->set('app.debug', true);