Skip to content
Merged
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,18 @@ 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:** 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 `RequestPipeline` of the preflight, route resolution, not found, prepare, dispatch, and send stages with a `RequestContext`, and `ConsoleAppAdapter::start()` runs a `ConsolePipeline` 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, and runtime stage events are not dispatched yet.
- `WebAppAdapter::start()` now runs a `WebPipeline` of the preflight, route resolution, not found, prepare, dispatch, and send stages with a `WebContext`, and `ConsoleAppAdapter::start()` runs a `ConsolePipeline` 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, and runtime stage events are not dispatched yet.
- Moved the web adapter, context, and pipeline 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:** Replaced the `Quantum\Hook` package with `Quantum\Event` (#536):
Expand Down
3 changes: 1 addition & 2 deletions src/App/Adapters/Console/ConsoleAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
use Quantum\App\Stages\Boot\InitConsoleStage;
use Quantum\App\Stages\Boot\LoadHelpersStage;
use Quantum\App\Contracts\AppInterface;
use Quantum\App\Enums\ExitCode;
use Quantum\App\BootPipeline;
use Quantum\App\AppContext;
use Throwable;
Expand Down Expand Up @@ -79,6 +78,6 @@ public function start(): ?int
throw $this->bootFailure ?? $exception;
}

return $context->getExitCode() ?: ExitCode::SUCCESS;
return $context->getExitCode();
}
}
17 changes: 8 additions & 9 deletions src/App/Adapters/Console/ConsoleContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,38 +21,37 @@
* Class ConsoleContext
*
* Carries the state of a single console run through the console stages.
* Unlike AppContext, which is immutable, the exit code is set by the stages.
* The Symfony application, input and output are read from the container of the AppContext.
* It is an AppContext (same base directory and container) whose exit code is set by the stages.
* The Symfony application, input and output are read from the container.
*
* @package Quantum\App
*/
class ConsoleContext
class ConsoleContext extends AppContext
{
private AppContext $appContext;

private ?Throwable $bootFailure;

private int $exitCode = ExitCode::SUCCESS;

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

$this->bootFailure = $bootFailure;
}

public function getApplication(): Application
{
return $this->appContext->getContainer()->get(Application::class);
return $this->resolveFromContainer(Application::class);
}

public function getInput(): InputInterface
{
return $this->appContext->getContainer()->get(InputInterface::class);
return $this->resolveFromContainer(InputInterface::class);
}

public function getOutput(): OutputInterface
{
return $this->appContext->getContainer()->get(OutputInterface::class);
return $this->resolveFromContainer(OutputInterface::class);
}

/**
Expand Down
18 changes: 9 additions & 9 deletions src/App/Adapters/Web/WebAppAdapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@

namespace Quantum\App\Adapters\Web;

use Quantum\App\Stages\Request\HandleRouteNotFoundStage;
use Quantum\App\Stages\Request\HandlePreflightStage;
use Quantum\App\Stages\Request\DispatchRequestStage;
use Quantum\App\Stages\Request\PrepareRequestStage;
use Quantum\App\Stages\Request\SendResponseStage;
use Quantum\App\Stages\Request\ResolveRouteStage;
use Quantum\App\Stages\Web\HandleRouteNotFoundStage;
use Quantum\App\Stages\Boot\SetupErrorHandlerStage;
use Quantum\App\Stages\Boot\LoadEnvironmentStage;
use Quantum\App\Stages\Web\HandlePreflightStage;
use Quantum\App\Stages\Web\DispatchRequestStage;
use Quantum\App\Stages\Web\PrepareRequestStage;
use Quantum\App\Stages\Boot\LoadAppConfigStage;
use Quantum\App\Stages\Boot\InitDebuggerStage;
use Quantum\App\Contracts\AppInterface;
use Quantum\App\Stages\Web\SendResponseStage;
use Quantum\App\Stages\Web\ResolveRouteStage;
use Quantum\App\Stages\Boot\LoadModulesStage;
use Quantum\App\Stages\Boot\LoadHelpersStage;
use Quantum\App\Stages\Boot\InitHttpStage;
use Quantum\App\Contracts\AppInterface;
use Quantum\App\Enums\ExitCode;
use Quantum\App\BootPipeline;
use Quantum\App\AppContext;
Expand Down Expand Up @@ -55,7 +55,7 @@ public function __construct(AppContext $context)

public function start(): ?int
{
$pipeline = new RequestPipeline([
$pipeline = new WebPipeline([
new HandlePreflightStage(),
new ResolveRouteStage(),
new HandleRouteNotFoundStage(),
Expand All @@ -64,7 +64,7 @@ public function start(): ?int
new SendResponseStage(),
]);

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

return ExitCode::SUCCESS;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,48 +11,46 @@
namespace Quantum\App\Adapters\Web;

use Quantum\ResourceCache\ViewCache;
use Quantum\Router\RouteCollection;
use Quantum\Router\MatchedRoute;
use Quantum\App\AppContext;
use Quantum\Http\Response;
use Quantum\Http\Request;

/**
* Class RequestContext
* Class WebContext
*
* Carries the state of a single web request through the request stages.
* Unlike AppContext, which is immutable, this object is mutated by the stages.
* 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;
* 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.
*
* @package Quantum\App
*/
class RequestContext
class WebContext extends AppContext
{
private AppContext $appContext;

private ?MatchedRoute $matchedRoute = null;

private ?Response $response = null;

private ?ViewCache $viewCache = null;

private bool $stopped = false;

public function __construct(AppContext $appContext)
{
$this->appContext = $appContext;
parent::__construct($appContext->getBaseDir(), $appContext->getContainer());
}

public function getAppContext(): AppContext
public function getRequest(): Request
{
return $this->appContext;
return $this->resolveFromContainer(Request::class);
}

public function getMatchedRoute(): ?MatchedRoute
public function getRoutes(): RouteCollection
{
return $this->matchedRoute;
return $this->resolveFromContainer(RouteCollection::class);
}

public function setMatchedRoute(?MatchedRoute $matchedRoute): void
public function getMatchedRoute(): ?MatchedRoute
{
$this->matchedRoute = $matchedRoute;
return $this->getRequest()->getMatchedRoute();
}

public function getResponse(): ?Response
Expand All @@ -65,14 +63,9 @@ public function setResponse(Response $response): void
$this->response = $response;
}

