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
28 changes: 10 additions & 18 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,16 @@ 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:** Replaced the boot, web and console pipelines with one generic `Pipeline` (#583):
- `Pipeline<TContext>` runs `StageInterface<TContext>` stages in order and dispatches the `BEFORE`/`AFTER` events they declare. The context type is a PHPDoc template checked by PHPStan: a stage states it with `@implements StageInterface<AppContext>` (or `WebContext`, `ConsoleContext`) and takes `process(AppContext $context)`.
- Removed `BootPipeline` and `BootStageInterface`: build a `new Pipeline([...])`, and implement `StageInterface` with `@implements StageInterface<AppContext>` in a boot stage. `WebPipeline`, `ConsolePipeline`, `WebStageInterface` and `ConsoleStageInterface`, added earlier in 3.1.0, are removed too.
- A pipeline still rejects anything that is not a `StageInterface`, but no longer rejects a stage of another kind at runtime (a boot stage in a web pipeline); PHPStan reports it instead.
- **BREAKING:** The web and console contexts now extend `AppContext` (#579):
- `AppContext` keeps only what both the web and the console boot provide: `getBaseDir()`, `getContainer()`, `getEnvironment()` and `getConfig()`. Removed `AppContext::getRequest()`, `getResponse()` and `getRoutes()`, which only the web boot can serve: use `WebContext::getRequest()` and `WebContext::getRoutes()`, and `response()` for the shared response.
- `WebContext` and `ConsoleContext` extend `AppContext` and are built from it with the same base directory and container; `WebContext::getAppContext()` is removed. Like `ConsoleContext`, `WebContext` reads its services from the container (the request, the routes and the view cache) and the matched route from the request, and stores only what the run produces: the response that is sent and the stopped flag. `WebContext::setMatchedRoute()` and `setViewCache()` are removed.
- The web stages read the request, the routes and the config from the context instead of the global `request()` and `config()` helpers, as the console stages do. They are the same objects from the same container, so behavior is unchanged.
- **BREAKING:** The console adapter constructor is now only the boot pipeline, like the web adapter (#576):
- The Symfony `Application`, the input and the output are provided by the new `InitConsoleStage` through the container, and the new `SetupConsoleApplicationStage` names the application from `app.name` and `app.version` once the whole boot has succeeded (it stays `UNKNOWN` after a failed boot). `ConsoleContext` is built from the `AppContext` and the boot failure, and reads those objects from the container. Removed `ConsoleAppAdapter::createApplication()` (get the application with `Di::get(Application::class)`) and `ConsoleContext::getAppContext()`.
- The adapter no longer makes the environment mutable for every command. `core:key` and the toolkit installer already do it themselves; a command outside the framework that calls `environment()->updateRow()` must call `setMutable(true)` first, or it gets `EnvException::environmentImmutable()`.
- The console boot no longer decides by the `core:env` command name (#575):
- `ConsoleAppAdapter` always runs the full boot and, when it fails, keeps the failure instead of stopping. `RunCommandStage` re-throws that original failure only for a command that requires the environment, so `core:env`, `list`, and `help` still run and `core:env --yes` can repair a missing, empty, or malformed `.env`. The reduced boot for `core:env` is gone, and the Symfony `Application` is named `UNKNOWN` until a successful boot names it from the app config.
- Added `CliCommand::$requiresEnvironment` (default `true`) with `CliCommand::requiresEnvironment()`; a command that works without the environment, like `EnvCommand`, sets it to `false`. `ConsoleContext` takes an optional boot failure and exposes it with `getBootFailure()`.
- **BREAKING:** Refactored the web and console runtime into staged pipelines (#537):
- `WebAppAdapter::start()` now runs a `Pipeline` of the preflight, route resolution, not found, prepare, dispatch, and send stages with a `WebContext`, and `ConsoleAppAdapter::start()` runs a `Pipeline` of the register core commands, register app commands, and run stages with a `ConsoleContext`; the run stage rejects a missing or unknown command before running anything, and registration goes through the new `Quantum\Console\CommandRegistrar`. Behavior is unchanged. The pipeline dispatches a stage's `BEFORE`/`AFTER` events when the stage declares them, but the built-in web and console stages declare none yet.
- Moved the web adapter and context to `Quantum\App\Adapters\Web`, the console ones to `Quantum\App\Adapters\Console`, and the boot stages to `Quantum\App\Stages\Boot`. Code that goes through `AppFactory` is unaffected; code that references these classes directly must update its imports.
- Removed the `AppAdapter` base class and the `WebAppTrait` and `ConsoleAppTrait` traits. `WebAppAdapter` and `ConsoleAppAdapter` now implement `AppInterface` directly.
- **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #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 boot stages moved to `Quantum\App\Stages\Boot`. Code that goes through `AppFactory` is unaffected; code that references these classes directly must update its imports.
- Replaced `BootPipeline` and `BootStageInterface` with one generic `Quantum\App\Pipeline<TContext>`, which runs `StageInterface<TContext>` stages in order and dispatches the `BEFORE`/`AFTER` events they declare. The context type is a PHPDoc template checked by PHPStan: a stage states it with `@implements StageInterface<AppContext>` (or `WebContext`, `ConsoleContext`) and takes `process(AppContext $context)`. A stage that calls `stop()` on the context ends the run; an optional closing stage, the second constructor argument, runs last, also after a stop, but not after a stage throws.
- `AppContext` keeps `getBaseDir()`, `getContainer()`, `getEnvironment()` and `getConfig()`, and carries the state of a run: `stop()`, `isStopped()`, `getExitCode()` and `setExitCode()`. Removed `AppContext::getRequest()`, `getResponse()` and `getRoutes()`: use `WebContext::getRequest()` and `WebContext::getRoutes()`, and `response()` for the shared response.
- The new `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 stores the response to send; `ConsoleContext` reads the Symfony `Application`, the input and the output from the container.
- The web runtime runs the preflight, route resolution, not found, prepare and dispatch stages, with `SendResponseStage` as its closing stage. Behavior is unchanged.
- The console boot registers the Symfony `Application`, the input and the output in the container (`InitConsoleStage`), loads the helpers, registers the core commands, loads the environment, the config and the error handler, names the application from `app.name` and `app.version` (`UNKNOWN` without them), and registers the app commands last, so their constructors can use `env()` and `config()`. The console runtime runs the command and rejects a missing or unknown one. Registration goes through the new `Quantum\Console\CommandRegistrar`. Removed `ConsoleAppAdapter::createApplication()`: get the application with `Di::get(Application::class)`.
- The console no longer decides by the `core:env` command name. Added `CliCommand::$requiresEnvironment` (default `true`) with `CliCommand::requiresEnvironment()`. When the `.env` file is missing or malformed, the new `ResolveEnvironmentStage` stops the boot for a core command that sets it to `false` (`core:env`) and for Symfony's `list` and `help`, so they still run; every other command fails with the exception of the environment file. With a valid `.env`, `core:env` now runs the full boot. A core command constructor must not use the environment or any config, since the core commands are registered before the environment is loaded.
- The console no longer makes the environment mutable for every command. `core:key` and the toolkit installer already do it themselves; a command outside the framework that calls `environment()->updateRow()` must call `setMutable(true)` first, or it gets `EnvException::environmentImmutable()`.
- **BREAKING:** Replaced the `Quantum\Hook` package with `Quantum\Event` (#536):
- `hook()` is now `event()`; `on()` and `fire()` are now `listen()` and `dispatch()`, and no aliases remain. Listeners receive one named payload array, which is empty when none is given.
- Event names no longer need to be registered, so `shared/config/hooks.php` is no longer read; dispatching a name without listeners does nothing and an empty name throws `EventException`.
Expand Down
50 changes: 22 additions & 28 deletions src/App/Adapters/Console/ConsoleAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@
namespace Quantum\App\Adapters\Console;

use Quantum\App\Stages\Boot\SetupConsoleApplicationStage;
use Quantum\App\Stages\Console\RegisterCoreCommandsStage;
use Quantum\App\Stages\Console\RegisterAppCommandsStage;
use Quantum\App\Stages\Boot\RegisterCoreCommandsStage;
use Quantum\App\Stages\Boot\RegisterAppCommandsStage;
use Quantum\App\Stages\Boot\ResolveEnvironmentStage;
use Quantum\App\Stages\Boot\SetupErrorHandlerStage;
use Quantum\App\Stages\Boot\LoadEnvironmentStage;
use Quantum\App\Stages\Console\RunCommandStage;
Expand All @@ -22,7 +23,6 @@
use Quantum\App\Contracts\AppInterface;
use Quantum\App\AppContext;
use Quantum\App\Pipeline;
use Throwable;
use Exception;

if (!defined('DS')) {
Expand All @@ -37,46 +37,40 @@ class ConsoleAppAdapter implements AppInterface
{
protected AppContext $context;

protected ?Throwable $bootFailure = null;

public function __construct(AppContext $context)
{
$this->context = $context;
}

try {
$pipeline = new Pipeline([
new InitConsoleStage(),
new LoadHelpersStage(),
new LoadEnvironmentStage(),
new LoadAppConfigStage(),
new SetupErrorHandlerStage(),
new SetupConsoleApplicationStage(),
]);
public function boot(): void
{
$pipeline = new Pipeline([
new InitConsoleStage(),
new LoadHelpersStage(),
new RegisterCoreCommandsStage(),
new ResolveEnvironmentStage(),
new LoadEnvironmentStage(),
new LoadAppConfigStage(),
new SetupErrorHandlerStage(),
new SetupConsoleApplicationStage(),
new RegisterAppCommandsStage(),
]);

$pipeline->run($this->context);
} catch (Throwable $exception) {
$this->bootFailure = $exception;
}
$pipeline->run($this->context);
}

/**
* @throws Exception|Throwable
*/
* @throws Exception
*/
public function start(): ?int
{
$pipeline = new Pipeline([
new RegisterCoreCommandsStage(),
new RegisterAppCommandsStage(),
new RunCommandStage(),
]);

$context = new ConsoleContext($this->context, $this->bootFailure);
$context = new ConsoleContext($this->context);

try {
$pipeline->run($context);
} catch (Throwable $exception) {
throw $this->bootFailure ?? $exception;
}
$pipeline->run($context);

return $context->getExitCode();
}
Expand Down
28 changes: 1 addition & 27 deletions src/App/Adapters/Console/ConsoleContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Application;
use Quantum\App\Enums\ExitCode;
use Quantum\App\AppContext;
use Throwable;

/**
* Class ConsoleContext
Expand All @@ -28,15 +26,9 @@
*/
class ConsoleContext extends AppContext
{
private ?Throwable $bootFailure;

private int $exitCode = ExitCode::SUCCESS;

public function __construct(AppContext $appContext, ?Throwable $bootFailure = null)
public function __construct(AppContext $appContext)
{
parent::__construct($appContext->getBaseDir(), $appContext->getContainer());

$this->bootFailure = $bootFailure;
}

public function getApplication(): Application
Expand All @@ -53,22 +45,4 @@ public function getOutput(): OutputInterface
{
return $this->resolveFromContainer(OutputInterface::class);
}

/**
* The failure of the boot that the console run continued after, if there was one
*/
public function getBootFailure(): ?Throwable
{
return $this->bootFailure;
}

public function getExitCode(): int
{
return $this->exitCode;
}

public function setExitCode(int $exitCode): void
{
$this->exitCode = $exitCode;
}
}
6 changes: 4 additions & 2 deletions src/App/Adapters/Web/WebAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ class WebAppAdapter implements AppInterface
public function __construct(AppContext $context)
{
$this->context = $context;
}

public function boot(): void
{
$pipeline = new Pipeline([
new LoadHelpersStage(),
new LoadEnvironmentStage(),
Expand All @@ -61,8 +64,7 @@ public function start(): ?int
new HandleRouteNotFoundStage(),
new PrepareRequestStage(),
new DispatchRequestStage(),
new SendResponseStage(),
]);
], new SendResponseStage());

$pipeline->run(new WebContext($this->context));

Expand Down
18 changes: 1 addition & 17 deletions src/App/Adapters/Web/WebContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
* Class WebContext
*
* Carries the state of a single web request through the web stages.
* It is an AppContext (same base directory and container) whose response and stopped flag are set by the stages;
* It is an AppContext (same base directory and container) whose response is set by the stages;
* the response the stages set is the one that is sent.
* The request, the routes and the view cache are read from the container, and the matched route from the request.
*
Expand All @@ -31,8 +31,6 @@ class WebContext extends AppContext
{
private ?Response $response = null;

private bool $stopped = false;

public function __construct(AppContext $appContext)
{
parent::__construct($appContext->getBaseDir(), $appContext->getContainer());
Expand Down Expand Up @@ -67,18 +65,4 @@ public function getViewCache(): ViewCache
{
return $this->resolveFromContainer(ViewCache::class);
}

/**
* Marks the request as finished early, so stages that compute a response skip their work.
* The stage that sends the response still runs.
*/
public function stop(): void
{
$this->stopped = true;
}

public function isStopped(): bool
{
return $this->stopped;
}
}
32 changes: 32 additions & 0 deletions src/App/AppContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,16 @@
namespace Quantum\App;

use Quantum\Environment\Environment;
use Quantum\App\Enums\ExitCode;
use Quantum\Di\DiContainer;
use Quantum\Config\Config;

/**
* Class AppContext
*
* Carries the base directory and the container of an application, and the state of the run it is passed through:
* the stopped flag, which ends the pipeline run early, and the exit code.
*
* @package Quantum\App
*/
class AppContext
Expand All @@ -24,6 +29,10 @@ class AppContext

private DiContainer $container;

private bool $stopped = false;

private int $exitCode = ExitCode::SUCCESS;

public function __construct(string $baseDir, DiContainer $container)
{
$this->baseDir = $baseDir;
Expand All @@ -50,6 +59,29 @@ public function getConfig(): Config
return $this->resolveFromContainer(Config::class);
}

/**
* Ends the pipeline run after the current stage; only the closing stage still runs
*/
public function stop(): void
{
$this->stopped = true;
}

public function isStopped(): bool
{
return $this->stopped;
}

public function getExitCode(): int
{
return $this->exitCode;
}

public function setExitCode(int $exitCode): void
{
$this->exitCode = $exitCode;
}

/**
* @template T of object
* @param class-string<T> $class
Expand Down
5 changes: 5 additions & 0 deletions src/App/Contracts/AppInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@
*/
interface AppInterface
{
/**
* Boots the app
*/
public function boot(): void;

/**
* Starts the app
*/
Expand Down
5 changes: 4 additions & 1 deletion src/App/Factories/AppFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ private static function createInstance(string $type, string $baseDir): App

$adapterClass = self::ADAPTERS[$type];

return new App(new $adapterClass($context));
$adapter = new $adapterClass($context);
$adapter->boot();

return new App($adapter);
}
}
34 changes: 30 additions & 4 deletions src/App/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
*
* Runs stages in order with a context. The event a stage declares in its BEFORE / AFTER
* constants is dispatched around it, with the context as the payload.
* A stage that stops the context ends the run: no later stage runs or dispatches events.
* The optional closing stage runs last, also after a stop, but not after a stage throws.
*
* @template TContext of AppContext
* @package Quantum\App
Expand All @@ -29,10 +31,16 @@ final class Pipeline
*/
private array $stages;

/**
* @var StageInterface<TContext>|null
*/
private ?StageInterface $closingStage;

/**
* @param array<StageInterface<TContext>> $stages
* @param StageInterface<TContext>|null $closingStage
*/
public function __construct(array $stages = [])
public function __construct(array $stages = [], ?StageInterface $closingStage = null)
{
foreach ($stages as $stage) {
if (!$stage instanceof StageInterface) {
Expand All @@ -41,6 +49,7 @@ public function __construct(array $stages = [])
}

$this->stages = $stages;
$this->closingStage = $closingStage;
}

/**
Expand All @@ -49,14 +58,31 @@ public function __construct(array $stages = [])
public function run(AppContext $context): void
{
foreach ($this->stages as $stage) {
$this->dispatchStageEvent($stage, 'BEFORE', $context);
$this->runStage($stage, $context);

$stage->process($context);
if ($context->isStopped()) {
break;
}
}

$this->dispatchStageEvent($stage, 'AFTER', $context);
if ($this->closingStage !== null) {
$this->runStage($this->closingStage, $context);
}
}

/**
* @param StageInterface<TContext> $stage
* @param TContext $context
*/
private function runStage(StageInterface $stage, AppContext $context): void
{
$this->dispatchStageEvent($stage, 'BEFORE', $context);

$stage->process($context);

$this->dispatchStageEvent($stage, 'AFTER', $context);
}

/**
* Dispatches the event a stage declares for the given point, if any.
* Skipped while the event() helper is not loaded yet, since nothing can listen then.
Expand Down
4 changes: 2 additions & 2 deletions src/App/Stages/Boot/InitConsoleStage.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
/**
* Class InitConsoleStage
*
* Runs before the helpers are loaded, so that the console application, input and output exist
* even when a helper file fails; for the same reason it declares no boot events.
* Runs first in the console boot, before the helpers are loaded, so it declares no boot events.
* The core command registration and ResolveEnvironmentStage read the application and the input it registers.
*
* @implements StageInterface<AppContext>
* @package Quantum\App
Expand Down
Loading
Loading