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: 1 addition & 1 deletion v3.0/advanced-features/request-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Follow one request, `GET /dashboard`, from the browser to the response:
6. If the page is in the view cache, that copy is the response. Otherwise the controller action runs.
7. The response is sent with the CORS headers from `config/cors`.

Each of these steps is a stage in a pipeline. The stage names and their exact order are listed in [App Architecture](../packages/app/architecture.md#web-boot-order).
Each of these steps is a stage in a pipeline. The stage names and their exact order are listed in [App Architecture](../packages/app/architecture.md#web-boot-order), and the events each step dispatches for your own listeners are in [Hooking into the running app](../packages/app/architecture.md#hooking-into-the-running-app).

## 1. The request enters the application

Expand Down
2 changes: 1 addition & 1 deletion v3.0/cli/console-commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ That base class gives commands:
- optional help text
- argument definitions
- option definitions
- `$requiresEnvironment` (`true` by default), which says whether the command needs `.env` and the app config; see [When the boot fails](qt-command-line-tool.md#when-the-boot-fails)
- `$requiresEnvironment` (`true` by default), which lets a framework command such as `core:env` run when `.env` is missing or malformed; see [Commands without a working environment](qt-command-line-tool.md#commands-without-a-working-environment)
- helper methods such as:
- `getArgument()`
- `getOption()`
Expand Down
53 changes: 15 additions & 38 deletions v3.0/cli/qt-command-line-tool.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,8 @@ Before anything else, it sets up:

So `qt` is not a shell wrapper around random scripts. It boots a real console application.

### 3. Quantum boots the project
Next, Quantum loads what commands usually rely on: helper functions, `.env`, the app config, and the error handler. It also names the console application from `app.name` and `app.version`.

This boot is the same for every command. If one of its steps fails, Quantum does not stop here; see [When the boot fails](#when-the-boot-fails).

### 4. Core commands are registered
Quantum then registers the framework's built-in commands from:
### 3. Helpers load and core commands are registered
Next, Quantum loads the helper functions and registers the framework's built-in commands from:

```text
Quantum\Console\Commands
Expand All @@ -73,14 +68,17 @@ These include commands such as:
- `core:env`
- `core:key`

### 4. Quantum loads the project
Then Quantum loads what commands usually rely on: `.env`, the app config, and the error handler. It also names the console application from `app.name` and `app.version`.

### 5. App commands are registered
After that, Quantum registers project-level commands from:
Last, Quantum registers project-level commands from:

```text
shared/Commands/
```

in the starter project.
in the starter project. They come after the environment and the config, so a command's constructor can use `env()` and `config()`.

That means `qt` can run both:

Expand All @@ -90,8 +88,7 @@ That means `qt` can run both:
### 6. The command is validated and executed
The console app reads the first CLI argument as the command name.

If the command does not exist, Quantum throws an error.
If the boot failed and the command requires the environment, Quantum throws the original boot error.
If the command does not exist, Quantum throws ``Command `<name>` is not defined``.
Otherwise, Symfony Console runs it.

## What `qt` does for developers
Expand Down Expand Up @@ -155,44 +152,24 @@ this is roughly what happens:

The same pattern applies to other commands.

## When the boot fails
## Commands without a working environment

Picture a fresh clone of a project: there is no `.env` yet, and the first thing you run is:

```bash
php qt core:env
```

That command creates `.env` from `.env.example`, so it has to work exactly when loading `.env` cannot. This is why the console boot keeps going when one of its steps fails: Quantum remembers the error and still registers and runs commands.
That command creates `.env` from `.env.example`, so it has to work exactly when loading `.env` cannot. When `.env` is missing or cannot be parsed, Quantum therefore looks at the requested command before it loads the rest of the project:

Each command then decides whether it can run without the failed part:
- `core:env`, and Symfony's `list` and `help`, run. The boot stops before the environment, so nothing that needs it is loaded: no app config, no error handler, and none of your own commands.
- Every other command stops right away with the error of the environment file, for example ``The file `.env` not found.`` or ``Failed to parse dotenv file``, instead of failing later with a confusing one.

- `core:env`, and Symfony's `list` and `help`, run normally.
- A command that needs the environment, such as `migration:migrate`, stops with the original error (for example ``The file `.env` not found.``) instead of failing later with a confusing one.

Your own commands need the environment by default. If a command must also run without it, for example a setup check, set `$requiresEnvironment` to `false`:

```php
namespace Shared\Commands;
Once `.env` works again, for example after `php qt core:env --yes` has recreated it from `.env.example`, every command runs as usual.

use Quantum\Console\CliCommand;

class DoctorCommand extends CliCommand
{
protected ?string $name = 'app:doctor';

protected ?string $description = 'Checks the project setup';

protected bool $requiresEnvironment = false;

public function exec(): void
{
$this->info('Checking the project setup...');
}
}
```
Your own commands are registered last ([step 5](#5-app-commands-are-registered)), after the point where the boot stops, so `php qt list` shows only the framework's commands and running one of yours reports the environment error. Which framework commands can run without the environment is set with `CliCommand::$requiresEnvironment`, which `core:env` sets to `false`.

After a failed boot the application name is not read from your config, so `php qt list` shows it as `UNKNOWN`.
Without a working `.env` the application name is not read from your config, so `php qt list` shows it as `UNKNOWN`.

## In one sentence

Expand Down
6 changes: 3 additions & 3 deletions v3.0/packages/app/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Quantum ships two built-in app adapters: `web` and `console`.

Both satisfy the same `start(): ?int` contract, but they boot different runtime services and handle different entry-point work.
Both follow the same lifecycle, `boot()` and then `start(): ?int` (see [Contracts](contracts.md#appinterface)), but they boot different runtime services and handle different entry-point work.

## Web adapter

Expand Down Expand Up @@ -52,9 +52,9 @@ exit($app->start() ?? 0);

### What it does

Every `php qt <command>` goes the same way: the adapter boots the project (helpers, `.env`, app config, error handler), registers the framework commands and your commands from `shared/Commands`, checks that the requested command exists, runs it through Symfony Console, and returns its exit code.
Every `php qt <command>` goes the same way: the adapter boots the project (helpers, the framework commands, `.env`, app config, error handler, then your commands from `shared/Commands`), checks that the requested command exists, runs it through Symfony Console, and returns its exit code.

The boot is the same for every command. If it fails, for example on a fresh clone without `.env`, commands are still registered and can run; see [When the boot fails](../../cli/qt-command-line-tool.md#when-the-boot-fails).
Without a working `.env`, only a few commands such as `core:env` can run; see [Commands without a working environment](../../cli/qt-command-line-tool.md#commands-without-a-working-environment).

The adapter does not make the environment writable. A command that changes `.env` through `environment()->updateRow()` calls `setMutable(true)` first, as `core:key` does.

Expand Down
97 changes: 59 additions & 38 deletions v3.0/packages/app/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,24 @@ This page follows that path: the factory that creates the app, the context every
$app = AppFactory::create(AppType::WEB, __DIR__);
```

It does three things:
It does four things:

1. validates the requested adapter type
2. creates a new `DiContainer` and `AppContext`
3. wraps the selected adapter in `Quantum\App\App`
3. creates the adapter and boots it with `boot()`
4. wraps the adapter in `Quantum\App\App`

The factory also stores the created app in a static cache keyed by app type.
So the app you get back is already booted, and `start()` handles the run. The factory also stores the created app in a static cache keyed by app type.

## 2) Shared context

`AppContext` carries two pieces of runtime state:

- the application base directory
- 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 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`

It also carries the state of the run it is passed through: whether a stage ended the run early (`stop()` / `isStopped()`) and the exit code.

`App` also stores the current context statically. Helpers like `base_dir()` depend on that global context being initialized first.

Each run also gets a runtime context that extends `AppContext` with what only that kind of app has. See [Runtime contexts](#runtime-contexts).
Expand All @@ -40,30 +38,43 @@ Each run also gets a runtime context that extends `AppContext` with what only th

Each adapter runs two pipelines, both with the generic `Quantum\App\Pipeline`:

- the **boot** pipeline, during construction, with the shared `AppContext`
- the **runtime** pipeline, in `start()`, with a runtime context (`WebContext` or `ConsoleContext`)
- the **boot** pipeline, in `boot()`, with the shared `AppContext`
- the **runtime** pipeline, in `start()`, with a new runtime context (`WebContext` or `ConsoleContext`)

The stages run in order, and each receives the context. A stage can end the run early: an `OPTIONS` request, for example, is fully answered by the first web stage, so it calls `$context->stop()` and the stages that would match a route and dispatch it are skipped. Some work still has to happen after such an early end, so a pipeline can have a **closing stage** that runs last in any case: the web runtime uses it to send the response.

The pipeline contract is simple: every stage receives the context and runs once, in order. A stage may declare `BEFORE` / `AFTER` event names that the pipeline dispatches around it; see [Contracts](contracts.md#stage-and-pipeline-contract).
A stop ends only its own pipeline. When a boot stage stops, the rest of the boot is skipped, but `start()` still runs; the console uses this when `.env` cannot be loaded, see [Console boot order](#console-boot-order).

### Hooking into the boot
The exact rules are in [Contracts](contracts.md#stage-and-pipeline-contract).

Say you want to register something right after the modules are loaded, or measure how long the config takes to load. The boot stages dispatch events around themselves, such as `boot.modules.before` and `boot.modules.after`, with the context in the payload.
### Hooking into the running app

Listen in a file under your project's `helpers/` directory. Helpers load first, so the listener is in place before the other boot stages run:
Say you want to add a header to every response, or log which console command ran. The runtime stages dispatch events around themselves, such as `web.send.before` and `console.command.before`, with the runtime context in the payload.

Listen in a file under your project's `helpers/` directory. Helpers load early in the boot, so the listener is in place before the first request or command is handled:

```php
event()->listen('boot.modules.after', function (array $payload) {
$baseDir = $payload['context']->getBaseDir();
// helpers/events.php
event()->listen('web.send.before', function (array $payload) {
$payload['context']->getResponse()->setHeader('X-Served-By', 'Quantum');
});

event()->listen('console.command.before', function (array $payload) {
$command = $payload['context']->getInput()->getFirstArgument();

// the module routes are loaded at this point
// ...
});
```

The full list of events and when they are skipped is in [Contracts](contracts.md#stage-and-pipeline-contract).
A stage that does not run fires no events, so a listener never has to check whether the request was stopped. The send stage is the closing stage, so the header above is also added to a `404` and an `OPTIONS` response.

The boot stages dispatch no events: the boot is the framework's own setup, and app code hooks in once it is finished. The first event of a request is `web.preflight.before`, and the first event of a command is `console.command.before`.

The full list of events and the order they fire in is in [Contracts](contracts.md#lifecycle-events).

## Web boot order

The web adapter runs these stages during construction:
`boot()` runs these stages:

1. `LoadHelpersStage`
2. `LoadEnvironmentStage`
Expand All @@ -75,46 +86,56 @@ The web adapter runs these stages during construction:

## Web runtime order

After boot, `start()` runs these stages with a `WebContext`:
`start()` runs these stages with a `WebContext`:

1. `HandlePreflightStage`: `OPTIONS` requests get `204 No Content`, and the remaining stages up to sending are skipped
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 remaining stages up to sending are skipped
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
5. `DispatchRequestStage`: middleware wraps the route dispatch or the cached response lookup
6. `SendResponseStage`: appends the CORS headers, sends the response, and clears the request's route and input state, also when sending fails

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

## Console boot order

The console adapter runs these stages during construction:
`boot()` runs these stages:

1. `InitConsoleStage`
1. `InitConsoleStage`: registers the Symfony application, the input and the output
2. `LoadHelpersStage`
3. `LoadEnvironmentStage`
4. `LoadAppConfigStage`
5. `SetupErrorHandlerStage`
6. `SetupConsoleApplicationStage`
3. `RegisterCoreCommandsStage`: registers the framework's commands
4. `ResolveEnvironmentStage`: decides what happens when `.env` cannot be loaded
5. `LoadEnvironmentStage`
6. `LoadAppConfigStage`
7. `SetupErrorHandlerStage`
8. `SetupConsoleApplicationStage`: names the application from `app.name` and `app.version`
9. `RegisterAppCommandsStage`: registers your commands from `shared/Commands`

`ResolveEnvironmentStage` loads `.env` once to see whether it works. If it does not, the stage stops the boot for a command that can run without the environment, and lets every other command fail with the error; which commands those are is in [Commands without a working environment](../../cli/qt-command-line-tool.md#commands-without-a-working-environment).

`InitConsoleStage` runs first, so the console application, input and output exist even when a later stage fails. A failure does not stop the adapter: it is stored on the `ConsoleContext` and `RunCommandStage` decides per command. What the user sees is described in [When the boot fails](../../cli/qt-command-line-tool.md#when-the-boot-fails).
The framework's commands are registered before the environment so that `core:env` can be found when `.env` cannot be loaded; their constructors therefore use neither the environment nor the config. Your commands are registered last, after the environment, the config and the error handler, so their constructors can use `env()` and `config()`.

## Console runtime order

`start()` runs these stages with a `ConsoleContext`:
`start()` runs one stage with a `ConsoleContext`:

1. `RegisterCoreCommandsStage`
2. `RegisterAppCommandsStage`
3. `RunCommandStage`: rejects a missing or unknown command (``Command `<name>` is not defined``), re-throws the boot failure for a command that requires the environment, and otherwise runs the command and keeps its exit code
1. `RunCommandStage`: rejects a missing or unknown command (``Command `<name>` is not defined``), and otherwise runs the command and keeps its exit code

## Runtime contexts

`WebContext` and `ConsoleContext` extend `AppContext`, with the same base directory and container.

- `WebContext` reads the request, the routes, and the view cache from the container, and the matched route from the request. It stores what the run produces: the response to send and whether the run was stopped early.
- `ConsoleContext` reads the Symfony application, input, and output from the container. It stores the boot failure, if any, and the command's exit code.
- `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.

Both keep the stop flag and the exit code of their run, which they inherit from `AppContext`.

## Where the stages live

A stage that both adapters run is in `Quantum\App\Stages\Boot`: `LoadHelpersStage`, `LoadEnvironmentStage`, `LoadAppConfigStage` and `SetupErrorHandlerStage`. A stage that only one adapter runs lives next to that adapter, in `Quantum\App\Adapters\Web\Stages\Boot`, `...\Web\Stages\Runtime`, `...\Console\Stages\Boot` or `...\Console\Stages\Runtime`.

## Command discovery model

The console runtime registers commands from two places:
The console boot registers commands from two places:

- framework commands under `vendor/quantum/framework/src/Console/Commands` (namespace `Quantum\Console\Commands`)
- application commands under `shared/Commands` (namespace `Shared\Commands`)
Expand Down
Loading