diff --git a/core/lib/Application.php b/core/lib/Application.php index d6b6d8a..e73cc92 100644 --- a/core/lib/Application.php +++ b/core/lib/Application.php @@ -70,9 +70,9 @@ final class Application } } - public function handleHttp(Request $request): Response + public function runHttpRequest(Request $request): Response { - return $this->http->handle($request); + return $this->http->run($request, send: false); } public function runConsole(): int diff --git a/core/lib/Application/Execution/ExecutionRunner.php b/core/lib/Application/Execution/ExecutionRunner.php new file mode 100644 index 0000000..cfe6ea6 --- /dev/null +++ b/core/lib/Application/Execution/ExecutionRunner.php @@ -0,0 +1,50 @@ +kernel->beginExecution($descriptor); + $outcome = ExecutionOutcome::incomplete(); + + try { + $result = $execution($scope); + $outcome = ExecutionOutcome::success($result); + + return $result; + } catch (\Throwable $error) { + if ($failure === null) { + $outcome = ExecutionOutcome::failure($error); + + throw $error; + } + + try { + $result = $failure($error, $scope); + $outcome = ExecutionOutcome::failure($error, $result); + + return $result; + } catch (\Throwable $failureError) { + $outcome = ExecutionOutcome::failure($failureError); + + throw $failureError; + } + } finally { + $this->kernel->terminateExecution($scope, $outcome); + } + } +} diff --git a/core/lib/Application/Execution/ExecutionRunnerInterface.php b/core/lib/Application/Execution/ExecutionRunnerInterface.php new file mode 100644 index 0000000..11a9c8e --- /dev/null +++ b/core/lib/Application/Execution/ExecutionRunnerInterface.php @@ -0,0 +1,22 @@ +initialized = false; $this->booted = false; $this->container = null; + $this->runner = null; } private function initialize(): void @@ -84,7 +88,10 @@ class Kernel implements KernelInterface } // Create logger from config (driver + level-filter; per-tenant wrapping applied later in DI) - $this->logger = LoggerFactory::create($this->config, $this->folderRoot()); + $this->logger = LoggerFactory::create( + $this->config, + $this->options->paths->project, + ); $this->initializeErrorHandlers(); @@ -162,6 +169,11 @@ class Kernel implements KernelInterface } } + public function executionRunner(): ExecutionRunnerInterface + { + return $this->runner ??= new ExecutionRunner($this); + } + public function reboot(): void { $this->shutdown(); @@ -284,8 +296,12 @@ class Kernel implements KernelInterface */ protected function parameters(): array { + $projectDir = $this->options->paths->project; + $cacheDir = $this->options->paths->cache($this->environment()); + $logsDir = $this->options->paths->logs(); + return [ - 'kernel.project_dir' => realpath($this->folderRoot()) ?: $this->folderRoot(), + 'kernel.project_dir' => realpath($projectDir) ?: $projectDir, 'kernel.environment' => $this->environment(), 'kernel.runtime_environment' => '%env(default:kernel.environment:APP_RUNTIME_ENV)%', 'kernel.runtime_mode' => '%env(query_string:default:container.runtime_mode:APP_RUNTIME_MODE)%', @@ -293,10 +309,10 @@ class Kernel implements KernelInterface 'kernel.runtime_mode.cli' => '%env(not:default:kernel.runtime_mode.web:)%', 'kernel.runtime_mode.worker' => '%env(bool:default::key:worker:default:kernel.runtime_mode:)%', 'kernel.debug' => $this->debug(), - 'kernel.build_dir' => realpath($this->getBuildDir()) ?: $this->getBuildDir(), - 'kernel.cache_dir' => realpath($this->getCacheDir()) ?: $this->getCacheDir(), - 'kernel.logs_dir' => realpath($this->getLogDir()) ?: $this->getLogDir(), - 'kernel.charset' => $this->getCharset(), + 'kernel.build_dir' => realpath($cacheDir) ?: $cacheDir, + 'kernel.cache_dir' => realpath($cacheDir) ?: $cacheDir, + 'kernel.logs_dir' => realpath($logsDir) ?: $logsDir, + 'kernel.charset' => 'UTF-8', ]; } @@ -324,43 +340,6 @@ class Kernel implements KernelInterface return $this->debug() && null !== $this->startTime ? $this->startTime : -\INF; } - /** - * Gets the application root dir (path of the project's composer file). - */ - public function folderRoot(): string - { - return $this->options->paths->project; - } - - - /** - * Gets the path to the configuration directory. - */ - private function getConfigDir(): string - { - return $this->options->paths->configuration(); - } - - public function getCacheDir(): string - { - return $this->options->paths->cache($this->environment()); - } - - public function getBuildDir(): string - { - return $this->getCacheDir(); - } - - public function getLogDir(): string - { - return $this->options->paths->logs(); - } - - public function getCharset(): string - { - return 'UTF-8'; - } - /** * Initializes the service container */ @@ -393,7 +372,7 @@ class Kernel implements KernelInterface protected function configureContainer(Builder $builder): void { // Service definitions - $projectDir = $this->folderRoot(); + $projectDir = $this->options->paths->project; $moduleDir = $projectDir . '/modules'; $environment = $this->environment(); diff --git a/core/lib/KernelInterface.php b/core/lib/KernelInterface.php index f5b4f60..8216b0e 100644 --- a/core/lib/KernelInterface.php +++ b/core/lib/KernelInterface.php @@ -6,6 +6,7 @@ namespace KTXC; use KTXC\Application\Execution\ExecutionDescriptor; use KTXC\Application\Execution\ExecutionOutcome; +use KTXC\Application\Execution\ExecutionRunnerInterface; use KTXC\Application\Execution\ExecutionScope; use KTXC\Application\Execution\TerminationReport; use Psr\Container\ContainerInterface; @@ -14,6 +15,8 @@ interface KernelInterface { public function boot(): void; + public function executionRunner(): ExecutionRunnerInterface; + public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope; public function terminateExecution( diff --git a/core/lib/Runtime/Console/ConsoleRuntime.php b/core/lib/Runtime/Console/ConsoleRuntime.php index 6312a82..45cb6f2 100644 --- a/core/lib/Runtime/Console/ConsoleRuntime.php +++ b/core/lib/Runtime/Console/ConsoleRuntime.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace KTXC\Runtime\Console; use KTXC\Application\Execution\ExecutionDescriptor; -use KTXC\Application\Execution\ExecutionOutcome; use KTXC\Kernel; use KTXC\KernelInterface; use KTXC\Module\ModuleManager; @@ -24,57 +23,37 @@ final class ConsoleRuntime ) { } - public function run( - ?InputInterface $input = null, - ?OutputInterface $output = null, - ): int + public function run(?InputInterface $input = null, ?OutputInterface $output = null): int { - $scope = $this->kernel->beginExecution(ExecutionDescriptor::cli()); - $outcome = ExecutionOutcome::incomplete(); + return $this->kernel->executionRunner()->execute( + ExecutionDescriptor::cli(), + function () use ($input, $output): int { + $container = $this->kernel->container(); + $console = new ConsoleApplication('Vallarx Console', Kernel::VERSION); + $console->setAutoExit(false); - try { - $exitCode = $this->application()->run($input, $output); - $outcome = ExecutionOutcome::success($exitCode); + /** @var ModuleManager $moduleManager */ + $moduleManager = $container->get(ModuleManager::class); + foreach ($moduleManager->list() as $module) { + $instance = $module->instance(); + if (!$instance instanceof ModuleConsoleInterface) { + continue; + } - return $exitCode; - } catch (\Throwable $error) { - $outcome = ExecutionOutcome::failure($error); - throw $error; - } finally { - $this->kernel->terminateExecution($scope, $outcome); - } - } + foreach ($instance->registerCI() as $commandClass) { + $this->registerCommand($console, $container, $commandClass); + } + } - private function application(): ConsoleApplication - { - $container = $this->kernel->container(); - $console = new ConsoleApplication('Vallarx Console', Kernel::VERSION); - $console->setAutoExit(false); - - /** @var ModuleManager $moduleManager */ - $moduleManager = $container->get(ModuleManager::class); - foreach ($moduleManager->list() as $module) { - $instance = $module->instance(); - if (!$instance instanceof ModuleConsoleInterface) { - continue; + return $console->run($input, $output); } - - foreach ($instance->registerCI() as $commandClass) { - $this->registerLazyCommand($console, $container, $commandClass); - } - } - - return $console; + ); } /** * @param class-string $commandClass */ - private function registerLazyCommand( - ConsoleApplication $console, - ContainerInterface $container, - string $commandClass, - ): void { + private function registerCommand(ConsoleApplication $console, ContainerInterface $container, string $commandClass): void { if (!class_exists($commandClass)) { throw new \RuntimeException("Command class not found: {$commandClass}"); } diff --git a/core/lib/Runtime/Http/HttpRuntime.php b/core/lib/Runtime/Http/HttpRuntime.php index be214a8..dba75a0 100644 --- a/core/lib/Runtime/Http/HttpRuntime.php +++ b/core/lib/Runtime/Http/HttpRuntime.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace KTXC\Runtime\Http; use KTXC\Application\Execution\ExecutionDescriptor; -use KTXC\Application\Execution\ExecutionOutcome; use KTXC\Http\Middleware\AuthenticationMiddleware; use KTXC\Http\Middleware\FirewallMiddleware; use KTXC\Http\Middleware\MiddlewarePipeline; @@ -23,48 +22,31 @@ final class HttpRuntime ) { } - public function run(?Request $request = null): Response + public function run(?Request $request = null, bool $send = true): Response { - return $this->execute( - $request ?? Request::createFromGlobals(), - send: true, + $request ??= Request::createFromGlobals(); + + return $this->kernel->executionRunner()->execute( + ExecutionDescriptor::http(), + function () use ($request, $send): Response { + $response = $this->pipeline()->handle($request); + if ($send) { + $response->send(); + } + + return $response; + }, + function (\Throwable $error) use ($send): Response { + $response = $this->errorResponse($error); + if ($send) { + $response->send(); + } + + return $response; + }, ); } - public function handle(Request $request): Response - { - return $this->execute($request, send: false); - } - - private function execute(Request $request, bool $send): Response - { - $scope = null; - $outcome = ExecutionOutcome::incomplete(); - - try { - $scope = $this->kernel->beginExecution(ExecutionDescriptor::http()); - $response = $this->pipeline()->handle($request); - $outcome = ExecutionOutcome::success($response); - if ($send) { - $response->send(); - } - - return $response; - } catch (\Throwable $error) { - $response = $this->errorResponse($error); - $outcome = ExecutionOutcome::failure($error, $response); - if ($send) { - $response->send(); - } - - return $response; - } finally { - if ($scope !== null) { - $this->kernel->terminateExecution($scope, $outcome); - } - } - } - private function pipeline(): MiddlewarePipeline { $pipeline = new MiddlewarePipeline($this->kernel->container()); diff --git a/tests/php/Unit/Application/KernelTest.php b/tests/php/Unit/Application/KernelTest.php index 294b9e4..b252163 100644 --- a/tests/php/Unit/Application/KernelTest.php +++ b/tests/php/Unit/Application/KernelTest.php @@ -65,6 +65,44 @@ final class KernelTest extends TestCase self::assertTrue($scope->terminated()); } + #[Test] + #[TestDox('Runner returns execution results and terminates')] + public function runs(): void + { + $processor = new RecordingProcessor(); + [$kernel] = $this->kernel($processor); + + $result = $kernel->executionRunner()->execute( + ExecutionDescriptor::cli(), + static fn(): string => 'complete', + ); + + self::assertSame('complete', $result); + self::assertSame(1, $processor->processed); + } + + #[Test] + #[TestDox('Runner terminates before rethrowing execution failures')] + public function rethrows(): void + { + [$kernel] = $this->kernel(); + + try { + $kernel->executionRunner()->execute( + ExecutionDescriptor::cli(), + static fn() => throw new \RuntimeException('Execution failed.'), + ); + self::fail('Expected execution to fail.'); + } catch (\RuntimeException $error) { + self::assertSame('Execution failed.', $error->getMessage()); + } + + $scope = $kernel->beginExecution(ExecutionDescriptor::cli()); + $kernel->terminateExecution($scope, ExecutionOutcome::success()); + + self::assertTrue($scope->terminated()); + } + #[Test] #[TestDox('Only one execution scope can be active')] public function rejectsConcurrentScopes(): void diff --git a/tests/php/Unit/Runtime/ConsoleRuntimeTest.php b/tests/php/Unit/Runtime/ConsoleRuntimeTest.php index 87fdb69..532de37 100644 --- a/tests/php/Unit/Runtime/ConsoleRuntimeTest.php +++ b/tests/php/Unit/Runtime/ConsoleRuntimeTest.php @@ -7,6 +7,8 @@ namespace KTXT\Unit\Runtime; use KTXC\Application\Execution\ExecutionContext; use KTXC\Application\Execution\ExecutionDescriptor; use KTXC\Application\Execution\ExecutionOutcome; +use KTXC\Application\Execution\ExecutionRunner; +use KTXC\Application\Execution\ExecutionRunnerInterface; use KTXC\Application\Execution\ExecutionScope; use KTXC\Application\Execution\TerminationReport; use KTXC\Context\IdentityContext; @@ -110,6 +112,11 @@ final class ConsoleKernel implements KernelInterface { } + public function executionRunner(): ExecutionRunnerInterface + { + return new ExecutionRunner($this); + } + public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope { return new ExecutionScope( diff --git a/tests/php/Unit/Runtime/HttpRuntimeTest.php b/tests/php/Unit/Runtime/HttpRuntimeTest.php index 3c637f5..5cf4639 100644 --- a/tests/php/Unit/Runtime/HttpRuntimeTest.php +++ b/tests/php/Unit/Runtime/HttpRuntimeTest.php @@ -7,6 +7,8 @@ namespace KTXT\Unit\Runtime; use KTXC\Application\Execution\ExecutionContext; use KTXC\Application\Execution\ExecutionDescriptor; use KTXC\Application\Execution\ExecutionOutcome; +use KTXC\Application\Execution\ExecutionRunner; +use KTXC\Application\Execution\ExecutionRunnerInterface; use KTXC\Application\Execution\ExecutionScope; use KTXC\Application\Execution\TerminationReport; use KTXC\Context\IdentityContext; @@ -68,7 +70,7 @@ final class HttpRuntimeTest extends TestCase public function succeeds(): void { $kernel = $this->kernel(new ResponseMiddleware(new Response('ok', 200))); - $response = (new HttpRuntime($kernel, false))->handle(Request::create('/')); + $response = (new HttpRuntime($kernel, false))->run(Request::create('/'), send: false); self::assertSame(200, $response->getStatusCode()); self::assertSame('ok', $response->getContent()); @@ -84,7 +86,7 @@ final class HttpRuntimeTest extends TestCase $previousLog = ini_get('error_log'); ini_set('error_log', '/dev/null'); try { - $response = (new HttpRuntime($kernel, false))->handle(Request::create('/')); + $response = (new HttpRuntime($kernel, false))->run(Request::create('/'), send: false); } finally { ini_set('error_log', (string) $previousLog); } @@ -104,7 +106,7 @@ final class HttpRuntimeTest extends TestCase failures: [new \RuntimeException('Deferred listener failed.')], ); - $response = (new HttpRuntime($kernel, false))->handle(Request::create('/')); + $response = (new HttpRuntime($kernel, false))->run(Request::create('/'), send: false); self::assertSame(202, $response->getStatusCode()); self::assertSame('accepted', $response->getContent()); @@ -144,6 +146,11 @@ final class RuntimeKernel implements KernelInterface { } + public function executionRunner(): ExecutionRunnerInterface + { + return new ExecutionRunner($this); + } + public function beginExecution(ExecutionDescriptor $descriptor): ExecutionScope { $this->active = true;