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
4 changes: 2 additions & 2 deletions v3.0/advanced-features/request-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ The request lifecycle is that connection.

Follow one request, `GET /dashboard`, from the browser to the response:

1. Quantum boots the project: helpers, `.env`, config, the error handler, request and response, the debugger, and the modules with their routes.
1. Quantum boots the project: helpers, `.env`, config, the error handler, request and response, and the modules with their routes.
2. A browser's CORS preflight (`OPTIONS`) is answered right away with `204 No Content`.
3. The router looks for a route matching `/dashboard`. If there is none, the visitor gets a 404.
4. The route matched, so Quantum detects the language and prepares the view cache.
Expand All @@ -39,7 +39,7 @@ Quantum checks the request against the routes the modules registered.

If no route matches, the request ends in a 404. A browser gets your HTML error page, and an API client gets JSON; see [`page_not_found_response()`](../packages/http/helpers.md).

If a route matches, Quantum prepares what the rest of the request may need: it detects the language from the request (translations load on the first `t()` call), writes the registered event listeners to the debug bar when it is enabled, and sets up the view cache when it is enabled. A 404 does none of this work.
If a route matches, Quantum prepares what the rest of the request may need: it detects the language from the request (translations load on the first `t()` call) and sets up the view cache when it is enabled. A 404 does none of this work.

## 3. Middleware runs before the controller action

Expand Down
1 change: 0 additions & 1 deletion v3.0/packages/app/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ The web adapter:
- loads environment and app config
- installs the error handler
- registers request and response objects in DI
- initializes the debugger store
- loads module routes into a `RouteCollection`
- matches the current request and dispatches it through middleware

