diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b794c028..0dbb8405b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +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 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()`. + - 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. @@ -46,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 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 3c84e6f69..000000000 --- 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/Stages/Runtime/PrepareRequestStage.php b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php index bb55dcab4..f60bda193 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/App/Adapters/Web/WebAppAdapter.php b/src/App/Adapters/Web/WebAppAdapter.php index 747c71912..8511924a2 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/src/Debugger/Debugger.php b/src/Debugger/Debugger.php index 558063b64..3c6124700 100644 --- a/src/Debugger/Debugger.php +++ b/src/Debugger/Debugger.php @@ -10,16 +10,25 @@ namespace Quantum\Debugger; +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\Logger\Adapters\MessageAdapter; +use Quantum\Mailer\Factories\MailerFactory; +use Quantum\App\Exceptions\BaseException; +use Quantum\View\Factories\ViewFactory; use Quantum\Di\Exceptions\DiException; use DebugBar\JavascriptRenderer; use DebugBar\DebugBarException; +use Quantum\Database\Database; use ReflectionException; use DebugBar\DebugBar; +use Quantum\Di\Di; /** * Class Debugger @@ -142,8 +151,9 @@ public function resetStore(): void } /** - * Renders the debug bar - * @throws DebugBarException|DiException|ReflectionException + * 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 { @@ -151,8 +161,8 @@ public function render(): string return ''; } - 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(); @@ -161,16 +171,122 @@ public function render(): string } /** - * Creates a tab + * 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 => $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> + * @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 + * @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(), + ]; + } + + $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 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::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 []; + } + + return Database::queryLog(); + } + + /** + * 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 6c22da049..fd641073c 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/src/Mailer/Adapters/SmtpAdapter.php b/src/Mailer/Adapters/SmtpAdapter.php index 4f2c6dffd..0aa4d9d61 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 e20caa913..4284d2c74 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 a7372d660..fca072549 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 17a2b14be..fe381be17 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/src/View/Factories/ViewFactory.php b/src/View/Factories/ViewFactory.php index b154a7c48..fa592d4ef 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 bf160dd23..7945dbeb0 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/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/PrepareRequestStageTest.php index 1d677b4dc..08665af74 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,46 +40,6 @@ public function testPrepareRequestStageLoadsLanguage(): void $this->assertTrue(config()->has('lang')); } - public function testPrepareRequestStageLogsRegisteredEventsToDebugger(): 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 { - }); - - (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/Debugger/DebuggerTest.php b/tests/Unit/Debugger/DebuggerTest.php index 4e963ba06..d44eea98a 100644 --- a/tests/Unit/Debugger/DebuggerTest.php +++ b/tests/Unit/Debugger/DebuggerTest.php @@ -2,14 +2,24 @@ namespace Quantum\Tests\Unit\Debugger; +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; +use Quantum\View\Factories\ViewFactory; use Quantum\Debugger\DebuggerStore; 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 @@ -26,6 +36,7 @@ public function setUp(): void parent::setUp(); $this->debuggerStore = new DebuggerStore(); + $this->debuggerStore->flush(); $this->debugBarMock = Mockery::mock(DebugBar::class); @@ -146,7 +157,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']], @@ -162,4 +173,301 @@ public function testDebugbarRenderReturnsEmptyWhenDisabled(): void $this->assertEmpty($output); } + + public function testDebugbarRenderPassesNothingToTheTabsWhenDisabled(): void + { + config()->set('app.debug', false); + + event()->listen('test.event', function (): void { + }); + + request()->setMatchedRoute(new MatchedRoute(new Route(['GET'], 'posts', 'PostController', 'posts'), [])); + + info('Logged message'); + + $this->connectSqlite(); + + Database::query('SELECT 1'); + + $this->assertSame([], $this->renderTabs()); + } + + public function testDebugbarRenderPassesTheStoredEntriesBeforeTheCollectedOnes(): void + { + config()->set('app.debug', true); + + $this->debugger->addToStoreCell(Debugger::MESSAGES, LogLevel::INFO, 'Written directly'); + + info('Logged message'); + + $tabs = $this->renderTabs(); + + $this->assertSame([['info' => 'Written directly'], ['info' => 'Logged message']], $tabs[Debugger::MESSAGES]); + } + + public function testDebugbarRenderLeavesTheStoreUnchanged(): void + { + config()->set('app.debug', true); + + $this->debugger->addToStoreCell(Debugger::MESSAGES, LogLevel::INFO, 'Written directly'); + + info('Logged message'); + + event()->listen('test.event', function (): void { + }); + + $this->renderTabs(); + + $this->assertSame([Debugger::MESSAGES => [['info' => 'Written directly']]], $this->debuggerStore->all()); + } + + public function testDebugbarRenderListsTheRegisteredEventListenersInTheEventsTab(): void + { + config()->set('app.debug', true); + + $listener = function (): void { + }; + + event()->listen('test.event', $listener); + + $tabs = $this->renderTabs(); + + $this->assertSame([['info' => ['test.event' => [$listener]]]], $tabs[Debugger::EVENTS]); + } + + 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); + + $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'])); + + $tabs = $this->renderTabs(); + + $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'], + ]]], $tabs[Debugger::ROUTES]); + } + + public function testDebugbarRenderListsAClosureRouteInTheRoutesTab(): void + { + config()->set('app.debug', true); + + $route = (new Route(['GET'], 'ping', null, null, function (): void { + }))->module('Api'); + + request()->setMatchedRoute(new MatchedRoute($route, [])); + + $tabs = $this->renderTabs(); + + $this->assertSame([['info' => [ + 'Methods' => 'GET', + 'Pattern' => 'ping', + 'Module' => 'Api', + 'Action' => 'Closure', + ]]], $tabs[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'); + + $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 testDebugbarRenderPassesNothingToTheRoutesTabWithoutARouteOrAView(): void + { + config()->set('app.debug', true); + + $this->assertNull(request()->getMatchedRoute()); + + $this->assertArrayNotHasKey(Debugger::ROUTES, $this->renderTabs()); + } + + public function testDebugbarRenderListsTheQueriesOfTheRequestInTheQueriesTab(): void + { + config()->set('app.debug', true); + + $this->connectSqlite(); + + Database::query('SELECT 1'); + + $queries = $this->renderTabs()[Debugger::QUERIES]; + + $this->assertCount(count(Database::queryLog()), $queries); + $this->assertSame(['info' => 'SELECT 1'], end($queries)); + } + + public function testDebugbarRenderLeavesTheDatabaseUnusedWhenTheRequestDidNotUseIt(): void + { + config()->set('app.debug', true); + + $tabs = $this->renderTabs(); + + $this->assertFalse(Di::isRegistered(Database::class)); + $this->assertArrayNotHasKey(Debugger::QUERIES, $tabs); + } + + public function testDebugbarRenderPassesNothingToTheQueriesTabForAnAdapterWithoutAQueryLog(): 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->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); + + info('First message'); + warning('Second message'); + + $tabs = $this->renderTabs(); + + $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 + */ + private function connectSqlite(): void + { + if (!config()->has('database')) { + config()->import(new Setup('config', 'database')); + } + + config()->set('database.default', 'sqlite'); + + IdiormDbal::disconnect(); + + db(); + } + + /** + * Renders against the mocked DebugBar and returns what each tab's collector received, as [tab => [[level => data], ...]] + * @return array>> + */ + private function renderTabs(): array + { + $logged = []; + + $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')->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 65cb2c50f..928378683 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 9bfa50107..657c9b8fb 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 49b18ca28..d88542d90 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()); } } diff --git a/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php b/tests/Unit/Mailer/Adapters/MailerAdapterTraitTest.php index 59db521d7..dbfba7a69 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 ff1c582c9..30e86f423 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'); diff --git a/tests/Unit/View/ViewTest.php b/tests/Unit/View/ViewTest.php index b252e2cb6..9b3ce6c79 100644 --- a/tests/Unit/View/ViewTest.php +++ b/tests/Unit/View/ViewTest.php @@ -100,6 +100,20 @@ 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 testRenderWithoutLayout(): void { $this->expectException(ViewException::class);