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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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

Expand Down
27 changes: 0 additions & 27 deletions src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php

This file was deleted.

18 changes: 1 addition & 17 deletions src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<WebContext>
* @package Quantum\App
Expand All @@ -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
*/
Expand Down
2 changes: 0 additions & 2 deletions src/App/Adapters/Web/WebAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -49,7 +48,6 @@ public function boot(): void
new LoadAppConfigStage(),
new SetupErrorHandlerStage(),
new InitHttpStage(),
new InitDebuggerStage(),
new LoadModulesStage(),
]);

Expand Down
134 changes: 125 additions & 9 deletions src/Debugger/Debugger.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -142,17 +151,18 @@ 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
{
if (!$this->isEnabled()) {
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();
Expand All @@ -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<string, array<int, array<string, mixed>>>
* @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<int, array<string, string>>
* @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());
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* The messages of the debug-mode logger
* @return array<int, array<string, mixed>>
* @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<string, mixed>
* @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<mixed>
* @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<int, array<string, mixed>> $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]);
}
Expand Down
23 changes: 15 additions & 8 deletions src/Logger/Adapters/MessageAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
namespace Quantum\Logger\Adapters;

use Quantum\Logger\Contracts\ReportableInterface;
use Quantum\Debugger\Debugger;
use Stringable;

/**
Expand All @@ -20,15 +19,23 @@
*/
class MessageAdapter implements ReportableInterface
{
/**
* Reported messages, as [level => message]
* @var array<int, array<string, string|Stringable>>
*/
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<int, array<string, string|Stringable>>
*/
public function getMessages(): array
{
return $this->messages;
}
}
5 changes: 2 additions & 3 deletions src/Mailer/Adapters/SmtpAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
};
}
}
Expand Down
6 changes: 6 additions & 0 deletions src/Mailer/Contracts/MailerInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>
*/
public function getTransportLog(): array;
}
1 change: 1 addition & 0 deletions src/Mailer/Mailer.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
* @method MailerInterface setBody($message)
* @method array<string, mixed>|string getBody()
* @method bool send()
* @method array<string> getTransportLog()
*/
class Mailer
{
Expand Down
Loading
Loading