From 6732af72dede657b6154b1741e53fff47489e3b5 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:33:27 +0400 Subject: [PATCH 1/5] [#538] Move the web stages under the web adapter The web boot stages that only the web adapter runs move to Adapters/Web/Stages/Boot and the web runtime stages to Adapters/Web/Stages/Runtime, with their tests. Only namespaces and imports change. --- .../Web}/Stages/Boot/InitDebuggerStage.php | 2 +- .../Web}/Stages/Boot/InitHttpStage.php | 2 +- .../Web}/Stages/Boot/LoadModulesStage.php | 2 +- .../Stages/Runtime}/DispatchRequestStage.php | 2 +- .../Stages/Runtime}/HandlePreflightStage.php | 2 +- .../Runtime}/HandleRouteNotFoundStage.php | 2 +- .../Stages/Runtime}/PrepareRequestStage.php | 2 +- .../Web/Stages/Runtime}/ResolveRouteStage.php | 2 +- .../Web/Stages/Runtime}/SendResponseStage.php | 2 +- src/App/Adapters/Web/WebAppAdapter.php | 18 +++++++++--------- .../Runtime}/DispatchRequestStageTest.php | 6 +++--- .../Runtime}/HandlePreflightStageTest.php | 4 ++-- .../Runtime}/HandleRouteNotFoundStageTest.php | 4 ++-- .../Runtime}/PrepareRequestStageTest.php | 4 ++-- .../Stages/Runtime}/ResolveRouteStageTest.php | 4 ++-- .../Stages/Runtime}/SendResponseStageTest.php | 4 ++-- tests/Unit/App/PipelineTest.php | 6 +++--- 17 files changed, 34 insertions(+), 34 deletions(-) rename src/App/{ => Adapters/Web}/Stages/Boot/InitDebuggerStage.php (90%) rename src/App/{ => Adapters/Web}/Stages/Boot/InitHttpStage.php (94%) rename src/App/{ => Adapters/Web}/Stages/Boot/LoadModulesStage.php (96%) rename src/App/{Stages/Web => Adapters/Web/Stages/Runtime}/DispatchRequestStage.php (96%) rename src/App/{Stages/Web => Adapters/Web/Stages/Runtime}/HandlePreflightStage.php (94%) rename src/App/{Stages/Web => Adapters/Web/Stages/Runtime}/HandleRouteNotFoundStage.php (95%) rename src/App/{Stages/Web => Adapters/Web/Stages/Runtime}/PrepareRequestStage.php (97%) rename src/App/{Stages/Web => Adapters/Web/Stages/Runtime}/ResolveRouteStage.php (95%) rename src/App/{Stages/Web => Adapters/Web/Stages/Runtime}/SendResponseStage.php (97%) rename tests/Unit/App/{Stages/Web => Adapters/Web/Stages/Runtime}/DispatchRequestStageTest.php (97%) rename tests/Unit/App/{Stages/Web => Adapters/Web/Stages/Runtime}/HandlePreflightStageTest.php (93%) rename tests/Unit/App/{Stages/Web => Adapters/Web/Stages/Runtime}/HandleRouteNotFoundStageTest.php (96%) rename tests/Unit/App/{Stages/Web => Adapters/Web/Stages/Runtime}/PrepareRequestStageTest.php (97%) rename tests/Unit/App/{Stages/Web => Adapters/Web/Stages/Runtime}/ResolveRouteStageTest.php (95%) rename tests/Unit/App/{Stages/Web => Adapters/Web/Stages/Runtime}/SendResponseStageTest.php (97%) diff --git a/src/App/Stages/Boot/InitDebuggerStage.php b/src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php similarity index 90% rename from src/App/Stages/Boot/InitDebuggerStage.php rename to src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php index 3da72583..3c84e6f6 100644 --- a/src/App/Stages/Boot/InitDebuggerStage.php +++ b/src/App/Adapters/Web/Stages/Boot/InitDebuggerStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Web\Stages\Boot; use Quantum\App\Contracts\StageInterface; use Quantum\App\AppContext; diff --git a/src/App/Stages/Boot/InitHttpStage.php b/src/App/Adapters/Web/Stages/Boot/InitHttpStage.php similarity index 94% rename from src/App/Stages/Boot/InitHttpStage.php rename to src/App/Adapters/Web/Stages/Boot/InitHttpStage.php index 8eebd363..b38f4b21 100644 --- a/src/App/Stages/Boot/InitHttpStage.php +++ b/src/App/Adapters/Web/Stages/Boot/InitHttpStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Web\Stages\Boot; use Quantum\App\Contracts\StageInterface; use Quantum\App\AppContext; diff --git a/src/App/Stages/Boot/LoadModulesStage.php b/src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php similarity index 96% rename from src/App/Stages/Boot/LoadModulesStage.php rename to src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php index 6d2bb86a..094eeb22 100644 --- a/src/App/Stages/Boot/LoadModulesStage.php +++ b/src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Web\Stages\Boot; use Quantum\Module\Exceptions\ModuleException; use Quantum\Router\Exceptions\RouteException; diff --git a/src/App/Stages/Web/DispatchRequestStage.php b/src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php similarity index 96% rename from src/App/Stages/Web/DispatchRequestStage.php rename to src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php index 9c3303aa..6312f9e5 100644 --- a/src/App/Stages/Web/DispatchRequestStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Web; +namespace Quantum\App\Adapters\Web\Stages\Runtime; use Quantum\Middleware\Exceptions\MiddlewareException; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Stages/Web/HandlePreflightStage.php b/src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php similarity index 94% rename from src/App/Stages/Web/HandlePreflightStage.php rename to src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php index ccf00da7..87955f6d 100644 --- a/src/App/Stages/Web/HandlePreflightStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Web; +namespace Quantum\App\Adapters\Web\Stages\Runtime; use Quantum\App\Contracts\StageInterface; use Quantum\App\Adapters\Web\WebContext; diff --git a/src/App/Stages/Web/HandleRouteNotFoundStage.php b/src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php similarity index 95% rename from src/App/Stages/Web/HandleRouteNotFoundStage.php rename to src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php index f7d27c8a..928fe516 100644 --- a/src/App/Stages/Web/HandleRouteNotFoundStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Web; +namespace Quantum\App\Adapters\Web\Stages\Runtime; use Quantum\Config\Exceptions\ConfigException; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Stages/Web/PrepareRequestStage.php b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php similarity index 97% rename from src/App/Stages/Web/PrepareRequestStage.php rename to src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php index f19989ca..41660931 100644 --- a/src/App/Stages/Web/PrepareRequestStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Web; +namespace Quantum\App\Adapters\Web\Stages\Runtime; use Quantum\Config\Exceptions\ConfigException; use Quantum\Lang\Exceptions\LangException; diff --git a/src/App/Stages/Web/ResolveRouteStage.php b/src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php similarity index 95% rename from src/App/Stages/Web/ResolveRouteStage.php rename to src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php index a261877c..a9cc3922 100644 --- a/src/App/Stages/Web/ResolveRouteStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Web; +namespace Quantum\App\Adapters\Web\Stages\Runtime; use Quantum\Router\Exceptions\RouteException; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Stages/Web/SendResponseStage.php b/src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php similarity index 97% rename from src/App/Stages/Web/SendResponseStage.php rename to src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php index 66c06d6c..5f362d89 100644 --- a/src/App/Stages/Web/SendResponseStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Web; +namespace Quantum\App\Adapters\Web\Stages\Runtime; use Quantum\Config\Exceptions\ConfigException; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Adapters/Web/WebAppAdapter.php b/src/App/Adapters/Web/WebAppAdapter.php index ee94c4ba..747c7191 100644 --- a/src/App/Adapters/Web/WebAppAdapter.php +++ b/src/App/Adapters/Web/WebAppAdapter.php @@ -10,19 +10,19 @@ namespace Quantum\App\Adapters\Web; -use Quantum\App\Stages\Web\HandleRouteNotFoundStage; +use Quantum\App\Adapters\Web\Stages\Runtime\HandleRouteNotFoundStage; +use Quantum\App\Adapters\Web\Stages\Runtime\HandlePreflightStage; +use Quantum\App\Adapters\Web\Stages\Runtime\DispatchRequestStage; +use Quantum\App\Adapters\Web\Stages\Runtime\PrepareRequestStage; +use Quantum\App\Adapters\Web\Stages\Runtime\SendResponseStage; +use Quantum\App\Adapters\Web\Stages\Runtime\ResolveRouteStage; +use Quantum\App\Adapters\Web\Stages\Boot\InitDebuggerStage; +use Quantum\App\Adapters\Web\Stages\Boot\LoadModulesStage; +use Quantum\App\Adapters\Web\Stages\Boot\InitHttpStage; 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\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\AppContext; diff --git a/tests/Unit/App/Stages/Web/DispatchRequestStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php similarity index 97% rename from tests/Unit/App/Stages/Web/DispatchRequestStageTest.php rename to tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php index 55a50775..5ef18980 100644 --- a/tests/Unit/App/Stages/Web/DispatchRequestStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/DispatchRequestStageTest.php @@ -39,11 +39,11 @@ public function apply(Request $request, Closure $next): Response } } -namespace Quantum\Tests\Unit\App\Stages\Web { +namespace Quantum\Tests\Unit\App\Adapters\Web\Stages\Runtime { use Quantum\Tests\_root\modules\Test\Middlewares\DispatchStageMwPassThrough; use Quantum\Tests\_root\modules\Test\Middlewares\DispatchStageMwRecorder; - use Quantum\App\Stages\Web\DispatchRequestStage; - use Quantum\App\Stages\Web\ResolveRouteStage; + use Quantum\App\Adapters\Web\Stages\Runtime\DispatchRequestStage; + use Quantum\App\Adapters\Web\Stages\Runtime\ResolveRouteStage; use Quantum\Router\Exceptions\RouteException; use Quantum\Di\Exceptions\DiException; use Quantum\Tests\Unit\AppTestCase; diff --git a/tests/Unit/App/Stages/Web/HandlePreflightStageTest.php b/tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php similarity index 93% rename from tests/Unit/App/Stages/Web/HandlePreflightStageTest.php rename to tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php index 8c20e443..451d297b 100644 --- a/tests/Unit/App/Stages/Web/HandlePreflightStageTest.php +++ b/tests/Unit/App/Adapters/Web/Stages/Runtime/HandlePreflightStageTest.php @@ -1,8 +1,8 @@ Date: Wed, 21 Oct 2026 14:40:39 +0400 Subject: [PATCH 2/5] [#538] Move the console stages under the console adapter The console boot stages move to Adapters/Console/Stages/Boot and RunCommandStage to Adapters/Console/Stages/Runtime, with their tests. src/App/Stages/Boot keeps only the stages both adapters run. Only namespaces and imports change. --- src/App/Adapters/Console/ConsoleAppAdapter.php | 12 ++++++------ .../Console}/Stages/Boot/InitConsoleStage.php | 2 +- .../Stages/Boot/RegisterAppCommandsStage.php | 2 +- .../Stages/Boot/RegisterCoreCommandsStage.php | 2 +- .../Console}/Stages/Boot/ResolveEnvironmentStage.php | 2 +- .../Stages/Boot/SetupConsoleApplicationStage.php | 2 +- .../Console/Stages/Runtime}/RunCommandStage.php | 2 +- .../Console}/Stages/Boot/InitConsoleStageTest.php | 4 ++-- .../Stages/Boot/RegisterAppCommandsStageTest.php | 6 +++--- .../Stages/Boot/RegisterCoreCommandsStageTest.php | 6 +++--- .../Stages/Boot/ResolveEnvironmentStageTest.php | 6 +++--- .../Stages/Boot/SetupConsoleApplicationStageTest.php | 4 ++-- .../Console/Stages}/ConsoleStageTestCase.php | 2 +- .../Console/Stages/Runtime}/RunCommandStageTest.php | 5 +++-- tests/Unit/App/PipelineTest.php | 4 ++-- 15 files changed, 31 insertions(+), 30 deletions(-) rename src/App/{ => Adapters/Console}/Stages/Boot/InitConsoleStage.php (96%) rename src/App/{ => Adapters/Console}/Stages/Boot/RegisterAppCommandsStage.php (95%) rename src/App/{ => Adapters/Console}/Stages/Boot/RegisterCoreCommandsStage.php (96%) rename src/App/{ => Adapters/Console}/Stages/Boot/ResolveEnvironmentStage.php (97%) rename src/App/{ => Adapters/Console}/Stages/Boot/SetupConsoleApplicationStage.php (93%) rename src/App/{Stages/Console => Adapters/Console/Stages/Runtime}/RunCommandStage.php (95%) rename tests/Unit/App/{ => Adapters/Console}/Stages/Boot/InitConsoleStageTest.php (95%) rename tests/Unit/App/{ => Adapters/Console}/Stages/Boot/RegisterAppCommandsStageTest.php (84%) rename tests/Unit/App/{ => Adapters/Console}/Stages/Boot/RegisterCoreCommandsStageTest.php (84%) rename tests/Unit/App/{ => Adapters/Console}/Stages/Boot/ResolveEnvironmentStageTest.php (96%) rename tests/Unit/App/{ => Adapters/Console}/Stages/Boot/SetupConsoleApplicationStageTest.php (94%) rename tests/Unit/App/{Stages/Console => Adapters/Console/Stages}/ConsoleStageTestCase.php (97%) rename tests/Unit/App/{Stages/Console => Adapters/Console/Stages/Runtime}/RunCommandStageTest.php (95%) diff --git a/src/App/Adapters/Console/ConsoleAppAdapter.php b/src/App/Adapters/Console/ConsoleAppAdapter.php index 9ca781b6..40fb4ab7 100644 --- a/src/App/Adapters/Console/ConsoleAppAdapter.php +++ b/src/App/Adapters/Console/ConsoleAppAdapter.php @@ -10,15 +10,15 @@ namespace Quantum\App\Adapters\Console; -use Quantum\App\Stages\Boot\SetupConsoleApplicationStage; -use Quantum\App\Stages\Boot\RegisterCoreCommandsStage; -use Quantum\App\Stages\Boot\RegisterAppCommandsStage; -use Quantum\App\Stages\Boot\ResolveEnvironmentStage; +use Quantum\App\Adapters\Console\Stages\Boot\SetupConsoleApplicationStage; +use Quantum\App\Adapters\Console\Stages\Boot\RegisterCoreCommandsStage; +use Quantum\App\Adapters\Console\Stages\Boot\RegisterAppCommandsStage; +use Quantum\App\Adapters\Console\Stages\Boot\ResolveEnvironmentStage; +use Quantum\App\Adapters\Console\Stages\Runtime\RunCommandStage; +use Quantum\App\Adapters\Console\Stages\Boot\InitConsoleStage; use Quantum\App\Stages\Boot\SetupErrorHandlerStage; use Quantum\App\Stages\Boot\LoadEnvironmentStage; -use Quantum\App\Stages\Console\RunCommandStage; use Quantum\App\Stages\Boot\LoadAppConfigStage; -use Quantum\App\Stages\Boot\InitConsoleStage; use Quantum\App\Stages\Boot\LoadHelpersStage; use Quantum\App\Contracts\AppInterface; use Quantum\App\AppContext; diff --git a/src/App/Stages/Boot/InitConsoleStage.php b/src/App/Adapters/Console/Stages/Boot/InitConsoleStage.php similarity index 96% rename from src/App/Stages/Boot/InitConsoleStage.php rename to src/App/Adapters/Console/Stages/Boot/InitConsoleStage.php index 4c026a4c..78b93738 100644 --- a/src/App/Stages/Boot/InitConsoleStage.php +++ b/src/App/Adapters/Console/Stages/Boot/InitConsoleStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Console\Stages\Boot; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Output\ConsoleOutput; diff --git a/src/App/Stages/Boot/RegisterAppCommandsStage.php b/src/App/Adapters/Console/Stages/Boot/RegisterAppCommandsStage.php similarity index 95% rename from src/App/Stages/Boot/RegisterAppCommandsStage.php rename to src/App/Adapters/Console/Stages/Boot/RegisterAppCommandsStage.php index f9e6efc2..12c1fbb1 100644 --- a/src/App/Stages/Boot/RegisterAppCommandsStage.php +++ b/src/App/Adapters/Console/Stages/Boot/RegisterAppCommandsStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Console\Stages\Boot; use Symfony\Component\Console\Application; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Stages/Boot/RegisterCoreCommandsStage.php b/src/App/Adapters/Console/Stages/Boot/RegisterCoreCommandsStage.php similarity index 96% rename from src/App/Stages/Boot/RegisterCoreCommandsStage.php rename to src/App/Adapters/Console/Stages/Boot/RegisterCoreCommandsStage.php index ce2b047f..7399fac7 100644 --- a/src/App/Stages/Boot/RegisterCoreCommandsStage.php +++ b/src/App/Adapters/Console/Stages/Boot/RegisterCoreCommandsStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Console\Stages\Boot; use Symfony\Component\Console\Application; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Stages/Boot/ResolveEnvironmentStage.php b/src/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStage.php similarity index 97% rename from src/App/Stages/Boot/ResolveEnvironmentStage.php rename to src/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStage.php index 7a286ee5..bde36151 100644 --- a/src/App/Stages/Boot/ResolveEnvironmentStage.php +++ b/src/App/Adapters/Console/Stages/Boot/ResolveEnvironmentStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Console\Stages\Boot; use Symfony\Component\Console\Input\InputInterface; use Quantum\Environment\Exceptions\EnvException; diff --git a/src/App/Stages/Boot/SetupConsoleApplicationStage.php b/src/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStage.php similarity index 93% rename from src/App/Stages/Boot/SetupConsoleApplicationStage.php rename to src/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStage.php index 6a566f74..2dff6831 100644 --- a/src/App/Stages/Boot/SetupConsoleApplicationStage.php +++ b/src/App/Adapters/Console/Stages/Boot/SetupConsoleApplicationStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Boot; +namespace Quantum\App\Adapters\Console\Stages\Boot; use Symfony\Component\Console\Application; use Quantum\App\Contracts\StageInterface; diff --git a/src/App/Stages/Console/RunCommandStage.php b/src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php similarity index 95% rename from src/App/Stages/Console/RunCommandStage.php rename to src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php index 3904f278..1e8e002e 100644 --- a/src/App/Stages/Console/RunCommandStage.php +++ b/src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php @@ -8,7 +8,7 @@ * @link https://quantumphp.io */ -namespace Quantum\App\Stages\Console; +namespace Quantum\App\Adapters\Console\Stages\Runtime; use Quantum\App\Adapters\Console\ConsoleContext; use Quantum\App\Contracts\StageInterface; diff --git a/tests/Unit/App/Stages/Boot/InitConsoleStageTest.php b/tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php similarity index 95% rename from tests/Unit/App/Stages/Boot/InitConsoleStageTest.php rename to tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php index 9e86981a..bf7104a2 100644 --- a/tests/Unit/App/Stages/Boot/InitConsoleStageTest.php +++ b/tests/Unit/App/Adapters/Console/Stages/Boot/InitConsoleStageTest.php @@ -1,14 +1,14 @@ Date: Wed, 21 Oct 2026 14:47:12 +0400 Subject: [PATCH 3/5] [#538] Give the web and console runtime stages lifecycle events The six web runtime stages declare web..before / .after and RunCommandStage declares console.command.before / .after; the pipeline dispatches them with the runtime context. --- .../Stages/Runtime/RunCommandStage.php | 4 + .../Stages/Runtime/DispatchRequestStage.php | 4 + .../Stages/Runtime/HandlePreflightStage.php | 4 + .../Runtime/HandleRouteNotFoundStage.php | 4 + .../Stages/Runtime/PrepareRequestStage.php | 4 + .../Web/Stages/Runtime/ResolveRouteStage.php | 4 + .../Web/Stages/Runtime/SendResponseStage.php | 4 + .../Console/ConsoleAppAdapterTest.php | 45 ++++++ .../App/Adapters/Web/WebAppAdapterTest.php | 139 ++++++++++++++++++ tests/Unit/App/PipelineTest.php | 31 ++++ 10 files changed, 243 insertions(+) diff --git a/src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php b/src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php index 1e8e002e..17329cd6 100644 --- a/src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php +++ b/src/App/Adapters/Console/Stages/Runtime/RunCommandStage.php @@ -25,6 +25,10 @@ */ class RunCommandStage implements StageInterface { + public const BEFORE = 'console.command.before'; + + public const AFTER = 'console.command.after'; + /** * @param ConsoleContext $context * @throws Exception diff --git a/src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php b/src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php index 6312f9e5..ec27c1ae 100644 --- a/src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/DispatchRequestStage.php @@ -33,6 +33,10 @@ */ class DispatchRequestStage implements StageInterface { + public const BEFORE = 'web.dispatch.before'; + + public const AFTER = 'web.dispatch.after'; + /** * @param WebContext $context * @throws MiddlewareException|BaseException|DiException diff --git a/src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php b/src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php index 87955f6d..a259681e 100644 --- a/src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/HandlePreflightStage.php @@ -23,6 +23,10 @@ */ class HandlePreflightStage implements StageInterface { + public const BEFORE = 'web.preflight.before'; + + public const AFTER = 'web.preflight.after'; + /** * Answers an OPTIONS request with an empty response and stops the remaining stages * @param WebContext $context diff --git a/src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php b/src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php index 928fe516..02cd28f5 100644 --- a/src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/HandleRouteNotFoundStage.php @@ -25,6 +25,10 @@ */ class HandleRouteNotFoundStage implements StageInterface { + public const BEFORE = 'web.not_found.before'; + + public const AFTER = 'web.not_found.after'; + /** * Answers a request that matched no route with the page not found response and stops the remaining stages * @param WebContext $context diff --git a/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php index 41660931..bb55dcab 100644 --- a/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/PrepareRequestStage.php @@ -32,6 +32,10 @@ */ class PrepareRequestStage implements StageInterface { + public const BEFORE = 'web.prepare.before'; + + public const AFTER = 'web.prepare.after'; + /** * @param WebContext $context * @throws LangException|ConfigException|DiException|ReflectionException diff --git a/src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php b/src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php index a9cc3922..d940556b 100644 --- a/src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/ResolveRouteStage.php @@ -25,6 +25,10 @@ */ class ResolveRouteStage implements StageInterface { + public const BEFORE = 'web.route.before'; + + public const AFTER = 'web.route.after'; + /** * Finds the route for the current request and stores it on the request, or null when nothing matches * @param WebContext $context diff --git a/src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php b/src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php index 5f362d89..c62f308e 100644 --- a/src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php +++ b/src/App/Adapters/Web/Stages/Runtime/SendResponseStage.php @@ -34,6 +34,10 @@ */ class SendResponseStage implements StageInterface { + public const BEFORE = 'web.send.before'; + + public const AFTER = 'web.send.after'; + /** * @param WebContext $context * @throws ConfigException|DiException|ReflectionException|Exception diff --git a/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php b/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php index 4b75af4f..c10d39e4 100644 --- a/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php +++ b/tests/Unit/App/Adapters/Console/ConsoleAppAdapterTest.php @@ -5,6 +5,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Quantum\App\Adapters\Console\ConsoleAppAdapter; use Symfony\Component\Console\Input\InputInterface; +use Quantum\App\Adapters\Console\ConsoleContext; use Quantum\Environment\Exceptions\EnvException; use Quantum\Tests\Helpers\NonExitingApplication; use Symfony\Component\Console\Input\ArgvInput; @@ -195,6 +196,50 @@ public function testConsoleAppAdapterBootsAllStagesForCoreEnvCommandWhenTheEnvFi $this->assertTrue(config()->has('app')); } + public function testConsoleAppAdapterStartDispatchesTheCommandEventsAroundTheCommandWithTheConsoleContext(): void + { + $_SERVER['argv'] = ['qt', 'list', '--quiet']; + + $adapter = $this->createAdapter(new NonExitingApplication('Qt Console Application', 'test')); + + $events = []; + + foreach (['console.command.before', 'console.command.after'] as $name) { + event()->listen($name, function (array $payload) use (&$events, $name): void { + $events[] = [$name, get_class($payload['context'])]; + }); + } + + $this->assertSame(0, $adapter->start()); + + $this->assertSame([ + ['console.command.before', ConsoleContext::class], + ['console.command.after', ConsoleContext::class], + ], $events); + } + + public function testConsoleAppAdapterStartDispatchesNoAfterEventWhenTheCommandIsUnknown(): void + { + $_SERVER['argv'] = ['qt', 'unknown:command', '--quiet']; + + $adapter = $this->createAdapter(); + + $events = []; + + foreach (['console.command.before', 'console.command.after'] as $name) { + event()->listen($name, function () use (&$events, $name): void { + $events[] = $name; + }); + } + + try { + $adapter->start(); + $this->fail('Expected the unknown command to be rejected.'); + } catch (Exception $exception) { + $this->assertSame(['console.command.before'], $events); + } + } + public function testConsoleAppAdapterConstructorDoesNotBoot(): void { $_SERVER['argv'] = ['qt', 'list', '--quiet']; diff --git a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php index 5e35b03d..f903c34c 100644 --- a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php +++ b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php @@ -4,6 +4,7 @@ use Quantum\Router\Exceptions\RouteException; use Quantum\App\Adapters\Web\WebAppAdapter; +use Quantum\App\Adapters\Web\WebContext; use Quantum\Environment\Environment; use Quantum\ResourceCache\ViewCache; use Quantum\Tests\Unit\AppTestCase; @@ -13,6 +14,7 @@ use Quantum\Debugger\Debugger; use Quantum\Router\Route; use Quantum\Di\Di; +use ArrayObject; class WebAppAdapterTest extends AppTestCase { @@ -248,6 +250,122 @@ public function testWebAppAdapterStartPropagatesDispatchExceptionWithoutCleanup( $this->assertNotNull(request()->getUri()); } + public function testWebAppAdapterStartDispatchesTheEventsOfEveryRuntimeStageForAMatchedRoute(): void + { + request()->create('GET', '/test/am/tests'); + + $events = $this->recordRuntimeEvents(); + + ob_start(); + $this->webAppAdapter->start(); + ob_end_clean(); + + $this->assertSame([ + 'web.preflight.before', 'web.preflight.after', + 'web.route.before', 'web.route.after', + 'web.not_found.before', 'web.not_found.after', + 'web.prepare.before', 'web.prepare.after', + 'web.dispatch.before', 'web.dispatch.after', + 'web.send.before', 'web.send.after', + ], $events->getArrayCopy()); + } + + public function testWebAppAdapterStartDispatchesOnlyThePreflightAndSendEventsForAnOptionsRequest(): void + { + request()->create('OPTIONS', '/test/am/tests'); + + $events = $this->recordRuntimeEvents(); + + ob_start(); + $this->webAppAdapter->start(); + ob_end_clean(); + + $this->assertSame([ + 'web.preflight.before', 'web.preflight.after', + 'web.send.before', 'web.send.after', + ], $events->getArrayCopy()); + } + + public function testWebAppAdapterStartDispatchesNoPrepareOrDispatchEventsWhenNoRouteMatches(): void + { + request()->create('GET', '/non-existing-uri'); + + $events = $this->recordRuntimeEvents(); + + ob_start(); + $this->webAppAdapter->start(); + ob_end_clean(); + + $this->assertSame([ + 'web.preflight.before', 'web.preflight.after', + 'web.route.before', 'web.route.after', + 'web.not_found.before', 'web.not_found.after', + 'web.send.before', 'web.send.after', + ], $events->getArrayCopy()); + } + + public function testWebAppAdapterStartDispatchesNoAfterEventAndNoSendEventsWhenAStageThrows(): void + { + request()->create('GET', '/test/Test/5'); + + $events = $this->recordRuntimeEvents(); + + ob_start(); + + try { + $this->webAppAdapter->start(); + $this->fail('Expected the dispatcher to fail for a missing controller action.'); + } catch (RouteException $exception) { + $this->assertSame([ + 'web.preflight.before', 'web.preflight.after', + 'web.route.before', 'web.route.after', + 'web.not_found.before', 'web.not_found.after', + 'web.prepare.before', 'web.prepare.after', + 'web.dispatch.before', + ], $events->getArrayCopy()); + } finally { + ob_end_clean(); + } + } + + public function testWebAppAdapterStartPassesTheWebContextToTheRuntimeEvents(): void + { + request()->create('GET', '/test/am/tests'); + + $contexts = []; + + foreach (['web.preflight.before', 'web.send.after'] as $name) { + event()->listen($name, function (array $payload) use (&$contexts): void { + $contexts[] = $payload['context']; + }); + } + + ob_start(); + $this->webAppAdapter->start(); + ob_end_clean(); + + $this->assertCount(2, $contexts); + $this->assertInstanceOf(WebContext::class, $contexts[0]); + $this->assertSame($contexts[0], $contexts[1]); + } + + public function testWebAppAdapterStartDispatchesTheSendAfterEventOnceTheRequestIsCleared(): void + { + request()->create('GET', '/test/am/tests'); + + $uriSeenAfterSend = 'not called'; + + event()->listen('web.send.after', function () use (&$uriSeenAfterSend): void { + $uriSeenAfterSend = request()->getUri(); + }); + + ob_start(); + $this->webAppAdapter->start(); + ob_end_clean(); + + $this->assertNull($uriSeenAfterSend); + } + public function testWebAppAdapterConstructorDoesNotBoot(): void { $this->clearAppContext(); @@ -302,6 +420,27 @@ public function testWebAppAdapterSecondBootDoesNotRegisterAppHelperListenerAgain }); } + /** + * Listens to the before and after events of every web runtime stage and records their names in order + * @return ArrayObject + */ + private function recordRuntimeEvents(): ArrayObject + { + $events = new ArrayObject(); + + foreach (['preflight', 'route', 'not_found', 'prepare', 'dispatch', 'send'] as $stage) { + foreach (['before', 'after'] as $point) { + $name = 'web.' . $stage . '.' . $point; + + event()->listen($name, function () use ($events, $name): void { + $events[] = $name; + }); + } + } + + return $events; + } + private function withTemporaryAppHelper(callable $test): void { $dir = PROJECT_ROOT . DS . 'helpers'; diff --git a/tests/Unit/App/PipelineTest.php b/tests/Unit/App/PipelineTest.php index e47b8fe9..69c3fd83 100644 --- a/tests/Unit/App/PipelineTest.php +++ b/tests/Unit/App/PipelineTest.php @@ -3,6 +3,13 @@ namespace Quantum\Tests\Unit\App; use Quantum\App\Adapters\Console\Stages\Boot\SetupConsoleApplicationStage; +use Quantum\App\Adapters\Web\Stages\Runtime\HandleRouteNotFoundStage; +use Quantum\App\Adapters\Web\Stages\Runtime\HandlePreflightStage; +use Quantum\App\Adapters\Web\Stages\Runtime\DispatchRequestStage; +use Quantum\App\Adapters\Console\Stages\Runtime\RunCommandStage; +use Quantum\App\Adapters\Web\Stages\Runtime\PrepareRequestStage; +use Quantum\App\Adapters\Web\Stages\Runtime\SendResponseStage; +use Quantum\App\Adapters\Web\Stages\Runtime\ResolveRouteStage; use Quantum\App\Adapters\Console\Stages\Boot\InitConsoleStage; use Quantum\App\Adapters\Web\Stages\Boot\InitDebuggerStage; use Quantum\App\Adapters\Web\Stages\Boot\LoadModulesStage; @@ -379,6 +386,30 @@ public function testPipelineStagesDeclareBootEventNames(): void $this->assertSame('boot.modules.after', LoadModulesStage::AFTER); } + public function testPipelineRuntimeStagesDeclareTheirEventNames(): void + { + $this->assertSame('web.preflight.before', HandlePreflightStage::BEFORE); + $this->assertSame('web.preflight.after', HandlePreflightStage::AFTER); + + $this->assertSame('web.route.before', ResolveRouteStage::BEFORE); + $this->assertSame('web.route.after', ResolveRouteStage::AFTER); + + $this->assertSame('web.not_found.before', HandleRouteNotFoundStage::BEFORE); + $this->assertSame('web.not_found.after', HandleRouteNotFoundStage::AFTER); + + $this->assertSame('web.prepare.before', PrepareRequestStage::BEFORE); + $this->assertSame('web.prepare.after', PrepareRequestStage::AFTER); + + $this->assertSame('web.dispatch.before', DispatchRequestStage::BEFORE); + $this->assertSame('web.dispatch.after', DispatchRequestStage::AFTER); + + $this->assertSame('web.send.before', SendResponseStage::BEFORE); + $this->assertSame('web.send.after', SendResponseStage::AFTER); + + $this->assertSame('console.command.before', RunCommandStage::BEFORE); + $this->assertSame('console.command.after', RunCommandStage::AFTER); + } + public function testLoadHelpersInitConsoleInitDebuggerAndSetupConsoleApplicationStagesDeclareNoEvents(): void { foreach ([LoadHelpersStage::class, InitConsoleStage::class, InitDebuggerStage::class, SetupConsoleApplicationStage::class] as $stage) { From 4f7f8906ed6a021254809b74ad28f4abd3e20578 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 14:53:24 +0400 Subject: [PATCH 4/5] [#538] Remove the lifecycle events from the boot stages The boot belongs to the framework: no boot stage declares BEFORE/AFTER, and a test guards it for every boot folder. App helper listeners hook into the runtime events instead. The pipeline no longer checks whether the event() helper is loaded, since runtime stages run after the helpers. --- .../Web/Stages/Boot/InitHttpStage.php | 4 -- .../Web/Stages/Boot/LoadModulesStage.php | 4 -- src/App/Pipeline.php | 4 +- src/App/Stages/Boot/LoadAppConfigStage.php | 4 -- src/App/Stages/Boot/LoadEnvironmentStage.php | 4 -- .../Stages/Boot/SetupErrorHandlerStage.php | 4 -- .../App/Adapters/Web/WebAppAdapterTest.php | 43 ++++++++---- tests/Unit/App/PipelineTest.php | 68 ++++++------------- 8 files changed, 51 insertions(+), 84 deletions(-) diff --git a/src/App/Adapters/Web/Stages/Boot/InitHttpStage.php b/src/App/Adapters/Web/Stages/Boot/InitHttpStage.php index b38f4b21..5dbb61cc 100644 --- a/src/App/Adapters/Web/Stages/Boot/InitHttpStage.php +++ b/src/App/Adapters/Web/Stages/Boot/InitHttpStage.php @@ -23,10 +23,6 @@ */ class InitHttpStage implements StageInterface { - public const BEFORE = 'boot.http.before'; - - public const AFTER = 'boot.http.after'; - public function process(AppContext $context): void { if (!Di::isRegistered(Request::class)) { diff --git a/src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php b/src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php index 094eeb22..e053bd4b 100644 --- a/src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php +++ b/src/App/Adapters/Web/Stages/Boot/LoadModulesStage.php @@ -28,10 +28,6 @@ */ class LoadModulesStage implements StageInterface { - public const BEFORE = 'boot.modules.before'; - - public const AFTER = 'boot.modules.after'; - /** * @throws ModuleException|RouteException|DiException|ReflectionException */ diff --git a/src/App/Pipeline.php b/src/App/Pipeline.php index 5c86ec09..9818ff5c 100644 --- a/src/App/Pipeline.php +++ b/src/App/Pipeline.php @@ -85,7 +85,7 @@ private function runStage(StageInterface $stage, AppContext $context): void /** * 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. + * Only runtime stages declare events, and they run after the helpers are loaded. * @param StageInterface $stage * @param TContext $context */ @@ -93,7 +93,7 @@ private function dispatchStageEvent(StageInterface $stage, string $point, AppCon { $constant = $stage::class . '::' . $point; - if (!defined($constant) || !function_exists('event')) { + if (!defined($constant)) { return; } diff --git a/src/App/Stages/Boot/LoadAppConfigStage.php b/src/App/Stages/Boot/LoadAppConfigStage.php index 82873094..e07959d1 100644 --- a/src/App/Stages/Boot/LoadAppConfigStage.php +++ b/src/App/Stages/Boot/LoadAppConfigStage.php @@ -24,10 +24,6 @@ */ class LoadAppConfigStage implements StageInterface { - public const BEFORE = 'boot.config.before'; - - public const AFTER = 'boot.config.after'; - /** * @throws ConfigException|DiException|ReflectionException */ diff --git a/src/App/Stages/Boot/LoadEnvironmentStage.php b/src/App/Stages/Boot/LoadEnvironmentStage.php index 81fb8701..2fd3f4c3 100644 --- a/src/App/Stages/Boot/LoadEnvironmentStage.php +++ b/src/App/Stages/Boot/LoadEnvironmentStage.php @@ -25,10 +25,6 @@ */ class LoadEnvironmentStage implements StageInterface { - public const BEFORE = 'boot.environment.before'; - - public const AFTER = 'boot.environment.after'; - /** * @throws EnvException|DiException|BaseException */ diff --git a/src/App/Stages/Boot/SetupErrorHandlerStage.php b/src/App/Stages/Boot/SetupErrorHandlerStage.php index c146a90a..c38e6c89 100644 --- a/src/App/Stages/Boot/SetupErrorHandlerStage.php +++ b/src/App/Stages/Boot/SetupErrorHandlerStage.php @@ -27,10 +27,6 @@ */ class SetupErrorHandlerStage implements StageInterface { - public const BEFORE = 'boot.error_handler.before'; - - public const AFTER = 'boot.error_handler.after'; - /** * @throws ConfigException|DiException|BaseException|ReflectionException */ diff --git a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php index f903c34c..98f54570 100644 --- a/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php +++ b/tests/Unit/App/Adapters/Web/WebAppAdapterTest.php @@ -386,15 +386,13 @@ public function testWebAppAdapterBootRunsTheBootStages(): void $this->assertTrue(config()->has('app')); } - public function testWebAppAdapterBootFiresAppHelperListenerAtModulesBeforeEvent(): void + public function testWebAppAdapterStartFiresAppHelperListenerAtPreflightBeforeEvent(): void { $this->withTemporaryAppHelper(function (): void { - $context = $this->createContext(); + $this->startWithAppHelperListener(); - (new WebAppAdapter($context))->boot(); - - $this->assertCount(1, $GLOBALS['bootEventPayloads']); - $this->assertSame($context, $GLOBALS['bootEventPayloads'][0]['context']); + $this->assertCount(1, $GLOBALS['helperEventPayloads']); + $this->assertInstanceOf(WebContext::class, $GLOBALS['helperEventPayloads'][0]['context']); }); } @@ -403,7 +401,7 @@ public function testWebAppAdapterBootRegistersAppHelperListenerBeforeAppConfigIs $this->withTemporaryAppHelper(function (): void { (new WebAppAdapter($this->createContext()))->boot(); - $this->assertFalse($GLOBALS['bootEventConfigLoadedAtRegistration']); + $this->assertFalse($GLOBALS['helperConfigLoadedAtRegistration']); $this->assertTrue(config()->has('app')); }); } @@ -411,15 +409,30 @@ public function testWebAppAdapterBootRegistersAppHelperListenerBeforeAppConfigIs public function testWebAppAdapterSecondBootDoesNotRegisterAppHelperListenerAgain(): void { $this->withTemporaryAppHelper(function (): void { - (new WebAppAdapter($this->createContext()))->boot(); - $this->assertCount(1, $GLOBALS['bootEventPayloads']); + $this->startWithAppHelperListener(); + $this->assertCount(1, $GLOBALS['helperEventPayloads']); // Helper files load with require_once, so a new container does not get the listener again. - (new WebAppAdapter($this->createContext()))->boot(); - $this->assertCount(1, $GLOBALS['bootEventPayloads']); + $this->startWithAppHelperListener(); + $this->assertCount(1, $GLOBALS['helperEventPayloads']); }); } + /** + * Boots a new adapter, which loads the temporary app helper, and starts it for a matched route + */ + private function startWithAppHelperListener(): void + { + $adapter = new WebAppAdapter($this->createContext()); + $adapter->boot(); + + request()->create('GET', '/test/am/tests'); + + ob_start(); + $adapter->start(); + ob_end_clean(); + } + /** * Listens to the before and after events of every web runtime stage and records their names in order * @return ArrayObject @@ -450,10 +463,10 @@ private function withTemporaryAppHelper(callable $test): void $helper = <<<'PHP' has('app'); +$GLOBALS['helperConfigLoadedAtRegistration'] = config()->has('app'); -event()->listen('boot.modules.before', function (array $payload): void { - $GLOBALS['bootEventPayloads'][] = $payload; +event()->listen('web.preflight.before', function (array $payload): void { + $GLOBALS['helperEventPayloads'][] = $payload; }); PHP; @@ -472,7 +485,7 @@ private function withTemporaryAppHelper(callable $test): void if ($createdDir && is_dir($dir)) { rmdir($dir); } - unset($GLOBALS['bootEventPayloads'], $GLOBALS['bootEventConfigLoadedAtRegistration']); + unset($GLOBALS['helperEventPayloads'], $GLOBALS['helperConfigLoadedAtRegistration']); } } } diff --git a/tests/Unit/App/PipelineTest.php b/tests/Unit/App/PipelineTest.php index 69c3fd83..b1cb3586 100644 --- a/tests/Unit/App/PipelineTest.php +++ b/tests/Unit/App/PipelineTest.php @@ -2,7 +2,6 @@ namespace Quantum\Tests\Unit\App; -use Quantum\App\Adapters\Console\Stages\Boot\SetupConsoleApplicationStage; use Quantum\App\Adapters\Web\Stages\Runtime\HandleRouteNotFoundStage; use Quantum\App\Adapters\Web\Stages\Runtime\HandlePreflightStage; use Quantum\App\Adapters\Web\Stages\Runtime\DispatchRequestStage; @@ -10,14 +9,7 @@ use Quantum\App\Adapters\Web\Stages\Runtime\PrepareRequestStage; use Quantum\App\Adapters\Web\Stages\Runtime\SendResponseStage; use Quantum\App\Adapters\Web\Stages\Runtime\ResolveRouteStage; -use Quantum\App\Adapters\Console\Stages\Boot\InitConsoleStage; -use Quantum\App\Adapters\Web\Stages\Boot\InitDebuggerStage; -use Quantum\App\Adapters\Web\Stages\Boot\LoadModulesStage; -use Quantum\App\Adapters\Web\Stages\Boot\InitHttpStage; -use Quantum\App\Stages\Boot\SetupErrorHandlerStage; -use Quantum\App\Stages\Boot\LoadEnvironmentStage; use Quantum\App\Adapters\Console\ConsoleContext; -use Quantum\App\Stages\Boot\LoadAppConfigStage; use Quantum\App\Stages\Boot\LoadHelpersStage; use Quantum\App\Contracts\StageInterface; use Quantum\App\Adapters\Web\WebContext; @@ -202,23 +194,6 @@ public function testPipelinePropagatesAnExceptionWithoutTheAfterEvent(): void $this->assertFalse($after); } - /** - * @runInSeparateProcess - * @preserveGlobalState disabled - */ - public function testPipelineSkipsStageEventsWhileEventHelperIsNotLoaded(): void - { - $this->assertFalse(function_exists('event')); - - $processed = false; - - (new Pipeline([$this->createFirstEventStage(function () use (&$processed): void { - $processed = true; - })]))->run($this->context); - - $this->assertTrue($processed); - } - public function testPipelineEndsTheRunWhenAStageStopsTheContext(): void { $log = []; @@ -368,24 +343,6 @@ public function testPipelineCarriesTheExitCodeOfAConsoleContextBetweenStages(): $this->assertSame(ExitCode::INVALID, $exitCodeSeenBySecondStage); } - public function testPipelineStagesDeclareBootEventNames(): void - { - $this->assertSame('boot.environment.before', LoadEnvironmentStage::BEFORE); - $this->assertSame('boot.environment.after', LoadEnvironmentStage::AFTER); - - $this->assertSame('boot.config.before', LoadAppConfigStage::BEFORE); - $this->assertSame('boot.config.after', LoadAppConfigStage::AFTER); - - $this->assertSame('boot.error_handler.before', SetupErrorHandlerStage::BEFORE); - $this->assertSame('boot.error_handler.after', SetupErrorHandlerStage::AFTER); - - $this->assertSame('boot.http.before', InitHttpStage::BEFORE); - $this->assertSame('boot.http.after', InitHttpStage::AFTER); - - $this->assertSame('boot.modules.before', LoadModulesStage::BEFORE); - $this->assertSame('boot.modules.after', LoadModulesStage::AFTER); - } - public function testPipelineRuntimeStagesDeclareTheirEventNames(): void { $this->assertSame('web.preflight.before', HandlePreflightStage::BEFORE); @@ -410,11 +367,28 @@ public function testPipelineRuntimeStagesDeclareTheirEventNames(): void $this->assertSame('console.command.after', RunCommandStage::AFTER); } - public function testLoadHelpersInitConsoleInitDebuggerAndSetupConsoleApplicationStagesDeclareNoEvents(): void + public function testPipelineBootStagesDeclareNoEvents(): void { - foreach ([LoadHelpersStage::class, InitConsoleStage::class, InitDebuggerStage::class, SetupConsoleApplicationStage::class] as $stage) { - $this->assertFalse(defined($stage . '::BEFORE'), $stage . ' must not declare BEFORE'); - $this->assertFalse(defined($stage . '::AFTER'), $stage . ' must not declare AFTER'); + $app = dirname(__DIR__, 3) . DS . 'src' . DS . 'App'; + + $bootFolders = [ + 'Quantum\\App\\Stages\\Boot\\' => $app . DS . 'Stages' . DS . 'Boot', + 'Quantum\\App\\Adapters\\Web\\Stages\\Boot\\' => $app . DS . 'Adapters' . DS . 'Web' . DS . 'Stages' . DS . 'Boot', + 'Quantum\\App\\Adapters\\Console\\Stages\\Boot\\' => $app . DS . 'Adapters' . DS . 'Console' . DS . 'Stages' . DS . 'Boot', + ]; + + foreach ($bootFolders as $namespace => $folder) { + $files = glob($folder . DS . '*Stage.php'); + + $this->assertNotEmpty($files, 'No boot stages found in ' . $folder); + + foreach ($files as $file) { + $stage = $namespace . basename($file, '.php'); + + $this->assertTrue(class_exists($stage), $stage . ' must exist'); + $this->assertFalse(defined($stage . '::BEFORE'), $stage . ' must not declare BEFORE'); + $this->assertFalse(defined($stage . '::AFTER'), $stage . ' must not declare AFTER'); + } } } From 5b652d1f3327c8397ebaa7faa870c6fb3a609b87 Mon Sep 17 00:00:00 2001 From: Arman <407448+armanist@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:02:19 +0400 Subject: [PATCH 5/5] [#538] Describe the runtime stage events and the stage layout in the changelog --- CHANGELOG.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f705fe9d..6b794c02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,13 @@ The format is based on Keep a Changelog. ### Added - Added `Request::expectsJson()`: true when the `Accept` header lists a JSON media type (`application/json` or a `+json` type, parameters and case ignored), or for an AJAX request that accepts any content type (#581) -- Added boot stage lifecycle events: stages may declare `BEFORE`/`AFTER` event names and the boot `Pipeline` dispatches them with the `AppContext` as a named payload. The environment, config, error handler, HTTP, and modules stages declare `boot..before` and `boot..after` (#536) +- Added lifecycle events for the web and console runtime: the runtime stages declare `web.preflight`, `web.route`, `web.not_found`, `web.prepare`, `web.dispatch`, `web.send` and `console.command`, each with `.before` and `.after`, and the `Pipeline` dispatches them with the runtime context as a named `context` payload. A stage that does not run dispatches nothing, a stage that throws dispatches no `.after`, and the closing `web.send` events also follow an OPTIONS or not found response. Boot stages declare no events. `console.command.after` fires only with a Symfony application that does not exit after the command (#536, #538) - Added adapter-based Lang provider support with `DeepL` and `Google Translate` adapters plus shared remote request/caching infrastructure (#533) ### Changed -- **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #575, #576, #579, #583, #589): +- **BREAKING:** Reworked the app boot and runtime into pipelines of stages that the adapters run in explicit steps (#537, #538, #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. + - 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 stages are grouped by the adapter that runs them: the boot stages both adapters run in `Quantum\App\Stages\Boot`, the others in `Quantum\App\Adapters\Web\Stages\Boot`, `...\Web\Stages\Runtime`, `...\Console\Stages\Boot` and `...\Console\Stages\Runtime`. 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`, which runs `StageInterface` 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` (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. @@ -26,7 +26,7 @@ The format is based on Keep a Changelog. - `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`. - Listeners stay registered after `dispatch()` instead of being removed after the first call. - - Listeners can be registered from app helper files, which load before config and before any boot event. + - Listeners can be registered from app helper files, which load before the config and before any runtime event. - Removed the unused `hooks_dir()` helper and renamed the debugger tab constant `Debugger::HOOKS` to `Debugger::EVENTS`; the debugbar tab is now `events` and lists only events that have listeners. - **BREAKING:** Removed `Quantum\Loader` and moved its responsibilities to their owning packages (#535): - Moved `Setup` to `Quantum\Config\Setup`; required config failures now use `ConfigException`, and optional imports use `Config::importIfExists()`.