Expand Down
5 changes: 2 additions & 3 deletions v3.0/packages/app/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,7 @@ The full list of events and the order they fire in is in [Contracts](contracts.m
3. `LoadAppConfigStage`
4. `SetupErrorHandlerStage`
5. `InitHttpStage`
6. `InitDebuggerStage`
7. `LoadModulesStage`
6. `LoadModulesStage`

## Web runtime order

Expand All @@ -91,7 +90,7 @@ The full list of events and the order they fire in is in [Contracts](contracts.m
1. `HandlePreflightStage`: `OPTIONS` requests get `204 No Content`, and the run stops
2. `ResolveRouteStage`: matches the request against the loaded module routes and stores the match on the request
3. `HandleRouteNotFoundStage`: a request without a match gets Quantum's not-found response, and the run stops
4. `PrepareRequestStage`: resolves the Lang package (translations load on first use), logs the registered events to the debugbar in debug mode, and sets up the view cache when it is enabled
4. `PrepareRequestStage`: resolves the Lang package (translations load on first use) and sets up the view cache when it is enabled
5. `DispatchRequestStage`: middleware wraps the route dispatch or the cached response lookup

`SendResponseStage` is the closing stage: it appends the CORS headers, sends the response, and clears the request's route and input state, also when sending fails. It runs after a stop too, so the `204` and the not-found response are sent the same way.
Expand Down
2 changes: 1 addition & 1 deletion v3.0/packages/app/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Use it when you need to start either a web request lifecycle or a console comman
- `App` as the adapter-backed application object
- `AppContext` for base-directory and DI-backed shared runtime access
- built-in `web` and `console` adapters
- bootstrap stages for helpers, environment, config, HTTP, debugger, modules, and error handling
- bootstrap stages for helpers, environment, config, HTTP, modules, and error handling
- path and utility helpers such as `base_dir()`, `modules_dir()`, `slugify()`, and `uuid_random()`

## Quick start
Expand Down
82 changes: 39 additions & 43 deletions v3.0/packages/debugger/architecture.md
Original file line number Diff line number Diff line change
@@ -1,34 +1,52 @@
# Debugger Architecture

The Debugger package follows a simple pipeline:
When a layout calls `debugbar()->render()`, the toolbar is built from two sources: the entries your code wrote with `addToStoreCell()`, and what the debugger reads at that moment from the packages that own the data. Nothing is pushed into the debugger while the request runs; each package keeps its own state and does not reference the Debugger package.

1. boot the shared debugger instance
2. initialize tab storage
3. let framework components append structured messages into store cells
4. replay those messages into DebugBar collectors during layout render
Reading at render time needs no wiring: there is no listener to register and no boot step. A tab also shows what is true when the page renders, for example an event listener a controller added late in the request.

## Runtime flow
## Render flow

### 1. Boot stage
`Debugger::render()`:

`Quantum\App\Adapters\Web\Stages\Boot\InitDebuggerStage` runs in the web boot pipeline and calls `debugbar()->initStore()`.
1. returns an empty string when `app.debug` is off, without collecting anything
2. collects the entries of each tab from the packages (see below)
3. for each of the five tabs, gives the tab's `MessagesCollector` its stored entries first and the collected entries after them
4. returns `renderHead()` and `render()` output from the DebugBar JavaScript renderer

It runs after helpers, environment, config, error handler, and HTTP initialization, but before modules are loaded; see the [web boot order](../app/architecture.md#web-boot-order).
The collected entries go straight to the collectors. `render()` never writes them into the store and never clears it, so the store keeps only what code wrote into it directly.

### 2. Producers write into the store
## Where each tab's data comes from

Several framework components write diagnostics into the same store:
| Tab | Collected from | Only when |
|---|---|---|
| `messages` | the debug-mode logger: `logger()->getAdapter()->getMessages()` | always in debug mode; the logger is built during boot for the error handler |
| `queries` | `Database::queryLog()`, one entry per query | the request started the database, it is still connected, and its adapter keeps a query log (SleekDB does not) |
| `routes` | the matched route (`request()->getMatchedRoute()`) and the last rendered view (`View::getViewFile()`) | a route matched or a view was rendered |
| `events` | `event()->getRegistered()` | at least one listener is registered |
| `mails` | the default mailer's `getTransportLog()` | the request used the mailer |

- `Quantum\Logger\Adapters\MessageAdapter` writes log messages into `messages` or a caller-provided tab
- `Quantum\App\Adapters\Web\Stages\Runtime\PrepareRequestStage` writes the registered event listeners (`event()->getRegistered()`) into `events` when the debug bar is enabled, once a route has matched
- `Quantum\View\View::updateDebugger()` rewrites the `routes` tab to include the resolved view file
- mailer code routes failures into the `mails` tab through logger helpers
The debugger never builds a database connection or a mailer the request did not use.

Each write is appended as `[$level => $data]`.
The `routes` entry holds the route's methods, pattern, module, controller and action (`Closure` for a closure route), name, group, prefix, middlewares and params, plus the view path; values that are not set are left out:

```php
[
'Methods' => 'GET',
'Pattern' => '/[:alpha:2]?/posts',
'Module' => 'Web',
'Controller' => 'Modules\Web\Controllers\PostController',
'Action' => 'posts',
'Name' => 'posts',
'Params' => ['_segment0' => 'en'],
'View' => 'Web/Views/post/post',
]
```

The `mails` tab reads only the default mailer (`mailer()`), so the transport log of another adapter does not appear there; a failed send is also logged as a warning, which reaches the `messages` tab.

## Store model

`DebuggerStore` keeps data in a private static array:
`DebuggerStore` keeps the direct writes in a private static array:

```php
private static array $store = [];
Expand All @@ -41,32 +59,10 @@ Important consequences:
- `flush()` removes the entire store array
- `get($key)` returns `[]` when the key does not exist

## Render model

`Debugger::render()` loops over the built-in tab names and calls `createTab($tab)` for each one.

That list is fixed to `messages`, `queries`, `routes`, `events`, and `mails`.

`createTab()`:

1. creates a `MessagesCollector` for the tab if it does not already exist
2. fetches stored messages for that tab
3. calls the collector method named by the stored level (`info`, `warning`, and so on)
## Collector replay

Because the level name is executed as a method call, the write side and the collector API must stay in sync.

It also means store-backed custom tab names are not replayed automatically. Writing to some other cell is only useful if you add your own render path around the package.

## View-path merge behavior

The `routes` tab is not append-only.

`Quantum\View\View::updateDebugger()` reads the first existing `info` payload from `routes`, merges in:

```php
['View' => request()->getCurrentModule() . '/Views/' . $viewFile]
```
Each entry, stored or collected, has the shape `[$level => $data]`. `createTab()` creates a `MessagesCollector` for the tab if it does not exist yet and calls `log($level, $data)` for each entry.

and then clears and rewrites the tab.
The collectors live as long as the `Debugger` instance, so calling `render()` twice in one request hands them every entry again and the toolbar lists them twice.

So the final `routes` payload is a merged structure, not a chronological list of route-related events.
Only the five built-in tab names are rendered. Writing to some other store key is only useful if you add your own render path around the package.
12 changes: 8 additions & 4 deletions v3.0/packages/debugger/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ So this write:
$debugger->addToStoreCell('messages', 'warning', 'Disk nearly full');
```

is later replayed roughly as:
is later replayed as:

```php
$debugBar['messages']->warning('Disk nearly full');
$debugBar['messages']->log('warning', 'Disk nearly full');
```

The level string is therefore part of the runtime contract, not just metadata.
The level becomes the entry's label in the toolbar. `MessagesCollector::log()` accepts any level string, so a custom level shows up as its own label.

## Built-in tab contract

Expand All @@ -91,6 +91,10 @@ The package only renders five store-backed tabs:
- `events`
- `mails`

You can write to any key in `DebuggerStore`, but `Debugger::render()` only replays those built-in names.
You can write to any key in `DebuggerStore`, but `Debugger::render()` only renders those built-in names.

## The store holds only direct writes

The store contains exactly what code wrote with `addToStoreCell()`: `render()` never writes into it or clears it, so a direct write is always shown, before the [collected entries](architecture.md#render-flow) of its tab.

If you need additional tabs, treat that as package customization rather than normal application usage.
7 changes: 1 addition & 6 deletions v3.0/packages/debugger/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ Behavior:

## Where the helper is used in core

Current framework usage includes:

- `InitDebuggerStage` for store initialization
- `MessageAdapter` for logging into debug tabs
- default web layout templates for final rendering
- `ViewFactory` for injecting the debugger into `Quantum\View\View`
Outside the Debugger package, the framework calls it in one place: the default web layout templates render the toolbar with `<?= debugbar()->render() ?>`.

For application code, this helper is the supported API. The framework does not expose a separate factory class for the package.
48 changes: 13 additions & 35 deletions v3.0/packages/debugger/overview.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Debugger

The Debugger package is Quantum's bridge to PHP DebugBar. It collects request-time diagnostics in an in-memory store and renders them into a debug toolbar at the end of a web response.
The Debugger package is Quantum's bridge to PHP DebugBar. When a layout renders the toolbar, it shows the matched route and view, the queries, the log messages, the mail transport log and the event listeners of the request, together with anything your code added.

In current core code, the package is built around two pieces:
The package is built around two pieces:

- `Quantum\Debugger\Debugger`, which owns the DebugBar instance and rendering flow
- `Quantum\Debugger\DebuggerStore`, which keeps per-process tab data until render time
- `Quantum\Debugger\Debugger`, which owns the DebugBar instance, collects the data of the tabs and renders the toolbar
- `Quantum\Debugger\DebuggerStore`, which keeps the entries code writes directly with `addToStoreCell()`

## Entry points

Expand All @@ -15,32 +15,26 @@ In current core code, the package is built around two pieces:

That means every caller writes into the same `DebuggerStore` and the same underlying `DebugBar` instance.

### Boot-time initialization
### Tabs

Web apps initialize the package through `Quantum\App\Adapters\Web\Stages\Boot\InitDebuggerStage`, which calls:

```php
debugbar()->initStore();
```

`initStore()` creates five built-in tabs:
The toolbar has five built-in tabs:

- `messages`
- `queries`
- `routes`
- `events`
- `mails`

The store is still tolerant of missing keys because reads fall back to `[]`, but this boot stage is what pre-seeds the standard tabs used by the rest of the framework.
The package needs no boot step: a tab exists from its first write. `debugbar()->initStore()` creates the five tabs empty up front if your code wants them before anything is written.

## Enablement

`Debugger::isEnabled()` reads `config()->get('app.debug')` and validates it with `FILTER_VALIDATE_BOOLEAN`.

Practical effect:

- truthy values such as `true`, `1`, or `'true'` enable rendering and logging
- falsey values return an empty string from `render()` and make framework log adapters skip writes
- truthy values such as `true`, `1`, or `'true'` enable rendering
- falsey values make `render()` return an empty string without collecting anything

## What the package collects

Expand All @@ -52,24 +46,11 @@ The package ships these default DebugBar collectors on construction:
- `TimeDataCollector`
- `MemoryCollector`

Framework integrations then add package-specific messages into named tabs:

- logger messages go to `messages` by default
- mail failures can be routed to `mails`
- for a matched web route, the registered event listeners go to `events` (see [Architecture](architecture.md#2-producers-write-into-the-store))
- view rendering updates the `routes` tab with the final view path

The built-in render flow only replays those five tab names. If you write data into some other store key, it stays in the store unless you extend the debugger package yourself.

## Rendering contract

`render()` does three things when debugging is enabled:
The five named tabs are filled when the toolbar renders; [Architecture](architecture.md#where-each-tabs-data-comes-from) shows where each tab's data comes from.

1. ensures each built-in tab has a `MessagesCollector`
2. replays stored messages into the matching collector
3. returns `renderHead()` and `render()` output from the DebugBar JavaScript renderer
## Rendering

Quantum's default web module templates print that HTML directly in the main layout with `<?= debugbar()->render() ?>`.
`render()` returns the DebugBar head assets and the toolbar markup, or an empty string when debugging is off. Quantum's default web module templates print it in the main layout with `<?= debugbar()->render() ?>`.

## Asset requirement

Expand All @@ -83,8 +64,5 @@ The package does not publish those files by itself during normal request handlin
## Important constraints

- The store is static in `DebuggerStore`, so data is process-scoped, not request-object-scoped.
- `render()` does not clear the store after output.
- `render()` replays stored entries into the same collector objects each time, so calling it more than once in the same request can duplicate toolbar messages.
- `addToStoreCell()` ignores empty payloads, so `''`, `[]`, `0`, and `'0'` are not recorded.
- Message levels are invoked dynamically on `MessagesCollector`; unsupported level names would fail at call time.
- Only `messages`, `queries`, `routes`, `events`, and `mails` are auto-created as store-backed tabs.
- The level of an entry is passed to `MessagesCollector::log()` and shown as its label; any string works.
15 changes: 8 additions & 7 deletions v3.0/packages/debugger/usage.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Debugger Usage

Most applications use the Debugger package indirectly through Quantum's web stack, but the underlying API is small enough to use directly.
Most applications only render the toolbar, and with `app.debug` on its tabs [fill themselves](architecture.md#where-each-tabs-data-comes-from). The API below adds your own entries.

## Basic logging

Expand All @@ -9,7 +9,7 @@ debugbar()->addToStoreCell('messages', 'info', 'Booted module');
debugbar()->addToStoreCell('messages', 'warning', 'Slow downstream response');
```

Those entries are rendered into the `messages` tab when `app.debug` is enabled.
Those entries are rendered into the `messages` tab when `app.debug` is enabled, before the messages the request logged with `logger()`.

## Writing to a specific tab

Expand All @@ -21,9 +21,9 @@ use Quantum\Debugger\Debugger;
debugbar()->addToStoreCell(Debugger::MAILS, 'warning', 'SMTP handshake failed');
```

You can also route logger output into a tab by passing a `tab` context value to the framework's message logger path.
This is the way to put an entry into a tab other than `messages`: the logger lists everything in `messages`.

Stick to the built-in tab constants for normal usage. `render()` only replays `messages`, `queries`, `routes`, `events`, and `mails` from the store.
Stick to the built-in tab constants; other tab names are not rendered.

## Rendering in a layout

Expand All @@ -35,7 +35,7 @@ When debugging is disabled, this returns an empty string.

When debugging is enabled, it returns both the renderer head assets and the toolbar markup, so it should only be emitted once per page.

If you call `render()` more than once on the same shared debugger instance, stored messages are replayed again and can appear duplicated in the toolbar.
If you call `render()` more than once on the same shared debugger instance, every entry is passed to the collectors again and appears twice in the toolbar.

## Custom collectors

Expand All @@ -48,6 +48,7 @@ That means custom construction replaces the default collector set; it does not a
## Practical caveats

- Run `install:debugbar` before expecting the toolbar assets to load in the browser.
- `resetStore()` clears all tabs for the shared store.
- `clearStoreCell($cell)` empties a tab but keeps the tab key available.
- `resetStore()` clears every entry written with `addToStoreCell()`.
- `clearStoreCell($cell)` empties a tab's stored entries but keeps the tab key available.
- `getStoreCell($cell)` returns only the entries written with `addToStoreCell()`. To read the request's log lines, use `logger()->getAdapter()->getMessages()`.
- Because the helper is DI-backed, mid-request config changes do not rebuild the debugger instance automatically.
2 changes: 1 addition & 1 deletion v3.0/packages/event/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ Both methods reject an empty name with `EventException::emptyEventName()` (`The
## Who dispatches inside the framework

- The app pipeline dispatches the runtime stage events, such as `web.preflight.before` and `console.command.before`; the boot dispatches none. The full list and the order they fire in are on [App Contracts](../app/contracts.md#lifecycle-events).
- When the debug bar is enabled, a matched web request writes `getRegistered()` to the debug bar's `events` tab; see [Debugger Architecture](../debugger/architecture.md).
- The debug toolbar reads `getRegistered()` into its `events` tab when it renders; see [Debugger Architecture](../debugger/architecture.md#where-each-tabs-data-comes-from).

Everything else is up to your application.
Loading