public function getViewCache(): ?ViewCache
{
return $this->viewCache;
}

public function setViewCache(ViewCache $viewCache): void
public function getViewCache(): ViewCache
{
$this->viewCache = $viewCache;
return $this->resolveFromContainer(ViewCache::class);
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,42 @@

namespace Quantum\App\Adapters\Web;

use Quantum\App\Contracts\RequestStageInterface;
use Quantum\App\Contracts\WebStageInterface;
use InvalidArgumentException;

/**
* Class RequestPipeline
* Class WebPipeline
*
* Runs the web request stages in order. The pipeline does not skip stages by itself;
* a stage checks RequestContext::isStopped() when it must be skipped.
* Runs the web stages in order. The pipeline does not skip stages by itself;
* a stage checks WebContext::isStopped() when it must be skipped.
* Stage lifecycle events are not dispatched here yet.
*
* @package Quantum\App
*/
class RequestPipeline
class WebPipeline
{
/**
* @var RequestStageInterface[]
* @var WebStageInterface[]
*/
private array $stages;

/**
* @param RequestStageInterface[] $stages
* @param WebStageInterface[] $stages
*/
public function __construct(array $stages = [])
{
foreach ($stages as $stage) {
if (!$stage instanceof RequestStageInterface) {
if (!$stage instanceof WebStageInterface) {
throw new InvalidArgumentException(
'All stages must implement ' . RequestStageInterface::class
'All stages must implement ' . WebStageInterface::class
);
}
}

$this->stages = $stages;
}

public function run(RequestContext $context): void
public function run(WebContext $context): void
{
foreach ($this->stages as $stage) {
$stage->process($context);
Expand Down
20 changes: 1 addition & 19 deletions src/App/AppContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,8 @@
namespace Quantum\App;

use Quantum\Environment\Environment;
use Quantum\Router\RouteCollection;
use Quantum\Di\DiContainer;
use Quantum\Config\Config;
use Quantum\Http\Response;
use Quantum\Http\Request;

/**
* Class AppContext
Expand Down Expand Up @@ -53,27 +50,12 @@ public function getConfig(): Config
return $this->resolveFromContainer(Config::class);
}

public function getRequest(): Request
{
return $this->resolveFromContainer(Request::class);
}

public function getResponse(): Response
{
return $this->resolveFromContainer(Response::class);
}

public function getRoutes(): RouteCollection
{
return $this->resolveFromContainer(RouteCollection::class);
}

/**
* @template T of object
* @param class-string<T> $class
* @return T
*/
private function resolveFromContainer(string $class)
protected function resolveFromContainer(string $class)
{
return $this->container->get($class);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@

namespace Quantum\App\Contracts;

use Quantum\App\Adapters\Web\RequestContext;
use Quantum\App\Adapters\Web\WebContext;

/**
* Interface RequestStageInterface
* Interface WebStageInterface
* @package Quantum\App
*/
interface RequestStageInterface
interface WebStageInterface
{
/**
* Processes a single request stage
* Processes a single web stage
*/
public function process(RequestContext $context): void;
public function process(WebContext $context): void;
}
46 changes: 0 additions & 46 deletions src/App/Stages/Request/ResolveRouteStage.php

This file was deleted.

Loading
Loading