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
2 changes: 2 additions & 0 deletions v3.0/advanced-features/database.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
4 changes: 2 additions & 2 deletions v3.0/packages/app/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion v3.0/packages/app/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,20 +73,35 @@ 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:

- `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

Expand Down
12 changes: 12 additions & 0 deletions v3.0/packages/app/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion v3.0/packages/database/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <charset>`

`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

Expand Down
17 changes: 14 additions & 3 deletions v3.0/packages/logger/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
14 changes: 12 additions & 2 deletions v3.0/packages/logger/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,24 @@ After that, the factory reads:
- `logging.default` for the default adapter name
- `logging.<adapter>.level` for the severity threshold
- `logging.<adapter>` 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

Expand Down
2 changes: 1 addition & 1 deletion v3.0/packages/logger/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
3 changes: 1 addition & 2 deletions v3.0/packages/logger/helpers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions v3.0/packages/logger/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down
8 changes: 4 additions & 4 deletions v3.0/packages/logger/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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.
10 changes: 6 additions & 4 deletions v3.0/packages/mailer/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ After that, it reads:
- `mailer.default` for the default adapter name
- `mailer.<adapter>` 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

Expand All @@ -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.

Expand Down
10 changes: 9 additions & 1 deletion v3.0/packages/mailer/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion v3.0/packages/mailer/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
6 changes: 4 additions & 2 deletions v3.0/packages/tracer/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@ 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
- debug prints the exception class, message, file/line, and trace

## 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
Expand Down