From 918ff119a8e26f4a0a996c188a446e5fe96b5ca6 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:11:15 +0400 Subject: [PATCH 1/4] docs: describe the web-only message logger and its limit --- v3.0/packages/logger/adapters.md | 17 ++++++++++++++--- v3.0/packages/logger/architecture.md | 14 ++++++++++++-- v3.0/packages/logger/contracts.md | 2 +- v3.0/packages/logger/helpers.md | 3 +-- v3.0/packages/logger/overview.md | 4 ++-- v3.0/packages/logger/usage.md | 8 ++++---- 6 files changed, 34 insertions(+), 14 deletions(-) diff --git a/v3.0/packages/logger/adapters.md b/v3.0/packages/logger/adapters.md index f4a7387..57916c6 100644 --- a/v3.0/packages/logger/adapters.md +++ b/v3.0/packages/logger/adapters.md @@ -58,15 +58,26 @@ Use `daily` when you want automatic file splitting by day. ## `message` -`MessageAdapter` is the logger of debug mode. It writes no file: it keeps each message in memory as `[level => message]`, in the order they were logged, and `getMessages()` returns them ([example](usage.md#check-what-was-logged-in-debug-mode)). The debug toolbar reads them into its `messages` tab when it renders ([Debugger Architecture](../debugger/architecture.md#where-each-tabs-data-comes-from)). +`MessageAdapter` is the logger of the web app in debug mode ([why only the web app](architecture.md#the-web-app-logs-to-message)). It writes no file: it keeps each message in memory as `[level => message]`, in the order they were logged, and `getMessages()` returns them ([example](usage.md#check-what-was-logged-in-debug-mode)). The debug toolbar reads them into its `messages` tab when it renders ([Debugger Architecture](../debugger/architecture.md#where-each-tabs-data-comes-from)). The adapter keeps only the level and the message. The context is not used, so file-style formatting and `trace` appends are specific to the file adapters, and a `tab` key does not move a message to another toolbar tab; to add an entry to another tab, write to the debugger directly ([Debugger Usage](../debugger/usage.md#writing-to-a-specific-tab)). +### Expected config shape + +A request that logs inside a loop can report thousands of lines, so the adapter keeps only the last ones. `logging.message.limit` sets how many: + +```php +'message' => [ + 'limit' => 1000, +], +``` + +Without the key the limit is `1000`. When the list grows past the limit, the oldest message is dropped; `0` keeps every message. + ### Contract - available only in debug mode -- keeps the messages for the life of the process; nothing clears them -- ignores file-path config because it has no constructor parameters +- takes the limit as its only constructor argument; the file-path config does not apply Outside debug mode, explicitly requesting `message` causes the factory to throw an adapter-not-supported exception. diff --git a/v3.0/packages/logger/architecture.md b/v3.0/packages/logger/architecture.md index 31182d7..1403d67 100644 --- a/v3.0/packages/logger/architecture.md +++ b/v3.0/packages/logger/architecture.md @@ -34,14 +34,24 @@ After that, the factory reads: - `logging.default` for the default adapter name - `logging..level` for the severity threshold - `logging.` for adapter-specific constructor parameters +- `logging.message.limit` for the number of messages the `message` adapter keeps ## Debug-mode override Debug mode changes the package behavior in two ways. -### Adapter selection is forced to `message` +### The web app logs to `message` -When `is_debug_mode()` is true, the factory ignores the requested adapter and the configured default and resolves `message` instead. +While you work on a page in debug mode, you want its log lines in the debug toolbar, not in a file you have to open. So when `is_debug_mode()` is true and the running app is the web app (`is_web_app()`), the factory ignores the requested adapter and the configured default and resolves `message` instead. + +A console command in debug mode has no toolbar to show its messages, so it keeps the requested adapter or the configured default and writes to the log file: + +```php +// inside a console command, in debug mode +info('Import finished'); // written by the default adapter, for example `single` to logs/app.log +``` + +Asking for `message` explicitly with `logger(LoggerType::MESSAGE)` still works in the console in debug mode. ### Threshold filtering is bypassed diff --git a/v3.0/packages/logger/contracts.md b/v3.0/packages/logger/contracts.md index 474d846..2ba0f41 100644 --- a/v3.0/packages/logger/contracts.md +++ b/v3.0/packages/logger/contracts.md @@ -37,7 +37,7 @@ If one part of the app resolves a logger whose config level is `debug`, and anot `LoggerFactory::resolve()` rejects `message` outside debug mode. -In debug mode, the opposite happens: the factory forces `message` even when you requested another adapter. +In debug mode, the opposite happens in the web app: the factory forces `message` even when you requested another adapter ([Debug-mode override](architecture.md#debug-mode-override)). So adapter selection is environment-sensitive, not purely call-driven. diff --git a/v3.0/packages/logger/helpers.md b/v3.0/packages/logger/helpers.md index 8609821..7b09cb9 100644 --- a/v3.0/packages/logger/helpers.md +++ b/v3.0/packages/logger/helpers.md @@ -22,7 +22,7 @@ $logger = logger(LoggerType::DAILY); $logger->warning('Slow downstream response'); ``` -Remember that debug mode overrides adapter selection to `message`. +Remember that in the web app, debug mode overrides adapter selection to `message` ([Debug-mode override](architecture.md#debug-mode-override)). ## Level helpers @@ -50,6 +50,5 @@ A few limits are easy to miss: - there are no global helpers for `critical`, `alert`, or `emergency` - the convenience helpers accept string messages, while `logger()` also lets you send array payloads through the PSR-3 methods - every helper call goes back through the shared factory -- in debug mode, helper output is kept in memory by the [`message` adapter](adapters.md#message) instead of written to files Use `logger()` when you need the full PSR-3 surface, adapter selection, or structured array messages. \ No newline at end of file diff --git a/v3.0/packages/logger/overview.md b/v3.0/packages/logger/overview.md index 347ba98..e42e90f 100644 --- a/v3.0/packages/logger/overview.md +++ b/v3.0/packages/logger/overview.md @@ -4,7 +4,7 @@ The Logger package gives Quantum a small PSR-3-compatible logging layer with thr - write to one file - write to a date-based daily file -- keep messages in memory during debug mode, for the debug toolbar +- keep messages in memory while the web app runs in debug mode, for the debug toolbar Use it when you want framework-level logging through `logger()` or the global level helpers, while keeping the output backend configurable. @@ -38,7 +38,7 @@ Logger supports these adapter names: In normal runtime, the factory uses the requested adapter or the configured default. -In debug mode, the factory always resolves the [`message` adapter](adapters.md#message) instead, so nothing is written to files. +In debug mode, the web app resolves the [`message` adapter](adapters.md#message) instead, so its messages go to the debug toolbar ([Debug-mode override](architecture.md#debug-mode-override)). ## Severity filtering diff --git a/v3.0/packages/logger/usage.md b/v3.0/packages/logger/usage.md index 07e3052..dca08e5 100644 --- a/v3.0/packages/logger/usage.md +++ b/v3.0/packages/logger/usage.md @@ -57,7 +57,7 @@ File adapters JSON-format array messages before writing them. The global level h ## Check what was logged in debug mode -In debug mode the messages stay in memory, so a test or a debug page can read them back from the [`message` adapter](adapters.md#message): +In debug mode the web app keeps the messages in memory, so a test or a debug page can read them back from the [`message` adapter](adapters.md#message): ```php info('Import started'); @@ -80,12 +80,12 @@ $messages = logger()->getAdapter()->getMessages(); ### Rely on debug-mode `message` when: -- you are troubleshooting locally -- you want logs in the debug toolbar instead of on disk +- you are troubleshooting a page locally +- you want its logs in the debug toolbar instead of on disk ## Practical caveats - `logger(LoggerType::MESSAGE)` is valid only in debug mode. -- `logger(LoggerType::DAILY)` or `logger(LoggerType::SINGLE)` still resolves to `message` in debug mode. +- In the web app, `logger(LoggerType::DAILY)` or `logger(LoggerType::SINGLE)` still resolves to `message` in debug mode. - Reusing a resolved logger reuses the adapter instance behind it. - Calling `log()` with a non-standard level string is not rejected by the package, but it can produce uneven behavior. Prefer the standard level methods instead. \ No newline at end of file From cbd462eeba88ab413cb1162405106195fd9156e6 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:13:08 +0400 Subject: [PATCH 2/4] docs: describe the mailer transport log limit and how mail_trap is read --- v3.0/packages/mailer/architecture.md | 10 ++++++---- v3.0/packages/mailer/contracts.md | 10 +++++++++- v3.0/packages/mailer/usage.md | 4 +++- 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/v3.0/packages/mailer/architecture.md b/v3.0/packages/mailer/architecture.md index b24aa34..7e0c8c0 100644 --- a/v3.0/packages/mailer/architecture.md +++ b/v3.0/packages/mailer/architecture.md @@ -32,6 +32,7 @@ After that, it reads: - `mailer.default` for the default adapter name - `mailer.` for adapter-specific constructor parameters - `mailer.mail_trap` during `send()` to decide whether to save or deliver +- `mailer.transport_log_limit` during `send()` to bound the [transport log](contracts.md#transport-log) ## Wrapper behavior @@ -45,10 +46,11 @@ That matters for adapter-specific features such as attachments: the wrapper does `MailerTrait::send()` does the same high-level work for every adapter: -1. call `prepare()` to translate the fluent fields into transport-specific payload data -2. choose `saveEmail()` when `mailer.mail_trap` is truthy, otherwise `sendEmail()` -3. reset the common message fields immediately after the attempt -4. log transport errors when the send failed +1. cut the transport log to its limit +2. call `prepare()` to translate the fluent fields into transport-specific payload data +3. choose `saveEmail()` when `mailer.mail_trap` is on, otherwise `sendEmail()` +4. reset the common message fields immediately after the attempt +5. log transport errors when the send failed The reset happens on both success and failure, so you cannot call `send()` twice on the same composed message without rebuilding it. diff --git a/v3.0/packages/mailer/contracts.md b/v3.0/packages/mailer/contracts.md index 3b0ba92..83840a0 100644 --- a/v3.0/packages/mailer/contracts.md +++ b/v3.0/packages/mailer/contracts.md @@ -78,7 +78,15 @@ if (!$mailer->setFrom('noreply@example.com')->setAddress('jane@example.com')->se } ``` -The log belongs to the adapter instance and nothing clears it. `MailerFactory` reuses one adapter per name, so in a long-running process it spans every send through that adapter. In debug mode the debug toolbar shows the default mailer's log in its `mails` tab ([Debugger Architecture](../debugger/architecture.md#where-each-tabs-data-comes-from)). +The log belongs to the adapter instance, and `MailerFactory` reuses one adapter per name. A worker that sends mail all day would keep every line, so each `send()` first cuts the log to its last `mailer.transport_log_limit` lines: + +```php +'transport_log_limit' => 1000, +``` + +Without the key the limit is `1000`; `0` keeps every line. The cut happens at the start of a send because every send reaches that point, also after a previous SMTP send threw. So after a send the log holds up to the limit plus the lines of that send, which is never cut halfway. + +In debug mode the debug toolbar shows the default mailer's log in its `mails` tab ([Debugger Architecture](../debugger/architecture.md#where-each-tabs-data-comes-from)). ## Mail trap contract diff --git a/v3.0/packages/mailer/usage.md b/v3.0/packages/mailer/usage.md index 7b4e8e2..faa7f22 100644 --- a/v3.0/packages/mailer/usage.md +++ b/v3.0/packages/mailer/usage.md @@ -75,7 +75,9 @@ If you need reply-to, the method name is `setReplay()` in this package. ## Enable local mail trapping -When `mailer.mail_trap` is truthy, `send()` saves a `.eml` file to `shared/emails` instead of contacting the transport. +When `mailer.mail_trap` is on, `send()` saves a `.eml` file to `shared/emails` instead of contacting the transport. + +The setting is read as a boolean, so it also works from `.env` (`'mail_trap' => env('MAIL_TRAP')`): `true`, `1`, `on` and `yes`, in any case, turn it on. Any other value leaves it off and sends real mail: `false`, `0`, `off`, `no`, an empty value, and also a typo such as `flase`. That is useful for local development and tests where you want to inspect the generated message. Create the `shared/emails` directory as part of project setup so each saved message has a destination. From 278796a0eba4313d01fa0b971e16c28bc44a1f98 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:14:39 +0400 Subject: [PATCH 3/4] docs: describe the app type, its helpers and the error output by app type --- v3.0/packages/app/architecture.md | 4 ++-- v3.0/packages/app/contracts.md | 17 ++++++++++++++++- v3.0/packages/app/helpers.md | 12 ++++++++++++ v3.0/packages/tracer/architecture.md | 6 ++++-- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/v3.0/packages/app/architecture.md b/v3.0/packages/app/architecture.md index 035583b..3631ae2 100644 --- a/v3.0/packages/app/architecture.md +++ b/v3.0/packages/app/architecture.md @@ -23,7 +23,7 @@ So the app you get back is already booted, and `start()` handles the run. The fa ## 2) Shared context -`AppContext` carries the application base directory and the DI container used to resolve shared services. From that context, code can resolve the objects both the web and the console boot provide: +`AppContext` carries the application base directory, the app type (web or console) and the DI container used to resolve shared services. From that context, code can resolve the objects both the web and the console boot provide: - `Environment` - `Config` @@ -121,7 +121,7 @@ The framework's commands are registered before the environment so that `core:env ## Runtime contexts -`WebContext` and `ConsoleContext` extend `AppContext`, with the same base directory and container. +`WebContext` and `ConsoleContext` extend `AppContext`, with the same base directory, container and app type. - `WebContext` reads the request, the routes, and the view cache from the container, and the matched route from the request. It stores the response to send. - `ConsoleContext` reads the Symfony application, input, and output from the container. diff --git a/v3.0/packages/app/contracts.md b/v3.0/packages/app/contracts.md index a48a77e..c9f786f 100644 --- a/v3.0/packages/app/contracts.md +++ b/v3.0/packages/app/contracts.md @@ -73,12 +73,27 @@ No adapter catches exceptions: a failing stage, in the boot or in the run, throw - base directory (`getBaseDir()`) - DI container (`getContainer()`) +- app type (`getType()`): `AppType::WEB` or `AppType::CONSOLE` - DI-resolved `Environment` (`getEnvironment()`) - DI-resolved `Config` (`getConfig()`) - the run state: `stop()` / `isStopped()` and the exit code (`getExitCode()` / `setExitCode()`, `ExitCode::SUCCESS` by default) The services are what both the web and the console boot provide. +### Creating one yourself + +`AppFactory` builds the context for you. A test or a custom bootstrap that creates one directly passes the app type as the third, required argument: + +```php +use Quantum\App\Enums\AppType; +use Quantum\Di\DiContainer; +use Quantum\App\AppContext; + +$context = new AppContext($baseDir, new DiContainer(), AppType::CONSOLE); +``` + +There is no default, so a context built by hand is never taken for the web app by accident. Code that needs to know which app is running asks [`is_web_app()` or `is_console_app()`](helpers.md#is_web_app-and-is_console_app). + ### Runtime contexts `WebContext` and `ConsoleContext` extend `AppContext` and add what only their kind of run has: @@ -86,7 +101,7 @@ The services are what both the web and the console boot provide. - `WebContext`: `getRequest()`, `getRoutes()` and `getViewCache()` from the container, `getMatchedRoute()` from the request, and the response to send (`getResponse()` / `setResponse()`) - `ConsoleContext`: `getApplication()`, `getInput()` and `getOutput()` from the container -A runtime context is built from the `AppContext` with the same base directory and container, but its own run state: a stop in the boot does not carry over into the run. +A runtime context is built from the `AppContext` with the same base directory, container and app type, but its own run state: a stop in the boot does not carry over into the run. ### Constraint diff --git a/v3.0/packages/app/helpers.md b/v3.0/packages/app/helpers.md index 69ea2ea..ac46491 100644 --- a/v3.0/packages/app/helpers.md +++ b/v3.0/packages/app/helpers.md @@ -85,6 +85,18 @@ Returns `true` only when the string matches the package's base64 validation patt Reads `config('app.debug')` and validates it as a boolean flag. +### `is_web_app()` and `is_console_app()` + +Tell whether the running application is the web app or the console, from the type on the current [`AppContext`](contracts.md#appcontext). Use them when a piece of shared code should behave differently in a request and in a `php qt` command: + +```php +if (is_console_app()) { + // a command: no response to render +} +``` + +The logger and the error handler make their web-or-console choices with these helpers too. Like the path helpers, they need `App::getContext()` to be initialized. + ### `export($var)` Exports a PHP value through Symfony VarExporter. diff --git a/v3.0/packages/tracer/architecture.md b/v3.0/packages/tracer/architecture.md index 525dfba..4974c09 100644 --- a/v3.0/packages/tracer/architecture.md +++ b/v3.0/packages/tracer/architecture.md @@ -7,13 +7,15 @@ Tracer runs as a short pipeline: 3. route uncaught throwables to CLI or web flow 4. print or render the final error output +The flow in step 3 follows the app that was booted ([`is_console_app()`](../app/helpers.md#is_web_app-and-is_console_app)), so `php qt` gets terminal output, and a request gets an error page, whether PHP-FPM or `php qt serve` serves it. + ## Registration `ErrorHandler::setup()` registers both error and exception handlers on the same instance. Call it once during bootstrap so the same handler owns both flows. ## CLI flow -For `PHP_SAPI === 'cli'`: +For the console app: - output goes to the injected CLI output, or to Symfony `ConsoleOutput` when you do not provide one - production prints the exception message for compact terminal output @@ -21,7 +23,7 @@ For `PHP_SAPI === 'cli'`: ## Web flow -For non-CLI runtime: +For the web app: - severity is resolved from the throwable before logging - production requests log through the matching logger method when available, with `error()` as the fallback method From bf6de7b6e0e40aa4b1bf7eac165c2527ca8adbd5 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:15:37 +0400 Subject: [PATCH 4/4] docs: describe Idiorm query logging in debug mode --- v3.0/advanced-features/database.md | 2 ++ v3.0/packages/database/adapters.md | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/v3.0/advanced-features/database.md b/v3.0/advanced-features/database.md index 441966f..62c4d94 100644 --- a/v3.0/advanced-features/database.md +++ b/v3.0/advanced-features/database.md @@ -64,6 +64,8 @@ These `Database` static methods are forwarded to the active adapter: Important: these APIs are implemented by `IdiormDbal` (SQL adapters). If `database.default` is `sleekdb`, SQL-only methods are not supported. +`Database::lastQuery()` and `Database::queryLog()` return queries only in debug mode ([query logging](../packages/database/adapters.md#connection-behavior)). + ## Transactions `Database::beginTransaction()`, `commit()`, `rollback()`, and `transaction(callable)` are available when using SQL adapters (`mysql`, `sqlite`, `pgsql`). diff --git a/v3.0/packages/database/adapters.md b/v3.0/packages/database/adapters.md index e04876f..d32a1fd 100644 --- a/v3.0/packages/database/adapters.md +++ b/v3.0/packages/database/adapters.md @@ -20,7 +20,9 @@ Driver-specific behavior: - `mysql` and `pgsql` use `host`, `dbname`, optional `port`, and optional `charset` - MySQL and PostgreSQL also set `username`, `password`, and a `PDO::MYSQL_ATTR_INIT_COMMAND` driver option for `SET NAMES ` -`logging` is enabled when `config()->get('app.debug', false)` is truthy. +Idiorm's `logging` option follows `is_debug_mode()`. In debug mode Idiorm keeps every query it runs, and that list is what `Database::lastQuery()`, `Database::queryLog()` and the debug toolbar's queries tab read. Outside debug mode it keeps none, so `lastQuery()` returns `null` and `queryLog()` returns `[]`. Since `is_debug_mode()` reads `app.debug` as a boolean, `DEBUG=false` from `.env` turns it off. + +The list has no limit, so a long-running command in debug mode holds it in memory until the command ends. ### Query behavior