From 7d2615ebf88de5ff16ddc21a6d1d678280b0cc4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Tue, 13 Oct 2026 12:36:22 +0200 Subject: [PATCH 01/32] Refactor README to focus on development branch and update badges Revised `README.md` to clarify the purpose and usage of the `develop` branch, emphasizing its role as the primary integration branch for ongoing development. Added PHPStan, PHPUnit, and Codecov badges for improved visibility of repository workflows. Updated references from `master` to `develop`, replaced outdated links, and removed non-essential community and support sections. Streamlined content to reflect the branch's intended audience: contributors and advanced users. --- README.md | 138 ++++++++++++++++++------------------------------------ 1 file changed, 45 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index 7faf576..34bfc77 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@

FireHub Core Logo -
+ FireHub Kernel
version + />

+[![PHPStan](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPStan.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPStan.yml) +[![PHPUnit](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPUnit.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPUnit.yml) +[![Codecov](https://codecov.io/gh/The-FireHub-Project/Kernel/branch/develop/graph/badge.svg?token=XW2YEONF51)](https://app.codecov.io/gh/The-FireHub-Project/Kernel/tree/develop) +

- - GitHub created at - + GitHub last commit (branch) - + GitHub activity (branch) - + GitHub contributors - - GitHub language count - - GitHub top language - - GitHub repo size - - GitHub code size

@@ -70,74 +49,47 @@ Designed as a shared, edition-agnostic runtime component for Standard, Professio --- -## FireHub Icon About FireHub - -**FireHub** is a modular PHP framework built around: +## FireHub Icon Development Branch -- Clean Architecture -- Domain-Driven Design (DDD) -- Explicit boundaries between Core, Adapters, and Modules -- Long-term maintainability and scalability +⚠️ **This is the `develop` branch** -FireHub is designed for building **enterprise-grade PHP applications** without sacrificing clarity or developer experience. +- Unstable +- APIs may change without notice +- Not intended for production use -## FireHub Icon Repository Purpose +## FireHub Icon Branch Purpose -FireHub Kernel provides the application-level orchestration required to initialize, operate, and shut down FireHub applications consistently. It implements stable contracts defined by Core Standard and remains independent of edition-specific features. +The `develop` branch is the **primary integration branch** for all ongoing development. -Its responsibilities include: -- Application lifecycle — managing initialization, readiness, failure, and shutdown. -- Bootstrap orchestration — executing initialization steps in a controlled order. -- Service integration — coordinating service registration and initialization through explicit contracts. -- Configuration initialization — making application configuration available during startup. -- Module integration — coordinating optional modules and capabilities without hard-coded edition checks. -- Application context — providing the environment and state needed for application initialization. +It serves as the staging area where: +- Feature branches are merged +- Bug fixes are integrated +- Experimental work is stabilized +- Code is prepared for upcoming releases -The Kernel does not implement HTTP routing, console commands, business logic, or individual capability features. Those responsibilities belong to adapters, applications, and dedicated capability packages. +All **release branches** are created **from `develop`**. -The same Kernel is used with all three FireHub editions. Core Standard is the minimum required edition; Professional and Enterprise extend the ecosystem through their own contracts and components, not separate Kernel implementations. +## Stability Guarantee -## FireHub Icon Security +❌ No backward compatibility guarantee +❌ APIs may change without notice +❌ Behavior may be incomplete or inconsistent +❌ Breaking changes are expected -For security reporting and guidelines, please refer to our [SECURITY.md](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file. +This branch is intended **only for contributors and advanced testers**. -## FireHub Icon Community - -![GitHub watchers](https://img.shields.io/github/watchers/The-FireHub-Project/Kernel?style=social) -![GitHub Repo stars](https://img.shields.io/github/stars/The-FireHub-Project/Kernel?style=social) -![GitHub forks](https://img.shields.io/github/forks/The-FireHub-Project/Kernel?style=social) - -- Be sure to review the [Code of Conduct](https://github.com/The-FireHub-Project/.github/blob/master/.github/CODE_OF_CONDUCT.md). -- Read a [Contributing](https://github.com/The-FireHub-Project/.github/blob/master/.github/CONTRIBUTING.md) file to see how you can contribute to this project. -- Read our [Security policy](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file to see how you can report a vulnerability. -- Review the project [Governance model](https://github.com/The-FireHub-Project/.github/blob/master/.github/GOVERNANCE.md), including roles, decision-making, RFC process, and releases. - -## FireHub Icon Support - -

- - GitHub in-progress issues - - GitHub total issues - - GitHub opened pull requests -

+## FireHub Icon Composer Usage (Not Recommended) -For help with FireHub products, please refer to the following resources: +For internal testing only: -- **General questions & discussions**: [GitHub Discussions](https://github.com/orgs/The-FireHub-Project/discussions) -- **Bug reports & issues**: [Support](https://github.com/The-FireHub-Project/Kernel/issues) -- **Documentation & guides**: [FireHub Docs](https://the-firehub-project.github.io) -- **Commercial support** (Pro/Enterprise): contact your account manager +```json +{ + "require": { + "the-firehub-project/kernel": "dev-develop" + } +} +``` +⚠️ Never use dev-develop in production. ## FireHub Icon Authors and Contributors From 7eaa24b936b140cc91d76cf872b32476b9754670 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Tue, 13 Oct 2026 15:02:37 +0200 Subject: [PATCH 02/32] Add application kernel implementation and lifecycle management components This commit introduces the core implementation of the application kernel for the FireHub project, complete with lifecycle management features. Key updates include: - **`Application.php`**: Implements contract-driven lifecycle management for kernel states (creation, booting, termination) with support for bootstrappers and terminators. - **`composer.json`**: Adds dependencies for `the-firehub-project/runtime` and `the-firehub-project/core-standard`. - **Interfaces**: Defines contracts for bootstrappers and terminators, both globally and specifically for the application kernel. - **Exceptions**: Adds custom exception classes for invalid kernel states, boot failures, and shutdown errors. This establishes the functional foundation for managing kernel state transitions, enabling flexible component registration and robust error handling. --- composer.json | 5 +- src/Application.php | 279 ++++++++++++++++++ src/Application/Bootstrapper.php | 28 ++ src/Application/Terminator.php | 28 ++ src/Bootstrapper.php | 28 ++ src/Exception/InvalidKernelStateException.php | 26 ++ src/Exception/KernelBootException.php | 26 ++ src/Exception/KernelShutdownException.php | 26 ++ src/Terminator.php | 28 ++ 9 files changed, 473 insertions(+), 1 deletion(-) create mode 100644 src/Application.php create mode 100644 src/Application/Bootstrapper.php create mode 100644 src/Application/Terminator.php create mode 100644 src/Bootstrapper.php create mode 100644 src/Exception/InvalidKernelStateException.php create mode 100644 src/Exception/KernelBootException.php create mode 100644 src/Exception/KernelShutdownException.php create mode 100644 src/Terminator.php diff --git a/composer.json b/composer.json index 48eb886..6614ee3 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,10 @@ "homepage": "https://the-firehub-project.github.io/kernel", "license": "Apache-2.0", "require": { - "php": "^8.5" + "php": "^8.5", + + "the-firehub-project/runtime": ">=0.1.0 <1.0.0", + "the-firehub-project/core-standard": "dev-develop" }, "require-dev": { "phpstan/phpstan": "^2.0", diff --git a/src/Application.php b/src/Application.php new file mode 100644 index 0000000..0f270c3 --- /dev/null +++ b/src/Application.php @@ -0,0 +1,279 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel; + +use FireHub\Core\Boundary\Kernel\Application as ApplicationBoundary; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Application\ { + Bootstrapper as ApplicationBootstrapper, Terminator as ApplicationTerminator, +}; +use FireHub\Core\Exception\Runtime\KernelException; +use FireHub\Kernel\Exception\ { + InvalidKernelStateException, KernelBootException, KernelShutdownException +}; +use FireHub\Runtime; + +/** + * ### Manages the application kernel lifecycle + * + * Implements the application kernel contract and coordinates transitions between kernel states. + * Subclasses can customize initialization and termination through protected lifecycle hooks. + * + * Repeated boot calls on a booted kernel and repeated shutdown calls on a terminated kernel + * are no-ops. A failed kernel cannot be rebooted, but shutdown can be attempted for cleanup. + * @since 1.0.0 + */ +final class Application implements ApplicationBoundary { + + /** + * ### Current kernel lifecycle state + * @since 1.0.0 + */ + private State $state = State::CREATED; + + /** + * ### Registered kernel bootstrappers + * @since 1.0.0 + * + * @var list<\FireHub\Kernel\Bootstrapper|\FireHub\Kernel\Application\Bootstrapper> + */ + private array $bootstrappers = []; + + /** + * ### Registered kernel terminators + * @since 1.0.0 + * + * @var list<\FireHub\Kernel\Terminator|\FireHub\Kernel\Application\Terminator> + */ + private array $terminators = []; + + /** + * ### Registers a kernel bootstrapper + * + * Adds a global or application-specific bootstrapper to the initialization sequence. + * + * Bootstrappers are executed in their registration order. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Application::assertRegistrationAllowed() To validate lifecycle component registration. + * + * @param \FireHub\Kernel\Bootstrapper|\FireHub\Kernel\Application\Bootstrapper $bootstrapper

+ * Bootstrapper to register. + *

+ * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. + * + * @return void + */ + public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $bootstrapper):void { + + $this->assertRegistrationAllowed(); + + $this->bootstrappers[] = $bootstrapper; + + } + + /** + * ### Registers a kernel terminator + * + * Adds a global or application-specific terminator to the shutdown sequence. + * + * Terminators are executed in reverse registration order. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Application::assertRegistrationAllowed() To validate lifecycle component registration. + * + * @param \FireHub\Kernel\Terminator|\FireHub\Kernel\Application\Terminator $terminator

+ * Terminator to register. + *

+ * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. + * + * @return void + */ + public function registerTerminator (Terminator|ApplicationTerminator $terminator):void { + + $this->assertRegistrationAllowed(); + + $this->terminators[] = $terminator; + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is in an invalid state. + * @throws \FireHub\Kernel\Exception\KernelBootException If the kernel fails to boot. + */ + public function boot ():void { + + if ($this->state === State::BOOTED) return; + + if ($this->state !== State::CREATED) + throw new InvalidKernelStateException( + 'Cannot boot the application kernel because of invalid state.', + [ + 'state' => $this->state + ] + ); + + $this->state = State::BOOTING; + + try { + + $this->initialize(); + + $this->state = State::BOOTED; + + } catch (KernelException $exception) { + + $this->state = State::FAILED; + + throw new KernelBootException( + 'Failed to boot the application kernel.', + previous: $exception + ); + + } + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is in an invalid state. + * @throws \FireHub\Kernel\Exception\KernelShutdownException If the kernel fails to shut down. + */ + public function shutdown ():void { + + if ($this->state === State::TERMINATED) return; + + if ($this->state !== State::CREATED && $this->state !== State::BOOTED && $this->state !== State::FAILED) + throw new InvalidKernelStateException( + 'Cannot shutdown the application kernel because of invalid state.', + [ + 'state' => $this->state + ] + ); + + $this->state = State::SHUTTING_DOWN; + + try { + $this->terminate(); + + $this->state = State::TERMINATED; + + } catch (KernelException $exception) { + + $this->state = State::FAILED; + + throw new KernelShutdownException( + 'Failed to shut down the application kernel.', + previous: $exception + ); + + } + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function state ():State { + + return $this->state; + + } + + /** + * ### Initializes application components + * + * Extension point for application-specific bootstrapping. The base implementation does nothing. + * @since 1.0.0 + * + * @uses \FireHub\Core\Boundary\Kernel\Bootstrapper::bootstrap() To bootstrap component. + * + * @return void + */ + private function initialize ():void { + + foreach ($this->bootstrappers as $bootstrapper) + $bootstrapper->bootstrap(); + + } + + /** + * ### Terminates application components + * + * Extension point for application-specific cleanup. The base implementation does nothing. + * @since 1.0.0 + * + * @uses \FireHub\Core\Boundary\Kernel\Terminator::terminate() To terminate component. + * @uses \FireHub\Runtime\Arr\Transform::reverse() Reverses an array. + * + * @throws \FireHub\Core\Exception\Runtime\KernelException If any terminator fails to terminate. + * + * @return void + */ + private function terminate ():void { + + $failure = null; + + foreach (Runtime\Arr\Transform::reverse($this->terminators) as $terminator) { + + try { + + $terminator->terminate(); + + } catch (KernelException $exception) { + + $failure ??= $exception; + + } + + } + + if ($failure !== null) throw $failure; + + } + + /** + * ### Validates lifecycle component registration + * + * Ensures that lifecycle components can only be registered before kernel initialization begins. + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. + * + * @return void + */ + private function assertRegistrationAllowed ():void { + + if ($this->state !== State::CREATED) + throw new InvalidKernelStateException( + 'Cannot register lifecycle components after kernel initialization has started.', + [ + 'state' => $this->state + ] + ); + + } + +} \ No newline at end of file diff --git a/src/Application/Bootstrapper.php b/src/Application/Bootstrapper.php new file mode 100644 index 0000000..681827b --- /dev/null +++ b/src/Application/Bootstrapper.php @@ -0,0 +1,28 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel + */ + +namespace FireHub\Kernel\Application; + +use FireHub\Core\Boundary\Kernel\Bootstrapper as BootstrapperBoundary; + +/** + * ### Defines an application kernel bootstrapper + * + * Establishes a bootstrapper contract for components that participate specifically in + * application kernel initialization. + * + * Application bootstrappers provide application-specific initialization behavior without + * requiring the kernel to depend on concrete component implementations. + * @since 1.0.0 + */ +interface Bootstrapper extends BootstrapperBoundary {} \ No newline at end of file diff --git a/src/Application/Terminator.php b/src/Application/Terminator.php new file mode 100644 index 0000000..07f369d --- /dev/null +++ b/src/Application/Terminator.php @@ -0,0 +1,28 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel + */ + +namespace FireHub\Kernel\Application; + +use FireHub\Core\Boundary\Kernel\Terminator as TerminatorBoundary; + +/** + * ### Defines an application kernel terminator + * + * Establishes a termination contract for components that participate specifically in + * application kernel shutdown. + * + * Application terminators provide application-specific cleanup behavior without requiring + * the kernel to depend on concrete component implementations. + * @since 1.0.0 + */ +interface Terminator extends TerminatorBoundary {} \ No newline at end of file diff --git a/src/Bootstrapper.php b/src/Bootstrapper.php new file mode 100644 index 0000000..9d37caf --- /dev/null +++ b/src/Bootstrapper.php @@ -0,0 +1,28 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel + */ + +namespace FireHub\Kernel; + +use FireHub\Core\Boundary\Kernel\Bootstrapper as BootstrapperBoundary; + +/** + * ### Defines a global kernel bootstrapper + * + * Establishes a bootstrapper contract for components that can participate in the initialization + * of any compatible kernel. + * + * Global bootstrappers are independent of a specific kernel type and can be shared across + * application, testing, worker, and other kernel implementations. + * @since 1.0.0 + */ +interface Bootstrapper extends BootstrapperBoundary {} \ No newline at end of file diff --git a/src/Exception/InvalidKernelStateException.php b/src/Exception/InvalidKernelStateException.php new file mode 100644 index 0000000..2afb66e --- /dev/null +++ b/src/Exception/InvalidKernelStateException.php @@ -0,0 +1,26 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Exception; + +use FireHub\Core\Exception\Runtime\KernelException; + +/** + * ### Represents an invalid kernel state + * @since 1.0.0 + */ +final class InvalidKernelStateException extends KernelException { + + protected const string DEFAULT_MESSAGE = 'The kernel is in an invalid state'; + +} \ No newline at end of file diff --git a/src/Exception/KernelBootException.php b/src/Exception/KernelBootException.php new file mode 100644 index 0000000..e54eb46 --- /dev/null +++ b/src/Exception/KernelBootException.php @@ -0,0 +1,26 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Exception; + +use FireHub\Core\Exception\Runtime\KernelException; + +/** + * ### Represents an invalid kernel state + * @since 1.0.0 + */ +final class KernelBootException extends KernelException { + + protected const string DEFAULT_MESSAGE = 'The kernel failed to boot'; + +} \ No newline at end of file diff --git a/src/Exception/KernelShutdownException.php b/src/Exception/KernelShutdownException.php new file mode 100644 index 0000000..82a3f99 --- /dev/null +++ b/src/Exception/KernelShutdownException.php @@ -0,0 +1,26 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Exception; + +use FireHub\Core\Exception\Runtime\KernelException; + +/** + * ### Represents an invalid kernel state + * @since 1.0.0 + */ +final class KernelShutdownException extends KernelException { + + protected const string DEFAULT_MESSAGE = 'The kernel failed to shutdown'; + +} \ No newline at end of file diff --git a/src/Terminator.php b/src/Terminator.php new file mode 100644 index 0000000..3664fea --- /dev/null +++ b/src/Terminator.php @@ -0,0 +1,28 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel + */ + +namespace FireHub\Kernel; + +use FireHub\Core\Boundary\Kernel\Terminator as TerminatorBoundary; + +/** + * ### Defines a global kernel terminator + * + * Establishes a termination contract for components that can participate in the shutdown + * of any compatible kernel. + * + * Global terminators are independent of a specific kernel type and can be shared across + * application, testing, worker, and other kernel implementations. + * @since 1.0.0 + */ +interface Terminator extends TerminatorBoundary {} \ No newline at end of file From baec1ca0872f80d3ed9534627802b06c089f32da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Tue, 13 Oct 2026 15:44:01 +0200 Subject: [PATCH 03/32] Add unit tests and stubs for application kernel lifecycle management This commit introduces the following updates to enhance testing and structure around the application kernel: - **Unit Tests (`ApplicationTest.php`)**: Implements comprehensive tests to verify kernel lifecycle behaviors (state transitions, bootstrappers, terminators, and idempotency of operations). - **New Stubs**: Adds `BasicBootstrapper`, `BasicTerminator`, `EventBootTerm`, and `AppEventBootTerm` to simulate bootstrapper and terminator behavior for kernel tests. - **Exception Updates**: Improves documentation for `KernelBootException` and `KernelShutdownException`, clarifying their purposes. - **Lifecycle Behavior Enhancements**: Adjusts `Application` logic to uniformly handle exceptions using `Throwable` instead of specific exception types. These changes establish a robust testing foundation for the kernel lifecycle while improving clarity and extensibility of lifecycle-related components. --- src/Application.php | 20 ++- src/Exception/KernelBootException.php | 4 +- src/Exception/KernelShutdownException.php | 4 +- tests/Stubs/AppEventBootTerm.php | 53 ++++++ tests/Stubs/BasicBootstrapper.php | 40 +++++ tests/Stubs/BasicTerminator.php | 40 +++++ tests/Stubs/EventBootTerm.php | 53 ++++++ tests/Unit/ApplicationTest.php | 198 ++++++++++++++++++++++ 8 files changed, 402 insertions(+), 10 deletions(-) create mode 100644 tests/Stubs/AppEventBootTerm.php create mode 100644 tests/Stubs/BasicBootstrapper.php create mode 100644 tests/Stubs/BasicTerminator.php create mode 100644 tests/Stubs/EventBootTerm.php create mode 100644 tests/Unit/ApplicationTest.php diff --git a/src/Application.php b/src/Application.php index 0f270c3..8b750c6 100644 --- a/src/Application.php +++ b/src/Application.php @@ -18,20 +18,24 @@ use FireHub\Kernel\Application\ { Bootstrapper as ApplicationBootstrapper, Terminator as ApplicationTerminator, }; -use FireHub\Core\Exception\Runtime\KernelException; use FireHub\Kernel\Exception\ { InvalidKernelStateException, KernelBootException, KernelShutdownException }; use FireHub\Runtime; +use Throwable; /** * ### Manages the application kernel lifecycle * - * Implements the application kernel contract and coordinates transitions between kernel states. - * Subclasses can customize initialization and termination through protected lifecycle hooks. + * Implements the application kernel contract and coordinates application initialization and termination. * - * Repeated boot calls on a booted kernel and repeated shutdown calls on a terminated kernel - * are no-ops. A failed kernel cannot be rebooted, but shutdown can be attempted for cleanup. + * Supports global and application-specific bootstrappers and terminators. Bootstrappers are executed + * in registration order, while terminators are executed in reverse registration order. + * + * Lifecycle components can only be registered before kernel initialization begins. + * + * Repeated boot calls on a booted kernel and repeated shutdown calls on a terminated kernel are no-ops. + * A failed kernel cannot be rebooted, but shutdown can be attempted to release resources. * @since 1.0.0 */ final class Application implements ApplicationBoundary { @@ -138,7 +142,7 @@ public function boot ():void { $this->state = State::BOOTED; - } catch (KernelException $exception) { + } catch (Throwable $exception) { $this->state = State::FAILED; @@ -178,7 +182,7 @@ public function shutdown ():void { $this->state = State::TERMINATED; - } catch (KernelException $exception) { + } catch (Throwable $exception) { $this->state = State::FAILED; @@ -242,7 +246,7 @@ private function terminate ():void { $terminator->terminate(); - } catch (KernelException $exception) { + } catch (Throwable $exception) { $failure ??= $exception; diff --git a/src/Exception/KernelBootException.php b/src/Exception/KernelBootException.php index e54eb46..0a9d015 100644 --- a/src/Exception/KernelBootException.php +++ b/src/Exception/KernelBootException.php @@ -16,7 +16,9 @@ use FireHub\Core\Exception\Runtime\KernelException; /** - * ### Represents an invalid kernel state + * ### ### Represents a kernel boot failure + * + * Indicates that application kernel initialization could not be completed successfully. * @since 1.0.0 */ final class KernelBootException extends KernelException { diff --git a/src/Exception/KernelShutdownException.php b/src/Exception/KernelShutdownException.php index 82a3f99..26c52bc 100644 --- a/src/Exception/KernelShutdownException.php +++ b/src/Exception/KernelShutdownException.php @@ -16,7 +16,9 @@ use FireHub\Core\Exception\Runtime\KernelException; /** - * ### Represents an invalid kernel state + * ### Represents a kernel shutdown failure + * + * Indicates that application kernel termination could not be completed successfully. * @since 1.0.0 */ final class KernelShutdownException extends KernelException { diff --git a/tests/Stubs/AppEventBootTerm.php b/tests/Stubs/AppEventBootTerm.php new file mode 100644 index 0000000..4fc8fdf --- /dev/null +++ b/tests/Stubs/AppEventBootTerm.php @@ -0,0 +1,53 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Stubs; + +use FireHub\Kernel\Application\ { + Bootstrapper, Terminator +}; + +/** + * ### Application event boot and term stub + * @since 1.0.0 + */ +class AppEventBootTerm implements Bootstrapper, Terminator { + + /** + * @since 1.0.0 + */ + public function __construct (private array &$events) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function bootstrap ():void { + + $this->events[] = 'application'; + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function terminate ():void { + + $this->events[] = 'application'; + + } + +} \ No newline at end of file diff --git a/tests/Stubs/BasicBootstrapper.php b/tests/Stubs/BasicBootstrapper.php new file mode 100644 index 0000000..bd48e6d --- /dev/null +++ b/tests/Stubs/BasicBootstrapper.php @@ -0,0 +1,40 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Stubs; + +use FireHub\Kernel\Bootstrapper; + +/** + * ### Basic bootstrapper + * @since 1.0.0 + */ +class BasicBootstrapper implements Bootstrapper { + + /** + * @since 1.0.0 + */ + public function __construct (private int &$calls) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function bootstrap ():void { + + $this->calls++; + + } + +} \ No newline at end of file diff --git a/tests/Stubs/BasicTerminator.php b/tests/Stubs/BasicTerminator.php new file mode 100644 index 0000000..48b97cb --- /dev/null +++ b/tests/Stubs/BasicTerminator.php @@ -0,0 +1,40 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Stubs; + +use FireHub\Kernel\Terminator; + +/** + * ### Basic terminator + * @since 1.0.0 + */ +class BasicTerminator implements Terminator { + + /** + * @since 1.0.0 + */ + public function __construct (private int &$calls) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function terminate ():void { + + $this->calls++; + + } + +} \ No newline at end of file diff --git a/tests/Stubs/EventBootTerm.php b/tests/Stubs/EventBootTerm.php new file mode 100644 index 0000000..ce7c1f3 --- /dev/null +++ b/tests/Stubs/EventBootTerm.php @@ -0,0 +1,53 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Stubs; + +use FireHub\Kernel\ { + Bootstrapper, Terminator +}; + +/** + * ### Event boot and term stub + * @since 1.0.0 + */ +class EventBootTerm implements Bootstrapper, Terminator { + + /** + * @since 1.0.0 + */ + public function __construct (private array &$events) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function bootstrap ():void { + + $this->events[] = 'global'; + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function terminate ():void { + + $this->events[] = 'global'; + + } + +} \ No newline at end of file diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php new file mode 100644 index 0000000..2a6b9bb --- /dev/null +++ b/tests/Unit/ApplicationTest.php @@ -0,0 +1,198 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Application; +use FireHub\Kernel\ { + Bootstrapper, Terminator, + Application\Bootstrapper as ApplicationBootstrapper, Application\Terminator as ApplicationTerminator, +}; +use FireHub\Tests\Kernel\Stubs\ { + AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Test application kernel lifecycle management + * @since 1.0.0 + */ +#[Small] +#[Group('application')] +#[CoversClass(Application::class)] +final class ApplicationTest extends FireHubTestCase { + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitialState ():void { + + self::assertSame(State::CREATED, new Application()->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBoot ():void { + + $kernel = new Application(); + + $kernel->boot(); + + self::assertSame(State::BOOTED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBootIsIdempotent ():void { + + $calls = 0; + $kernel = new Application(); + $kernel->registerBootstrapper(new BasicBootstrapper($calls)); + + $kernel->boot(); + $kernel->boot(); + + self::assertSame(1, $calls); + self::assertSame(State::BOOTED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdown ():void { + + $kernel = new Application(); + $kernel->boot(); + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownFromCreatedState ():void { + + $kernel = new Application(); + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownIsIdempotent ():void { + + $calls = 0; + $kernel = new Application(); + $kernel->registerTerminator(new BasicTerminator($calls)); + + $kernel->boot(); + $kernel->shutdown(); + $kernel->shutdown(); + + self::assertSame(1, $calls); + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testGlobalAndApplicationBootstrappersRunInRegistrationOrder ():void { + + $events = []; + $kernel = new Application(); + + $kernel->registerBootstrapper(new EventBootTerm($events)); + + $kernel->registerBootstrapper(new AppEventBootTerm($events)); + + $kernel->boot(); + + self::assertSame(['global', 'application'], $events); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testGlobalAndApplicationTerminatorsRunInReverseRegistrationOrder ():void { + + $events = []; + $kernel = new Application(); + + $kernel->registerBootstrapper(new EventBootTerm($events)); + + $kernel->registerBootstrapper(new AppEventBootTerm($events)); + + $kernel->boot(); + $kernel->shutdown(); + + self::assertSame(['global', 'application'], $events); + + } + +} \ No newline at end of file From 5db43f04523bfecc741626c17f955085430edb36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 09:08:19 +0200 Subject: [PATCH 04/32] Refactor application kernel to decouple lifecycle management from state handling This update restructures the application kernel's lifecycle management for improved modularity and maintainability. Key changes: - **New Lifecycle Components**: Implements `Lifecycle`, `Controller`, and `Manager` classes to separate state transition logic from lifecycle operations. - **Unit Tests**: Replaces `ApplicationTest.php` with `LifecycleTest.php` and `ManagerTest.php` for component-specific testing, ensuring fine-grained validation of state transitions and execution order. - **Simplified Application**: Refactors `Application.php` to delegate lifecycle handling to `Controller`, reducing complexity and enhancing clarity. - **Removals**: Removes redundant initialization and termination methods, relying on `Manager` to handle bootstrapper and terminator execution. This refactor paves the way for scalable lifecycle extensions while preserving backward compatibility for existing integrations. --- src/Application.php | 201 +++------------ src/Lifecycle.php | 172 +++++++++++++ src/Lifecycle/Controller.php | 224 +++++++++++++++++ src/Lifecycle/Manager.php | 134 ++++++++++ tests/Unit/ApplicationTest.php | 198 --------------- tests/Unit/LifecycleTest.php | 148 +++++++++++ tests/Unit/Lifestyle/ControllerTest.php | 321 ++++++++++++++++++++++++ tests/Unit/Lifestyle/ManagerTest.php | 226 +++++++++++++++++ 8 files changed, 1255 insertions(+), 369 deletions(-) create mode 100644 src/Lifecycle.php create mode 100644 src/Lifecycle/Controller.php create mode 100644 src/Lifecycle/Manager.php delete mode 100644 tests/Unit/ApplicationTest.php create mode 100644 tests/Unit/LifecycleTest.php create mode 100644 tests/Unit/Lifestyle/ControllerTest.php create mode 100644 tests/Unit/Lifestyle/ManagerTest.php diff --git a/src/Application.php b/src/Application.php index 8b750c6..d765421 100644 --- a/src/Application.php +++ b/src/Application.php @@ -7,7 +7,7 @@ * @copyright 2026-present The FireHub Project - All rights reserved * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 * - * @php-version >=8.1 + * @php-version >=8.5 * @package Kernel */ @@ -18,11 +18,7 @@ use FireHub\Kernel\Application\ { Bootstrapper as ApplicationBootstrapper, Terminator as ApplicationTerminator, }; -use FireHub\Kernel\Exception\ { - InvalidKernelStateException, KernelBootException, KernelShutdownException -}; -use FireHub\Runtime; -use Throwable; +use FireHub\Kernel\Lifecycle\Controller; /** * ### Manages the application kernel lifecycle @@ -38,39 +34,31 @@ * A failed kernel cannot be rebooted, but shutdown can be attempted to release resources. * @since 1.0.0 */ -final class Application implements ApplicationBoundary { +final readonly class Application implements ApplicationBoundary { /** - * ### Current kernel lifecycle state + * ### Application lifecycle controller * @since 1.0.0 */ - private State $state = State::CREATED; + private Controller $lifecycle; /** - * ### Registered kernel bootstrappers + * ### Constructor * @since 1.0.0 * - * @var list<\FireHub\Kernel\Bootstrapper|\FireHub\Kernel\Application\Bootstrapper> + * @return void */ - private array $bootstrappers = []; + public function __construct () { - /** - * ### Registered kernel terminators - * @since 1.0.0 - * - * @var list<\FireHub\Kernel\Terminator|\FireHub\Kernel\Application\Terminator> - */ - private array $terminators = []; + $this->lifecycle = new Controller; + + } /** - * ### Registers a kernel bootstrapper - * - * Adds a global or application-specific bootstrapper to the initialization sequence. - * - * Bootstrappers are executed in their registration order. + * ### Registers a global or application-specific bootstrapper * @since 1.0.0 * - * @uses \FireHub\Kernel\Application::assertRegistrationAllowed() To validate lifecycle component registration. + * @uses \FireHub\Kernel\Lifecycle\Controller::registerBootstrapper() To register a bootstrapper. * * @param \FireHub\Kernel\Bootstrapper|\FireHub\Kernel\Application\Bootstrapper $bootstrapper

* Bootstrapper to register. @@ -82,21 +70,15 @@ final class Application implements ApplicationBoundary { */ public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $bootstrapper):void { - $this->assertRegistrationAllowed(); - - $this->bootstrappers[] = $bootstrapper; + $this->lifecycle->registerBootstrapper($bootstrapper); } /** - * ### Registers a kernel terminator - * - * Adds a global or application-specific terminator to the shutdown sequence. - * - * Terminators are executed in reverse registration order. + * ### Registers a global or application-specific terminator * @since 1.0.0 * - * @uses \FireHub\Kernel\Application::assertRegistrationAllowed() To validate lifecycle component registration. + * @uses \FireHub\Kernel\Lifecycle\Controller::registerTerminator() To register a terminator. * * @param \FireHub\Kernel\Terminator|\FireHub\Kernel\Application\Terminator $terminator

* Terminator to register. @@ -108,9 +90,7 @@ public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $boot */ public function registerTerminator (Terminator|ApplicationTerminator $terminator):void { - $this->assertRegistrationAllowed(); - - $this->terminators[] = $terminator; + $this->lifecycle->registerTerminator($terminator); } @@ -119,39 +99,14 @@ public function registerTerminator (Terminator|ApplicationTerminator $terminator * * @since 1.0.0 * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is in an invalid state. - * @throws \FireHub\Kernel\Exception\KernelBootException If the kernel fails to boot. + * @uses \FireHub\Kernel\Lifecycle\Controller::boot() To boot the kernel. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If booting is not allowed. + * @throws \FireHub\Kernel\Exception\KernelBootException If initialization fails. */ public function boot ():void { - if ($this->state === State::BOOTED) return; - - if ($this->state !== State::CREATED) - throw new InvalidKernelStateException( - 'Cannot boot the application kernel because of invalid state.', - [ - 'state' => $this->state - ] - ); - - $this->state = State::BOOTING; - - try { - - $this->initialize(); - - $this->state = State::BOOTED; - - } catch (Throwable $exception) { - - $this->state = State::FAILED; - - throw new KernelBootException( - 'Failed to boot the application kernel.', - previous: $exception - ); - - } + $this->lifecycle->boot(); } @@ -160,38 +115,14 @@ public function boot ():void { * * @since 1.0.0 * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is in an invalid state. - * @throws \FireHub\Kernel\Exception\KernelShutdownException If the kernel fails to shut down. + * @uses \FireHub\Kernel\Lifecycle\Controller::shutdown() To shutdown the kernel. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. + * @throws \FireHub\Kernel\Exception\KernelShutdownException If termination fails. */ public function shutdown ():void { - if ($this->state === State::TERMINATED) return; - - if ($this->state !== State::CREATED && $this->state !== State::BOOTED && $this->state !== State::FAILED) - throw new InvalidKernelStateException( - 'Cannot shutdown the application kernel because of invalid state.', - [ - 'state' => $this->state - ] - ); - - $this->state = State::SHUTTING_DOWN; - - try { - $this->terminate(); - - $this->state = State::TERMINATED; - - } catch (Throwable $exception) { - - $this->state = State::FAILED; - - throw new KernelShutdownException( - 'Failed to shut down the application kernel.', - previous: $exception - ); - - } + $this->lifecycle->shutdown(); } @@ -199,84 +130,12 @@ public function shutdown ():void { * @inheritDoc * * @since 1.0.0 - */ - public function state ():State { - - return $this->state; - - } - - /** - * ### Initializes application components - * - * Extension point for application-specific bootstrapping. The base implementation does nothing. - * @since 1.0.0 - * - * @uses \FireHub\Core\Boundary\Kernel\Bootstrapper::bootstrap() To bootstrap component. - * - * @return void - */ - private function initialize ():void { - - foreach ($this->bootstrappers as $bootstrapper) - $bootstrapper->bootstrap(); - - } - - /** - * ### Terminates application components - * - * Extension point for application-specific cleanup. The base implementation does nothing. - * @since 1.0.0 - * - * @uses \FireHub\Core\Boundary\Kernel\Terminator::terminate() To terminate component. - * @uses \FireHub\Runtime\Arr\Transform::reverse() Reverses an array. - * - * @throws \FireHub\Core\Exception\Runtime\KernelException If any terminator fails to terminate. - * - * @return void - */ - private function terminate ():void { - - $failure = null; - - foreach (Runtime\Arr\Transform::reverse($this->terminators) as $terminator) { - - try { - - $terminator->terminate(); - - } catch (Throwable $exception) { - - $failure ??= $exception; - - } - - } - - if ($failure !== null) throw $failure; - - } - - /** - * ### Validates lifecycle component registration - * - * Ensures that lifecycle components can only be registered before kernel initialization begins. - * @since 1.0.0 * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. - * - * @return void + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. */ - private function assertRegistrationAllowed ():void { + public function state ():State { - if ($this->state !== State::CREATED) - throw new InvalidKernelStateException( - 'Cannot register lifecycle components after kernel initialization has started.', - [ - 'state' => $this->state - ] - ); + return $this->lifecycle->state(); } diff --git a/src/Lifecycle.php b/src/Lifecycle.php new file mode 100644 index 0000000..88a23fb --- /dev/null +++ b/src/Lifecycle.php @@ -0,0 +1,172 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel; + +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Exception\InvalidKernelStateException; + +/** + * ### Manages kernel lifecycle state transitions + * + * Encapsulates the kernel state machine and ensures that lifecycle transitions happen only from valid states. This + * class does not execute lifecycle components. + * @since 1.0.0 + */ +final class Lifecycle { + + /** + * ### Current lifecycle state + * @since 1.0.0 + */ + private State $state = State::CREATED; + + /** + * ### Gets the current lifecycle state + * @since 1.0.0 + * + * @return State Current lifecycle state. + */ + public function state ():State { + + return $this->state; + + } + + /** + * ### Begins kernel initialization + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle::transition() Performs a guarded lifecycle transition. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel cannot begin booting. + * + * @return void + */ + public function beginBoot ():void { + + $this->transition(State::CREATED, State::BOOTING); + + } + + /** + * ### Completes kernel initialization + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle::transition() Performs a guarded lifecycle transition. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is not booting. + * + * @return void + */ + public function completeBoot ():void { + + $this->transition(State::BOOTING, State::BOOTED); + + } + + /** + * ### Begins kernel termination + * + * Shutdown may begin before initialization, after a successful initialization, + * or after an initialization failure. + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. + * + * @return void + */ + public function beginShutdown ():void { + + if ($this->state !== State::CREATED && $this->state !== State::BOOTED && $this->state !== State::FAILED) + throw new InvalidKernelStateException( + 'Cannot begin kernel shutdown in the current state.', + [ + 'state' => $this->state + ] + ); + + $this->state = State::SHUTTING_DOWN; + + } + + /** + * ### Completes kernel termination + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle::transition() Performs a guarded lifecycle transition. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is not shutting down. + * + * @return void + */ + public function completeShutdown ():void { + + $this->transition(State::SHUTTING_DOWN, State::TERMINATED); + + } + + /** + * ### Marks an active lifecycle operation as failed + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If no lifecycle operation is active. + * + * @return void + */ + public function fail ():void { + + if ($this->state !== State::BOOTING && $this->state !== State::SHUTTING_DOWN) + throw new InvalidKernelStateException( + 'Cannot mark the kernel as failed outside an active lifecycle operation.', + [ + 'state' => $this->state + ] + ); + + $this->state = State::FAILED; + + } + + /** + * ### Performs a guarded lifecycle transition + * @since 1.0.0 + * + * @param State $expected

+ * Expected current state. + *

+ * @param State $next

+ * Next state. + *

+ * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the current state does not match. + * + * @return void + */ + private function transition (State $expected, State $next):void { + + if ($this->state !== $expected) + throw new InvalidKernelStateException( + 'Cannot perform the requested kernel lifecycle transition.', + [ + 'state' => $this->state, + 'expected' => $expected, + 'next' => $next + ] + ); + + $this->state = $next; + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php new file mode 100644 index 0000000..d915198 --- /dev/null +++ b/src/Lifecycle/Controller.php @@ -0,0 +1,224 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle; + +use FireHub\Core\Boundary\Kernel; +use FireHub\Core\Boundary\Kernel\ { + Bootstrapper, Terminator +}; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Lifecycle; +use FireHub\Kernel\Exception\ { + InvalidKernelStateException, KernelBootException, KernelShutdownException +}; +use Throwable; + +/** + * ### Coordinates the kernel lifecycle + * + * Owns the lifecycle state machine and component execution manager. Validates + * registration and orchestrates boot and shutdown operations. + * + * The controller is kernel-type agnostic. Application-specific registration + * restrictions are enforced by the public Application facade. + * @since 1.0.0 + */ +final readonly class Controller implements Kernel { + + /** + * ### Lifecycle state machine + * @since 1.0.0 + */ + private Lifecycle $lifecycle; + + /** + * ### Lifecycle component manager + * @since 1.0.0 + */ + private Manager $manager; + + /** + * ### Constructor + * @since 1.0.0 + * + * @return void + */ + public function __construct () { + + $this->lifecycle = new Lifecycle; + + $this->manager = new Manager; + + } + + /** + * ### Registers a bootstrapper before initialization + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Controller::assertRegistrationAllowed() To validate registration. + * @uses \FireHub\Kernel\Lifecycle\Manager::registerBootstrapper() To register bootstrapper. + * + * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

+ * Bootstrapper to register. + *

+ * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. + * + * @return void + */ + public function registerBootstrapper (Bootstrapper $bootstrapper):void { + + $this->assertRegistrationAllowed(); + + $this->manager->registerBootstrapper($bootstrapper); + + } + + /** + * ### Registers a terminator before initialization + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Controller::assertRegistrationAllowed() To validate registration. + * @uses \FireHub\Kernel\Lifecycle\Manager::registerTerminator() To register terminator. + * + * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

+ * Terminator to register. + *

+ * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. + * + * @return void + */ + public function registerTerminator (Terminator $terminator):void { + + $this->assertRegistrationAllowed(); + + $this->manager->registerTerminator($terminator); + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. + * @uses \FireHub\Kernel\Lifecycle\Manager::initialize() To initialize. + * @uses \FireHub\Kernel\Lifecycle::beginBoot() To begin booting. + * @uses \FireHub\Kernel\Lifecycle::completeBoot() To complete booting. + * @uses \FireHub\Kernel\Lifecycle::fail() To fail booting. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If booting is not allowed. + * @throws \FireHub\Kernel\Exception\KernelBootException If a bootstrapper fails. + */ + public function boot ():void { + + if ($this->state() === State::BOOTED) return; + + $this->lifecycle->beginBoot(); + + try { + + $this->manager->initialize(); + + $this->lifecycle->completeBoot(); + + } catch (Throwable $exception) { + + $this->lifecycle->fail(); + + throw new KernelBootException( + 'Failed to boot the kernel.', + previous: $exception + ); + + } + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. + * @uses \FireHub\Kernel\Lifecycle\Manager::terminate() To terminate. + * @uses \FireHub\Kernel\Lifecycle::beginShutdown() To begin booting. + * @uses \FireHub\Kernel\Lifecycle::completeShutdown() To complete shutdown. + * @uses \FireHub\Kernel\Lifecycle::fail() To fail booting. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. + * @throws \FireHub\Kernel\Exception\KernelShutdownException If a terminator fails. + */ + public function shutdown ():void { + + if ($this->state() === State::TERMINATED) return; + + $this->lifecycle->beginShutdown(); + + try { + + $this->manager->terminate(); + + $this->lifecycle->completeShutdown(); + + } catch (Throwable $exception) { + + $this->lifecycle->fail(); + + throw new KernelShutdownException( + 'Failed to shut down the kernel.', + previous: $exception + ); + + } + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle::state() To get the current state. + */ + public function state ():State { + + return $this->lifecycle->state(); + + } + + /** + * ### Validates lifecycle component registration + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. + * + * @return void + */ + private function assertRegistrationAllowed ():void { + + if ($this->state() !== State::CREATED) + throw new InvalidKernelStateException( + 'Cannot register lifecycle components after kernel initialization has started.', + [ + 'state' => $this->state() + ] + ); + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Manager.php b/src/Lifecycle/Manager.php new file mode 100644 index 0000000..f594ff2 --- /dev/null +++ b/src/Lifecycle/Manager.php @@ -0,0 +1,134 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle; + +use FireHub\Core\Boundary\Kernel\ { + Bootstrapper, Terminator +}; +use FireHub\Runtime; +use Throwable; + +/** + * ### Registers and executes kernel lifecycle components + * + * Executes bootstrappers in registration order and terminators in reverse order. + * Shutdown attempts every terminator even if an earlier terminator fails. + * + * State validation belongs to the lifecycle controller, not this execution manager. + * @since 1.0.0 + */ +final class Manager { + + /** + * ### Registered bootstrappers + * @since 1.0.0 + * + * @var list<\FireHub\Core\Boundary\Kernel\Bootstrapper> + */ + private array $bootstrappers = []; + + /** + * ### Registered terminators + * @since 1.0.0 + * + * @var list<\FireHub\Core\Boundary\Kernel\Terminator> + */ + private array $terminators = []; + + /** + * ### Registers a bootstrapper + * @since 1.0.0 + * + * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

+ * Bootstrapper to register. + *

+ * + * @return void + */ + public function registerBootstrapper (Bootstrapper $bootstrapper):void { + + $this->bootstrappers[] = $bootstrapper; + + } + + /** + * ### Registers a terminator + * @since 1.0.0 + * + * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

+ * Terminator to register. + *

+ * + * @return void + */ + public function registerTerminator (Terminator $terminator):void { + + $this->terminators[] = $terminator; + + } + + /** + * ### Initializes registered components + * + * Stops immediately if a bootstrapper fails, preserving its original exception. + * @since 1.0.0 + * + * @uses \FireHub\Core\Boundary\Kernel\Bootstrapper::bootstrap() To bootstrap component. + * + * @return void + */ + public function initialize ():void { + + foreach ($this->bootstrappers as $bootstrapper) + $bootstrapper->bootstrap(); + + } + + /** + * ### Terminates registered components + * + * Attempts every terminator in reverse registration order and rethrows the + * first failure after all terminators have been attempted. + * @since 1.0.0 + * + * @uses \FireHub\Core\Boundary\Kernel\Terminator::terminate() To terminate component. + * @uses \FireHub\Runtime\Arr\Transform::reverse() Reverses an array. + * + * @throws Throwable If any terminator fails to terminate. + * + * @return void + */ + public function terminate ():void { + + $failure = null; + + foreach (Runtime\Arr\Transform::reverse($this->terminators) as $terminator) { + + try { + + $terminator->terminate(); + + } catch (Throwable $exception) { + + $failure ??= $exception; + + } + + } + + if ($failure !== null) throw $failure; + + } + +} \ No newline at end of file diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php deleted file mode 100644 index 2a6b9bb..0000000 --- a/tests/Unit/ApplicationTest.php +++ /dev/null @@ -1,198 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Unit; - -use FireHub\Testing\FireHubTestCase; -use FireHub\Core\Meta\Enum\Kernel\State; -use FireHub\Kernel\Application; -use FireHub\Kernel\ { - Bootstrapper, Terminator, - Application\Bootstrapper as ApplicationBootstrapper, Application\Terminator as ApplicationTerminator, -}; -use FireHub\Tests\Kernel\Stubs\ { - AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm -}; -use PHPUnit\Framework\Attributes\ { - CoversClass, Group, Small -}; - -/** - * ### Test application kernel lifecycle management - * @since 1.0.0 - */ -#[Small] -#[Group('application')] -#[CoversClass(Application::class)] -final class ApplicationTest extends FireHubTestCase { - - /** - * @since 1.0.0 - * - * @return void - */ - public function testInitialState ():void { - - self::assertSame(State::CREATED, new Application()->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testBoot ():void { - - $kernel = new Application(); - - $kernel->boot(); - - self::assertSame(State::BOOTED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testBootIsIdempotent ():void { - - $calls = 0; - $kernel = new Application(); - $kernel->registerBootstrapper(new BasicBootstrapper($calls)); - - $kernel->boot(); - $kernel->boot(); - - self::assertSame(1, $calls); - self::assertSame(State::BOOTED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testShutdown ():void { - - $kernel = new Application(); - $kernel->boot(); - $kernel->shutdown(); - - self::assertSame(State::TERMINATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testShutdownFromCreatedState ():void { - - $kernel = new Application(); - $kernel->shutdown(); - - self::assertSame(State::TERMINATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testShutdownIsIdempotent ():void { - - $calls = 0; - $kernel = new Application(); - $kernel->registerTerminator(new BasicTerminator($calls)); - - $kernel->boot(); - $kernel->shutdown(); - $kernel->shutdown(); - - self::assertSame(1, $calls); - self::assertSame(State::TERMINATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testGlobalAndApplicationBootstrappersRunInRegistrationOrder ():void { - - $events = []; - $kernel = new Application(); - - $kernel->registerBootstrapper(new EventBootTerm($events)); - - $kernel->registerBootstrapper(new AppEventBootTerm($events)); - - $kernel->boot(); - - self::assertSame(['global', 'application'], $events); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testGlobalAndApplicationTerminatorsRunInReverseRegistrationOrder ():void { - - $events = []; - $kernel = new Application(); - - $kernel->registerBootstrapper(new EventBootTerm($events)); - - $kernel->registerBootstrapper(new AppEventBootTerm($events)); - - $kernel->boot(); - $kernel->shutdown(); - - self::assertSame(['global', 'application'], $events); - - } - -} \ No newline at end of file diff --git a/tests/Unit/LifecycleTest.php b/tests/Unit/LifecycleTest.php new file mode 100644 index 0000000..56fa8dc --- /dev/null +++ b/tests/Unit/LifecycleTest.php @@ -0,0 +1,148 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Lifecycle; +use FireHub\Kernel\Exception\InvalidKernelStateException; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Test kernel lifecycle state transitions + * @since 1.0.0 + */ +#[Small] +#[Group('lifecycle')] +#[CoversClass(Lifecycle::class)] +final class LifecycleTest extends FireHubTestCase { + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitialState ():void { + + self::assertSame(State::CREATED, new Lifecycle()->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testBootTransitions ():void { + + $lifecycle = new Lifecycle(); + + $lifecycle->beginBoot(); + self::assertSame(State::BOOTING, $lifecycle->state()); + + $lifecycle->completeBoot(); + self::assertSame(State::BOOTED, $lifecycle->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testShutdownTransitions ():void { + + $lifecycle = new Lifecycle(); + + $lifecycle->beginShutdown(); + self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); + + $lifecycle->completeShutdown(); + self::assertSame(State::TERMINATED, $lifecycle->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testFailedBootCanBeShutDown ():void { + + $lifecycle = new Lifecycle(); + $lifecycle->beginBoot(); + $lifecycle->fail(); + + self::assertSame(State::FAILED, $lifecycle->state()); + + $lifecycle->beginShutdown(); + $lifecycle->completeShutdown(); + + self::assertSame(State::TERMINATED, $lifecycle->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testCannotBootTwice ():void { + + $lifecycle = new Lifecycle(); + $lifecycle->beginBoot(); + + $this->expectException(InvalidKernelStateException::class); + + $lifecycle->beginBoot(); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testCannotCompleteBootBeforeBeginning ():void { + + $this->expectException(InvalidKernelStateException::class); + + new Lifecycle()->completeBoot(); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testCannotFailWhenIdle ():void { + + $this->expectException(InvalidKernelStateException::class); + + new Lifecycle()->fail(); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Lifestyle/ControllerTest.php b/tests/Unit/Lifestyle/ControllerTest.php new file mode 100644 index 0000000..288c551 --- /dev/null +++ b/tests/Unit/Lifestyle/ControllerTest.php @@ -0,0 +1,321 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Lifecycle\Controller; +use FireHub\Kernel\Exception\InvalidKernelStateException; +use FireHub\Tests\Kernel\Stubs\ { + AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Test kernel lifecycle coordination + * @since 1.0.0 + */ +#[Small] +#[Group('lifecycle')] +#[CoversClass(Controller::class)] +final class ControllerTest extends FireHubTestCase { + + /** + * ### Tests the initial lifecycle state + * @since 1.0.0 + * + * @return void + */ + public function testInitialState ():void { + + $controller = new Controller(); + + self::assertSame(State::CREATED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBoot ():void { + + $controller = new Controller(); + + $controller->boot(); + + self::assertSame(State::BOOTED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdown ():void { + + $controller = new Controller(); + + $controller->boot(); + $controller->shutdown(); + + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownFromCreatedState ():void { + + $controller = new Controller(); + + $controller->shutdown(); + + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBootExecutesBootstrapper ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->registerBootstrapper(new BasicBootstrapper($calls)); + + $controller->boot(); + + self::assertSame(1, $calls); + self::assertSame(State::BOOTED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownExecutesTerminator ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->registerTerminator(new BasicTerminator($calls)); + + $controller->boot(); + $controller->shutdown(); + + self::assertSame(1, $calls); + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBootIsIdempotent ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->registerBootstrapper(new BasicBootstrapper($calls)); + + $controller->boot(); + $controller->boot(); + + self::assertSame(1, $calls); + self::assertSame(State::BOOTED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownIsIdempotent ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->registerTerminator(new BasicTerminator($calls)); + + $controller->boot(); + $controller->shutdown(); + $controller->shutdown(); + + self::assertSame(1, $calls); + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testLifecycleExecutionOrder ():void { + + $controller = new Controller(); + $events = []; + + $global = new EventBootTerm($events); + $application = new AppEventBootTerm($events); + + $controller->registerBootstrapper($global); + $controller->registerBootstrapper($application); + + $controller->registerTerminator($global); + $controller->registerTerminator($application); + + $controller->boot(); + $controller->shutdown(); + + self::assertSame([ + 'global', + 'application', + 'application', + 'global' + ], $events); + + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testCannotRegisterBootstrapperAfterBoot ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->boot(); + + $this->expectException(InvalidKernelStateException::class); + + $controller->registerBootstrapper(new BasicBootstrapper($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testCannotRegisterTerminatorAfterBoot ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->boot(); + + $this->expectException(InvalidKernelStateException::class); + + $controller->registerTerminator(new BasicTerminator($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testCannotRegisterAfterShutdown ():void { + + $controller = new Controller(); + $calls = 0; + + $controller->shutdown(); + + $this->expectException(InvalidKernelStateException::class); + + $controller->registerBootstrapper(new BasicBootstrapper($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testCannotBootAfterShutdown ():void { + + $controller = new Controller(); + + $controller->shutdown(); + + $this->expectException(InvalidKernelStateException::class); + + $controller->boot(); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Lifestyle/ManagerTest.php b/tests/Unit/Lifestyle/ManagerTest.php new file mode 100644 index 0000000..4d213eb --- /dev/null +++ b/tests/Unit/Lifestyle/ManagerTest.php @@ -0,0 +1,226 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit\Manager; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Kernel\Lifecycle\Manager; +use FireHub\Tests\Kernel\Stubs\ { + AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; +use Throwable; + +/** + * ### Test kernel lifecycle component execution + * @since 1.0.0 + */ +#[Small] +#[Group('lifecycle')] +#[CoversClass(Manager::class)] +final class ManagerTest extends FireHubTestCase { + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitializeWithoutBootstrappers ():void { + + $manager = new Manager(); + + $manager->initialize(); + + self::assertTrue(true); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testTerminateWithoutTerminators ():void { + + $manager = new Manager(); + + $manager->terminate(); + + self::assertTrue(true); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitializeExecutesBootstrapper ():void { + + $manager = new Manager(); + $calls = 0; + + $manager->registerBootstrapper(new BasicBootstrapper($calls)); + + $manager->initialize(); + + self::assertSame(1, $calls); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testTerminateExecutesTerminator ():void { + + $manager = new Manager(); + $calls = 0; + + $manager->registerTerminator(new BasicTerminator($calls)); + + $manager->terminate(); + + self::assertSame(1, $calls); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitializeExecutesAllBootstrappers ():void { + + $manager = new Manager(); + $calls = 0; + + $manager->registerBootstrapper(new BasicBootstrapper($calls)); + $manager->registerBootstrapper(new BasicBootstrapper($calls)); + $manager->registerBootstrapper(new BasicBootstrapper($calls)); + + $manager->initialize(); + + self::assertSame(3, $calls); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testTerminateExecutesAllTerminators ():void { + + $manager = new Manager(); + $calls = 0; + + $manager->registerTerminator(new BasicTerminator($calls)); + $manager->registerTerminator(new BasicTerminator($calls)); + $manager->registerTerminator(new BasicTerminator($calls)); + + $manager->terminate(); + + self::assertSame(3, $calls); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitializePreservesRegistrationOrder ():void { + + $manager = new Manager(); + $events = []; + + $manager->registerBootstrapper(new EventBootTerm($events)); + $manager->registerBootstrapper(new AppEventBootTerm($events)); + + $manager->initialize(); + + self::assertSame([ + 'global', + 'application' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testTerminateUsesReverseRegistrationOrder ():void { + + $manager = new Manager(); + $events = []; + + $manager->registerTerminator(new EventBootTerm($events)); + $manager->registerTerminator(new AppEventBootTerm($events)); + + $manager->terminate(); + + self::assertSame([ + 'application', + 'global' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testInitializeAndTerminate ():void { + + $manager = new Manager(); + $events = []; + + $global = new EventBootTerm($events); + $application = new AppEventBootTerm($events); + + $manager->registerBootstrapper($global); + $manager->registerBootstrapper($application); + + $manager->registerTerminator($global); + $manager->registerTerminator($application); + + $manager->initialize(); + $manager->terminate(); + + self::assertSame([ + 'global', + 'application', + 'application', + 'global' + ], $events); + + } + +} \ No newline at end of file From 14f8d94bc694c2ebf5fe495f785b82e5f93220cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 10:26:44 +0200 Subject: [PATCH 05/32] Add new dependencies to `composer.json` for extended functionality Updated the `composer.json` file to include additional dependencies: - **`ext-mbstring`**: Ensures multibyte string support is available for string operations. - **`the-firehub-project/foundation`**: Adds the Foundation package to align with ongoing kernel enhancements. These changes support compatibility and prepare the kernel for future extensions. --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 6614ee3..4e28abc 100644 --- a/composer.json +++ b/composer.json @@ -7,9 +7,11 @@ "license": "Apache-2.0", "require": { "php": "^8.5", + "ext-mbstring": "*", "the-firehub-project/runtime": ">=0.1.0 <1.0.0", - "the-firehub-project/core-standard": "dev-develop" + "the-firehub-project/core-standard": "dev-develop", + "the-firehub-project/foundation": "dev-develop" }, "require-dev": { "phpstan/phpstan": "^2.0", From 812fab2923c26eb59eff2796080dec1f71bee003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 10:56:24 +0200 Subject: [PATCH 06/32] Add `Application` components and their unit tests This commit introduces new components to manage application context and execution environments, as well as their associated tests: - **`Name.php`**: Adds an immutable, strongly-typed `Name` class for application identification. - **`Environment.php`**: Introduces an `Environment` enum to represent execution environments (production, staging, development, testing). - **`Context.php`**: Implements the `Context` class to encapsulate application metadata and environment in an immutable structure. Updates within the kernel integrate the `Context` component and provide a getter for application metadata. Other updates include: - Refactoring of lifecycle tests to align namespaces with related kernel classes. - New test classes for verifying application component behavior. These changes improve type safety, modularity, and testability of application metadata and environment management. --- src/Application.php | 22 +- src/Application/Context.php | 81 ++++ src/Application/Environment.php | 59 +++ src/Application/Name.php | 71 +++ tests/Unit/Application/ContextTest.php | 136 ++++++ tests/Unit/ApplicationTest.php | 416 ++++++++++++++++++ .../ControllerTest.php | 2 +- .../{Lifestyle => Lifecycle}/ManagerTest.php | 2 +- 8 files changed, 785 insertions(+), 4 deletions(-) create mode 100644 src/Application/Context.php create mode 100644 src/Application/Environment.php create mode 100644 src/Application/Name.php create mode 100644 tests/Unit/Application/ContextTest.php create mode 100644 tests/Unit/ApplicationTest.php rename tests/Unit/{Lifestyle => Lifecycle}/ControllerTest.php (99%) rename tests/Unit/{Lifestyle => Lifecycle}/ManagerTest.php (99%) diff --git a/src/Application.php b/src/Application.php index d765421..e03b7ab 100644 --- a/src/Application.php +++ b/src/Application.php @@ -16,7 +16,7 @@ use FireHub\Core\Boundary\Kernel\Application as ApplicationBoundary; use FireHub\Core\Meta\Enum\Kernel\State; use FireHub\Kernel\Application\ { - Bootstrapper as ApplicationBootstrapper, Terminator as ApplicationTerminator, + Bootstrapper as ApplicationBootstrapper, Context, Terminator as ApplicationTerminator, }; use FireHub\Kernel\Lifecycle\Controller; @@ -46,9 +46,15 @@ * ### Constructor * @since 1.0.0 * + * @param \FireHub\Kernel\Application\Context $context

+ * Application context. + *

+ * * @return void */ - public function __construct () { + public function __construct ( + private Context $context + ) { $this->lifecycle = new Controller; @@ -139,4 +145,16 @@ public function state ():State { } + /** + * ### Gets the application execution context + * @since 1.0.0 + * + * @return \FireHub\Kernel\Application\Context Application context. + */ + public function context ():Context { + + return $this->context; + + } + } \ No newline at end of file diff --git a/src/Application/Context.php b/src/Application/Context.php new file mode 100644 index 0000000..bfef5f6 --- /dev/null +++ b/src/Application/Context.php @@ -0,0 +1,81 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Kernel + */ + +namespace FireHub\Kernel\Application; + +use FireHub\Core\Type\ValueObject; + +/** + * ### Represents the application execution context + * + * Defines the immutable identity and execution environment of an application. + * + * The context contains application-wide metadata and remains unchanged throughout the application lifecycle. + * @since 1.0.0 + * + * @extends \FireHub\Core\Type\ValueObject, + * environment: \FireHub\Kernel\Application\Environment + * }> + */ +final readonly class Context extends ValueObject { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Kernel\Application\Name $name

+ * Application name. + *

+ * @param \FireHub\Kernel\Application\Environment $environment

+ * Application environment. + *

+ * + * @return void + */ + public function __construct ( + public Name $name, + public Environment $environment + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function value ():array { + + return [ + 'name' => $this->name, + 'environment' => $this->environment + ]; + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Application\Name::value() To get the name value. + */ + protected function comparisonValue ():array { + + return [ + 'name' => $this->name->value(), + 'environment' => $this->environment + ]; + + } + +} \ No newline at end of file diff --git a/src/Application/Environment.php b/src/Application/Environment.php new file mode 100644 index 0000000..2d058c1 --- /dev/null +++ b/src/Application/Environment.php @@ -0,0 +1,59 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Application; + +/** + * ### Defines the application execution environment + * + * Represents the environment in which a FireHub application is executed. + * + * Application environments allow the framework and its components to distinguish between production, staging, + * development, and testing configurations without relying on arbitrary string values. + * @since 1.0.0 + */ +enum Environment { + + /** + * ### Production environment + * + * Represents a live application environment intended for end users. + * @since 1.0.0 + */ + case PRODUCTION; + + /** + * ### Staging environment + * + * Represents a pre-production environment used for deployment verification and acceptance testing. + * @since 1.0.0 + */ + case STAGING ; + + /** + * ### Development environment + * + * Represents an environment used for application development, debugging, and local execution. + * @since 1.0.0 + */ + case DEVELOPMENT; + + /** + * ### Testing environment + * + * Represents an environment used for automated tests and application behavior verification. + * @since 1.0.0 + */ + case TESTING; + +} \ No newline at end of file diff --git a/src/Application/Name.php b/src/Application/Name.php new file mode 100644 index 0000000..d6d7358 --- /dev/null +++ b/src/Application/Name.php @@ -0,0 +1,71 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Kernel + */ + +namespace FireHub\Kernel\Application; + +use FireHub\Core\Type\Str\Encoding; +use FireHub\Foundation\Identifier\Name as IdentifierName; +use FireHub\Foundation\Identifier\Exception\InvalidIdentifierValueException; +use FireHub\Runtime; + +/** + * ### Represents an application name + * + * Defines an immutable, strongly typed name used to identify an application within the FireHub ecosystem. + * + * The application name inherits its fundamental validation rules and value semantics from the Foundation identifier + * name implementation. + * + * Application names are compared by their concrete type and underlying value, ensuring that they remain distinct + * from other identifier types. + * @since 1.0.0 + * + * @template TValue of non-empty-string + * + * @extends \FireHub\Foundation\Identifier\Name + */ +final readonly class Name extends IdentifierName { + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Core\Type\Identifier::guard() To enforce the invariant that an identifier name cannot be empty. + * + * @param TValue $value

+ * The identifier name. + *

+ * + * @throws \FireHub\Foundation\Identifier\Exception\InvalidIdentifierValueException If the identifier name is + * empty or does not meet the length requirements. + * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. + * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. + * + * @return void + */ + public function __construct (string $value) { + + parent::__construct($value); + + $length = Runtime\Str\MB\Inspection::length($value, Encoding::UTF_8); + + $this->guard( + fn() => $length >= 3 && $length <= 64, + fn() => new InvalidIdentifierValueException( + 'Application name must contain between 3 and 64 characters.' + ) + ); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Application/ContextTest.php b/tests/Unit/Application/ContextTest.php new file mode 100644 index 0000000..580f694 --- /dev/null +++ b/tests/Unit/Application/ContextTest.php @@ -0,0 +1,136 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit\Application; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Kernel\Application\ { + Context, Environment, Name +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Tests immutable application execution context + * @since 1.0.0 + */ +#[Small] +#[Group('application')] +#[CoversClass(Context::class)] +final class ApplicationContextTest extends FireHubTestCase { + + /** + * @since 1.0.0 + * + * @return void + */ + public function testProperties ():void { + + $name = new Name('FireHub'); + + $context = new Context( + name: $name, + environment: Environment::TESTING + ); + + self::assertSame($name, $context->name); + self::assertSame(Environment::TESTING, $context->environment); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testValue ():void { + + $name = new Name('FireHub'); + + $context = new Context( + name: $name, + environment: Environment::DEVELOPMENT + ); + + self::assertSame([ + 'name' => $name, + 'environment' => Environment::DEVELOPMENT + ], $context->value()); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testEquals ():void { + + $first = new Context( + new Name('FireHub'), + Environment::TESTING + ); + + $second = new Context( + new Name('FireHub'), + Environment::TESTING + ); + + self::assertTrue($first->equals($second)); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testDifferentNamesAreNotEqual ():void { + + $first = new Context( + new Name('FireHub'), + Environment::TESTING + ); + + $second = new Context( + new Name('OtherApplication'), + Environment::TESTING + ); + + self::assertFalse($first->equals($second)); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testDifferentEnvironmentsAreNotEqual ():void { + + $first = new Context( + new Name('FireHub'), + Environment::TESTING + ); + + $second = new Context( + new Name('FireHub'), + Environment::PRODUCTION + ); + + self::assertFalse($first->equals($second)); + + } + +} \ No newline at end of file diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php new file mode 100644 index 0000000..f99d6bf --- /dev/null +++ b/tests/Unit/ApplicationTest.php @@ -0,0 +1,416 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Application; +use FireHub\Kernel\Application\ { + Context, Environment, Name +}; +use FireHub\Kernel\Exception\InvalidKernelStateException; +use FireHub\Tests\Kernel\Stubs\ { + AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Tests application kernel lifecycle management + * @since 1.0.0 + */ +#[Small] +#[Group('application')] +#[CoversClass(Application::class)] +final class ApplicationTest extends FireHubTestCase { + + /** + * @since 1.0.0 + * + * @return void + */ + public function testInitialState ():void { + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + self::assertSame(State::CREATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @return void + */ + public function testContext ():void { + + $name = new Name('FireHub'); + + $context = new Context( + name: $name, + environment: Environment::TESTING + ); + + $kernel = new Application($context); + + self::assertSame($context, $kernel->context()); + + self::assertSame($name, $kernel->context()->name); + self::assertSame('FireHub', $kernel->context()->name->value()); + self::assertSame(Environment::TESTING, $kernel->context()->environment); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBoot ():void { + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->boot(); + + self::assertSame(State::BOOTED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBootIsIdempotent ():void { + + $calls = 0; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->registerBootstrapper(new BasicBootstrapper($calls)); + + $kernel->boot(); + $kernel->boot(); + + self::assertSame(1, $calls); + self::assertSame(State::BOOTED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdown ():void { + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->boot(); + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownFromCreatedState ():void { + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownIsIdempotent ():void { + + $calls = 0; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->registerTerminator(new BasicTerminator($calls)); + + $kernel->boot(); + $kernel->shutdown(); + $kernel->shutdown(); + + self::assertSame(1, $calls); + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testGlobalAndApplicationBootstrappersRunInRegistrationOrder ():void { + + $events = []; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->registerBootstrapper(new EventBootTerm($events)); + $kernel->registerBootstrapper(new AppEventBootTerm($events)); + + $kernel->boot(); + + self::assertSame([ + 'global', + 'application' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testGlobalAndApplicationTerminatorsRunInReverseRegistrationOrder ():void { + + $events = []; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->registerTerminator(new EventBootTerm($events)); + $kernel->registerTerminator(new AppEventBootTerm($events)); + + $kernel->boot(); + $kernel->shutdown(); + + self::assertSame([ + 'application', + 'global' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testCompleteLifecycle ():void { + + $events = []; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $global = new EventBootTerm($events); + $application = new AppEventBootTerm($events); + + $kernel->registerBootstrapper($global); + $kernel->registerBootstrapper($application); + + $kernel->registerTerminator($global); + $kernel->registerTerminator($application); + + self::assertSame(State::CREATED, $kernel->state()); + + $kernel->boot(); + + self::assertSame(State::BOOTED, $kernel->state()); + + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + self::assertSame([ + 'global', + 'application', + 'application', + 'global' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testCannotRegisterBootstrapperAfterBoot ():void { + + $calls = 0; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->boot(); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->registerBootstrapper(new BasicBootstrapper($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testCannotRegisterTerminatorAfterBoot ():void { + + $calls = 0; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->boot(); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->registerTerminator(new BasicTerminator($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testCannotRegisterBootstrapperAfterShutdown ():void { + + $calls = 0; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->shutdown(); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->registerBootstrapper(new BasicBootstrapper($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testCannotRegisterTerminatorAfterShutdown ():void { + + $calls = 0; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->shutdown(); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->registerTerminator(new BasicTerminator($calls)); + + } + + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testCannotBootAfterShutdown ():void { + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->shutdown(); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->boot(); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Lifestyle/ControllerTest.php b/tests/Unit/Lifecycle/ControllerTest.php similarity index 99% rename from tests/Unit/Lifestyle/ControllerTest.php rename to tests/Unit/Lifecycle/ControllerTest.php index 288c551..11d8061 100644 --- a/tests/Unit/Lifestyle/ControllerTest.php +++ b/tests/Unit/Lifecycle/ControllerTest.php @@ -11,7 +11,7 @@ * @package Kernel\Tests */ -namespace FireHub\Tests\Kernel\Unit; +namespace FireHub\Tests\Kernel\Unit\Lifecycle; use FireHub\Testing\FireHubTestCase; use FireHub\Core\Meta\Enum\Kernel\State; diff --git a/tests/Unit/Lifestyle/ManagerTest.php b/tests/Unit/Lifecycle/ManagerTest.php similarity index 99% rename from tests/Unit/Lifestyle/ManagerTest.php rename to tests/Unit/Lifecycle/ManagerTest.php index 4d213eb..8805624 100644 --- a/tests/Unit/Lifestyle/ManagerTest.php +++ b/tests/Unit/Lifecycle/ManagerTest.php @@ -11,7 +11,7 @@ * @package Kernel\Tests */ -namespace FireHub\Tests\Kernel\Unit\Manager; +namespace FireHub\Tests\Kernel\Unit\Lifecycle; use FireHub\Testing\FireHubTestCase; use FireHub\Kernel\Lifecycle\Manager; From 159255e7ed99d5e490614dd9371ca710795e844f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 12:33:09 +0200 Subject: [PATCH 07/32] Introduce ordered bootstrapper and terminator sequences for kernel lifecycle Added new `Bootstrapper\Sequence` and `Terminator\Sequence` components to manage execution order of kernel lifecycle operations. Key updates include: - **`Bootstrapper\Sequence.php`** and **`Terminator\Sequence.php`**: Implement classes to manage registration and execution of bootstrappers and terminators in ascending order based on their defined execution order. - **Kernel Lifecycle Integration**: Updated `Lifecycle\Manager`, `Lifecycle\Controller`, and `Application` to leverage the new sequence components for orderly execution. - **Backward Compatibility**: Refactored lifecycle handling to use ordered sequences while preserving existing functionality and signatures (e.g., allowing default `order = 0` for consistent behavior). - **Bootstrapper and Terminator Execution**: Ensured that the system gracefully handles exceptions during sequence execution, either halting or propagating them as appropriate. These changes enhance the flexibility and reliability of lifecycle operations, ensuring predictable component execution while maintaining fault tolerance. --- src/Application.php | 14 +++-- src/Application/Context.php | 5 ++ src/Bootstrapper/Sequence.php | 89 ++++++++++++++++++++++++++++ src/Lifecycle/Controller.php | 14 +++-- src/Lifecycle/Manager.php | 85 ++++++++++++++------------ src/Terminator/Sequence.php | 108 ++++++++++++++++++++++++++++++++++ 6 files changed, 268 insertions(+), 47 deletions(-) create mode 100644 src/Bootstrapper/Sequence.php create mode 100644 src/Terminator/Sequence.php diff --git a/src/Application.php b/src/Application.php index e03b7ab..cf03aee 100644 --- a/src/Application.php +++ b/src/Application.php @@ -69,14 +69,17 @@ public function __construct ( * @param \FireHub\Kernel\Bootstrapper|\FireHub\Kernel\Application\Bootstrapper $bootstrapper

* Bootstrapper to register. *

+ * @param int $order [optional]

+ * Execution order. + *

* * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. * * @return void */ - public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $bootstrapper):void { + public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $bootstrapper, int $order = 0):void { - $this->lifecycle->registerBootstrapper($bootstrapper); + $this->lifecycle->registerBootstrapper($bootstrapper, $order); } @@ -89,14 +92,17 @@ public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $boot * @param \FireHub\Kernel\Terminator|\FireHub\Kernel\Application\Terminator $terminator

* Terminator to register. *

+ * @param int $order [optional]

+ * Execution order. + *

* * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. * * @return void */ - public function registerTerminator (Terminator|ApplicationTerminator $terminator):void { + public function registerTerminator (Terminator|ApplicationTerminator $terminator, int $order = 0):void { - $this->lifecycle->registerTerminator($terminator); + $this->lifecycle->registerTerminator($terminator, $order); } diff --git a/src/Application/Context.php b/src/Application/Context.php index bfef5f6..2ddecff 100644 --- a/src/Application/Context.php +++ b/src/Application/Context.php @@ -68,6 +68,11 @@ public function value ():array { * @since 1.0.0 * * @uses \FireHub\Kernel\Application\Name::value() To get the name value. + * + * @return array{ + * name: non-empty-string, + * environment: \FireHub\Kernel\Application\Environment + * } Comparison value. */ protected function comparisonValue ():array { diff --git a/src/Bootstrapper/Sequence.php b/src/Bootstrapper/Sequence.php new file mode 100644 index 0000000..c82a144 --- /dev/null +++ b/src/Bootstrapper/Sequence.php @@ -0,0 +1,89 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Bootstrapper; + +use FireHub\Core\Boundary\Kernel\Bootstrapper; +use FireHub\Runtime; + +/** + * ### Manages the kernel bootstrap sequence + * + * Registers and executes bootstrap components according to their configured execution order. + * + * Components with lower order values execute first. Components with equal order values retain their original + * registration order. + * + * The sequence does not manage kernel lifecycle states. + * @since 1.0.0 + */ +final class Sequence { + + /** + * ### Registered bootstrappers + * @since 1.0.0 + * + * @var list + */ + private array $bootstrappers = []; + + /** + * ### Registers a bootstrapper + * @since 1.0.0 + * + * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

+ * Bootstrap component. + *

+ * @param int $order

+ * Execution order. + *

+ * + * @return void + */ + public function register (Bootstrapper $bootstrapper, int $order):void { + + $this->bootstrappers[] = [ + 'bootstrapper' => $bootstrapper, + 'order' => $order + ]; + + } + + /** + * ### Executes the bootstrap sequence + * + * Executes registered bootstrappers in ascending order. + * + * Execution stops immediately if a bootstrapper throws an exception. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Ordering::sortBy() Sorts an array by a user-defined comparison function. + * @uses \FireHub\Core\Boundary\Kernel\Bootstrapper::bootstrap() To bootstrap component. + * + * @return void + */ + public function execute ():void { + + $entries = $this->bootstrappers; + + Runtime\Arr\Ordering::sortBy( + $entries, + static fn (array $a, array $b):int => $a['order'] <=> $b['order'] + ); + + foreach ($entries as $entry) + $entry['bootstrapper']->bootstrap(); + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index d915198..7e93e1c 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -72,16 +72,19 @@ public function __construct () { * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

* Bootstrapper to register. *

+ * @param int $order

+ * Execution order. + *

* * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. * * @return void */ - public function registerBootstrapper (Bootstrapper $bootstrapper):void { + public function registerBootstrapper (Bootstrapper $bootstrapper, int $order):void { $this->assertRegistrationAllowed(); - $this->manager->registerBootstrapper($bootstrapper); + $this->manager->registerBootstrapper($bootstrapper, $order); } @@ -95,16 +98,19 @@ public function registerBootstrapper (Bootstrapper $bootstrapper):void { * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

* Terminator to register. *

+ * @param int $order

+ * Execution order. + *

* * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. * * @return void */ - public function registerTerminator (Terminator $terminator):void { + public function registerTerminator (Terminator $terminator, int $order):void { $this->assertRegistrationAllowed(); - $this->manager->registerTerminator($terminator); + $this->manager->registerTerminator($terminator, $order); } diff --git a/src/Lifecycle/Manager.php b/src/Lifecycle/Manager.php index f594ff2..863a175 100644 --- a/src/Lifecycle/Manager.php +++ b/src/Lifecycle/Manager.php @@ -7,7 +7,7 @@ * @copyright 2026-present The FireHub Project - All rights reserved * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 * - * @php-version >=8.1 + * @php-version >=8.5 * @package Kernel */ @@ -16,7 +16,8 @@ use FireHub\Core\Boundary\Kernel\ { Bootstrapper, Terminator }; -use FireHub\Runtime; +use FireHub\Kernel\Bootstrapper\Sequence as BootstrapperSequence; +use FireHub\Kernel\Terminator\Sequence as TerminatorSequence; use Throwable; /** @@ -31,34 +32,49 @@ final class Manager { /** - * ### Registered bootstrappers + * ### Registered bootstrappers in registration order + * @since 1.0.0 + */ + private BootstrapperSequence $bootstrap_sequence; + + /** + * ### Registered terminators in shutdown order * @since 1.0.0 - * - * @var list<\FireHub\Core\Boundary\Kernel\Bootstrapper> */ - private array $bootstrappers = []; + private TerminatorSequence $termination_sequence; /** - * ### Registered terminators + * ### Constructor * @since 1.0.0 * - * @var list<\FireHub\Core\Boundary\Kernel\Terminator> + * @return void */ - private array $terminators = []; + public function __construct () { + + $this->bootstrap_sequence = new BootstrapperSequence; + + $this->termination_sequence = new TerminatorSequence; + + } /** * ### Registers a bootstrapper * @since 1.0.0 * + * @uses \FireHub\Kernel\Bootstrapper\Sequence::register() To register bootstrapper. + * * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

* Bootstrapper to register. *

+ * @param int $order

+ * Execution order. + *

* * @return void */ - public function registerBootstrapper (Bootstrapper $bootstrapper):void { + public function registerBootstrapper (Bootstrapper $bootstrapper, int $order):void { - $this->bootstrappers[] = $bootstrapper; + $this->bootstrap_sequence->register($bootstrapper, $order); } @@ -66,68 +82,59 @@ public function registerBootstrapper (Bootstrapper $bootstrapper):void { * ### Registers a terminator * @since 1.0.0 * + * @uses \FireHub\Kernel\Terminator\Sequence::register() To register terminator. + * * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

* Terminator to register. *

+ * @param int $order

+ * Execution order. + *

* * @return void */ - public function registerTerminator (Terminator $terminator):void { + public function registerTerminator (Terminator $terminator, int $order):void { - $this->terminators[] = $terminator; + $this->termination_sequence->register($terminator, $order); } /** * ### Initializes registered components * - * Stops immediately if a bootstrapper fails, preserving its original exception. + * Executes the bootstrap sequence according to its configured execution order. + * + * Initialization stops immediately if a bootstrapper throws an exception. * @since 1.0.0 * - * @uses \FireHub\Core\Boundary\Kernel\Bootstrapper::bootstrap() To bootstrap component. + * @uses \FireHub\Kernel\Bootstrapper\Sequence::execute() To execute registered bootstrappers. * * @return void */ public function initialize ():void { - foreach ($this->bootstrappers as $bootstrapper) - $bootstrapper->bootstrap(); + $this->bootstrap_sequence->execute(); } /** * ### Terminates registered components * - * Attempts every terminator in reverse registration order and rethrows the - * first failure after all terminators have been attempted. + * Executes the termination sequence according to its configured execution order. + * + * The termination sequence is responsible for attempting all registered + * terminators and propagating execution failures. * @since 1.0.0 * - * @uses \FireHub\Core\Boundary\Kernel\Terminator::terminate() To terminate component. - * @uses \FireHub\Runtime\Arr\Transform::reverse() Reverses an array. + * @uses \FireHub\Kernel\Terminator\Sequence::execute() To execute registered terminators. * - * @throws Throwable If any terminator fails to terminate. + * @throws Throwable If a terminator fails. * * @return void */ public function terminate ():void { - $failure = null; - - foreach (Runtime\Arr\Transform::reverse($this->terminators) as $terminator) { - - try { - - $terminator->terminate(); - - } catch (Throwable $exception) { - - $failure ??= $exception; - - } - - } - - if ($failure !== null) throw $failure; + $this->termination_sequence->execute(); } diff --git a/src/Terminator/Sequence.php b/src/Terminator/Sequence.php new file mode 100644 index 0000000..05744c6 --- /dev/null +++ b/src/Terminator/Sequence.php @@ -0,0 +1,108 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Terminator; + +use FireHub\Core\Boundary\Kernel\Terminator; +use FireHub\Runtime; +use Throwable; + +/** + * ### Manages the kernel termination sequence + * + * Registers and executes termination components, according to their configured execution order. + * + * Components with lower order values execute first. Components with equal order values retain their original + * registration order. + * + * Termination continues when individual components fail, allowing remaining components to release their resources. + * @since 1.0.0 + */ +final class Sequence { + + /** + * ### Registered bootstrappers + * @since 1.0.0 + * + * @var list + */ + private array $terminators = []; + + /** + * ### Registers a terminator + * @since 1.0.0 + * + * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

+ * Termination component. + *

+ * @param int $order

+ * Execution order. + *

+ * + * @return void + */ + public function register (Terminator $terminator, int $order):void { + + $this->terminators[] = [ + 'terminator' => $terminator, + 'order' => $order + ]; + + } + + /** + * ### Executes the termination sequence + * + * Executes terminators in ascending order. + * + * Attempts to execute every registered terminator even when one fails. The first captured exception is rethrown + * after execution completes. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Ordering::sortBy() Sorts an array by a user-defined comparison function. + * @uses \FireHub\Core\Boundary\Kernel\Terminator::terminate() To terminate component. + * + * @throws Throwable If a terminator fails. + * + * @return void + */ + public function execute ():void { + + $entries = $this->terminators; + + Runtime\Arr\Ordering::sortBy( + $entries, + static fn (array $a, array $b):int => $a['order'] <=> $b['order'] + ); + + $failure = null; + + foreach ($entries as $entry) { + + try { + + $entry['terminator']->terminate(); + + } catch (Throwable $exception) { + + $failure ??= $exception; + + } + + } + + if ($failure !== null) throw $failure; + + } + +} \ No newline at end of file From ff0e18fe9da2558dd78fd1ed20d94b84b9f5933b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 12:51:04 +0200 Subject: [PATCH 08/32] Update tests for ordered bootstrapper and terminator execution in kernel lifecycle Enhanced unit tests across `Manager`, `Controller`, and `Application` to support and validate ordered execution of bootstrappers and terminators. Key updates include: - **Execution Order Support**: Modified test cases to register bootstrappers and terminators with explicit execution order, ensuring they execute in the expected sequence. - **New Test Scenarios**: Added `testInitializeUsesExecutionOrder` and `testTerminateUsesExecutionOrder` in `ManagerTest`, covering ascending and descending order handling. - **Default Order Validation**: Introduced `testDefaultExecutionOrder` in `ApplicationTest` to confirm backward compatibility with default (0) execution order. - **Refactored Registration**: Updated test bootstrapper and terminator registrations to include order parameters in multiple scenarios. These updates ensure correctness and reliability of the ordered execution mechanism while maintaining compatibility with existing functionality. --- tests/Unit/ApplicationTest.php | 40 ++++++++++--- tests/Unit/Lifecycle/ControllerTest.php | 22 +++---- tests/Unit/Lifecycle/ManagerTest.php | 80 ++++++++++++++++++++----- 3 files changed, 108 insertions(+), 34 deletions(-) diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php index f99d6bf..31194ee 100644 --- a/tests/Unit/ApplicationTest.php +++ b/tests/Unit/ApplicationTest.php @@ -228,7 +228,7 @@ public function testGlobalAndApplicationBootstrappersRunInRegistrationOrder ():v * * @return void */ - public function testGlobalAndApplicationTerminatorsRunInReverseRegistrationOrder ():void { + public function testGlobalAndApplicationTerminatorsRunInExecutionOrder ():void { $events = []; @@ -236,8 +236,8 @@ public function testGlobalAndApplicationTerminatorsRunInReverseRegistrationOrder new Context(new Name('FireHub'), Environment::TESTING) ); - $kernel->registerTerminator(new EventBootTerm($events)); - $kernel->registerTerminator(new AppEventBootTerm($events)); + $kernel->registerTerminator(new EventBootTerm($events), 100); + $kernel->registerTerminator(new AppEventBootTerm($events), -100); $kernel->boot(); $kernel->shutdown(); @@ -269,11 +269,11 @@ public function testCompleteLifecycle ():void { $global = new EventBootTerm($events); $application = new AppEventBootTerm($events); - $kernel->registerBootstrapper($global); - $kernel->registerBootstrapper($application); + $kernel->registerBootstrapper($global, -100); + $kernel->registerBootstrapper($application, 100); - $kernel->registerTerminator($global); - $kernel->registerTerminator($application); + $kernel->registerTerminator($global, 100); + $kernel->registerTerminator($application, -100); self::assertSame(State::CREATED, $kernel->state()); @@ -294,6 +294,32 @@ public function testCompleteLifecycle ():void { } + /** + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + */ + public function testDefaultExecutionOrder ():void { + + $events = []; + + $kernel = new Application( + new Context(new Name('FireHub'), Environment::TESTING) + ); + + $kernel->registerBootstrapper(new EventBootTerm($events)); + $kernel->registerBootstrapper(new AppEventBootTerm($events)); + + $kernel->boot(); + + self::assertSame([ + 'global', + 'application' + ], $events); + + } + /** * @since 1.0.0 * diff --git a/tests/Unit/Lifecycle/ControllerTest.php b/tests/Unit/Lifecycle/ControllerTest.php index 11d8061..2802904 100644 --- a/tests/Unit/Lifecycle/ControllerTest.php +++ b/tests/Unit/Lifecycle/ControllerTest.php @@ -116,7 +116,7 @@ public function testBootExecutesBootstrapper ():void { $controller = new Controller(); $calls = 0; - $controller->registerBootstrapper(new BasicBootstrapper($calls)); + $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); $controller->boot(); @@ -139,7 +139,7 @@ public function testShutdownExecutesTerminator ():void { $controller = new Controller(); $calls = 0; - $controller->registerTerminator(new BasicTerminator($calls)); + $controller->registerTerminator(new BasicTerminator($calls), 0); $controller->boot(); $controller->shutdown(); @@ -162,7 +162,7 @@ public function testBootIsIdempotent ():void { $controller = new Controller(); $calls = 0; - $controller->registerBootstrapper(new BasicBootstrapper($calls)); + $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); $controller->boot(); $controller->boot(); @@ -186,7 +186,7 @@ public function testShutdownIsIdempotent ():void { $controller = new Controller(); $calls = 0; - $controller->registerTerminator(new BasicTerminator($calls)); + $controller->registerTerminator(new BasicTerminator($calls), 0); $controller->boot(); $controller->shutdown(); @@ -214,11 +214,11 @@ public function testLifecycleExecutionOrder ():void { $global = new EventBootTerm($events); $application = new AppEventBootTerm($events); - $controller->registerBootstrapper($global); - $controller->registerBootstrapper($application); + $controller->registerBootstrapper($global, -100); + $controller->registerBootstrapper($application, 100); - $controller->registerTerminator($global); - $controller->registerTerminator($application); + $controller->registerTerminator($global, 100); + $controller->registerTerminator($application, -100); $controller->boot(); $controller->shutdown(); @@ -251,7 +251,7 @@ public function testCannotRegisterBootstrapperAfterBoot ():void { $this->expectException(InvalidKernelStateException::class); - $controller->registerBootstrapper(new BasicBootstrapper($calls)); + $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); } @@ -272,7 +272,7 @@ public function testCannotRegisterTerminatorAfterBoot ():void { $this->expectException(InvalidKernelStateException::class); - $controller->registerTerminator(new BasicTerminator($calls)); + $controller->registerTerminator(new BasicTerminator($calls), 0); } @@ -293,7 +293,7 @@ public function testCannotRegisterAfterShutdown ():void { $this->expectException(InvalidKernelStateException::class); - $controller->registerBootstrapper(new BasicBootstrapper($calls)); + $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); } diff --git a/tests/Unit/Lifecycle/ManagerTest.php b/tests/Unit/Lifecycle/ManagerTest.php index 8805624..83b4b4f 100644 --- a/tests/Unit/Lifecycle/ManagerTest.php +++ b/tests/Unit/Lifecycle/ManagerTest.php @@ -74,7 +74,7 @@ public function testInitializeExecutesBootstrapper ():void { $manager = new Manager(); $calls = 0; - $manager->registerBootstrapper(new BasicBootstrapper($calls)); + $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); $manager->initialize(); @@ -94,7 +94,7 @@ public function testTerminateExecutesTerminator ():void { $manager = new Manager(); $calls = 0; - $manager->registerTerminator(new BasicTerminator($calls)); + $manager->registerTerminator(new BasicTerminator($calls), 0); $manager->terminate(); @@ -112,9 +112,9 @@ public function testInitializeExecutesAllBootstrappers ():void { $manager = new Manager(); $calls = 0; - $manager->registerBootstrapper(new BasicBootstrapper($calls)); - $manager->registerBootstrapper(new BasicBootstrapper($calls)); - $manager->registerBootstrapper(new BasicBootstrapper($calls)); + $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); + $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); + $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); $manager->initialize(); @@ -134,9 +134,9 @@ public function testTerminateExecutesAllTerminators ():void { $manager = new Manager(); $calls = 0; - $manager->registerTerminator(new BasicTerminator($calls)); - $manager->registerTerminator(new BasicTerminator($calls)); - $manager->registerTerminator(new BasicTerminator($calls)); + $manager->registerTerminator(new BasicTerminator($calls), 0); + $manager->registerTerminator(new BasicTerminator($calls), 0); + $manager->registerTerminator(new BasicTerminator($calls), 0); $manager->terminate(); @@ -154,8 +154,8 @@ public function testInitializePreservesRegistrationOrder ():void { $manager = new Manager(); $events = []; - $manager->registerBootstrapper(new EventBootTerm($events)); - $manager->registerBootstrapper(new AppEventBootTerm($events)); + $manager->registerBootstrapper(new EventBootTerm($events), 0); + $manager->registerBootstrapper(new AppEventBootTerm($events), 0); $manager->initialize(); @@ -178,8 +178,56 @@ public function testTerminateUsesReverseRegistrationOrder ():void { $manager = new Manager(); $events = []; - $manager->registerTerminator(new EventBootTerm($events)); - $manager->registerTerminator(new AppEventBootTerm($events)); + $manager->registerTerminator(new EventBootTerm($events), 0); + $manager->registerTerminator(new AppEventBootTerm($events), 0); + + $manager->terminate(); + + self::assertSame([ + 'global', + 'application' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testInitializeUsesExecutionOrder ():void { + + $manager = new Manager(); + $events = []; + + $manager->registerBootstrapper(new EventBootTerm($events), 100); + $manager->registerBootstrapper(new AppEventBootTerm($events), -100); + + $manager->initialize(); + + self::assertSame([ + 'application', + 'global' + ], $events); + + } + + /** + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testTerminateUsesExecutionOrder ():void { + + $manager = new Manager(); + $events = []; + + $manager->registerTerminator(new EventBootTerm($events), 100); + $manager->registerTerminator(new AppEventBootTerm($events), -100); $manager->terminate(); @@ -205,11 +253,11 @@ public function testInitializeAndTerminate ():void { $global = new EventBootTerm($events); $application = new AppEventBootTerm($events); - $manager->registerBootstrapper($global); - $manager->registerBootstrapper($application); + $manager->registerBootstrapper($global, -100); + $manager->registerBootstrapper($application, 100); - $manager->registerTerminator($global); - $manager->registerTerminator($application); + $manager->registerTerminator($global, 100); + $manager->registerTerminator($application, -100); $manager->initialize(); $manager->terminate(); From cd09f4161b3bfd3b00640152a0dd815994510ed6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 13:11:08 +0200 Subject: [PATCH 09/32] Refactor lifecycle handling for improved state clarity and execution ordering - Renamed `FAILED` state to `BOOT_FAILED` and introduced `SHUTDOWN_FAILED` for precise representation of lifecycle failure contexts. - Updated conditional checks in `fail()` and `beginShutdown()` to align with refined state transitions. - Enhanced sequence execution in `Manager`, `Controller`, and `Application` to ensure terminators and bootstrappers execute strictly based on their configured execution order. - Refined lifecycle documentation to clarify shutdown behaviors, failure states, and component order handling across multiple classes. These changes provide cleaner state handling, predictable execution order for lifecycle components, and clearer documentation for maintainers. --- src/Application.php | 4 ++-- src/Lifecycle.php | 12 +++++++----- src/Lifecycle/Controller.php | 4 ++-- src/Lifecycle/Manager.php | 8 ++++---- src/Terminator/Sequence.php | 2 +- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Application.php b/src/Application.php index cf03aee..2d93eae 100644 --- a/src/Application.php +++ b/src/Application.php @@ -25,8 +25,8 @@ * * Implements the application kernel contract and coordinates application initialization and termination. * - * Supports global and application-specific bootstrappers and terminators. Bootstrappers are executed - * in registration order, while terminators are executed in reverse registration order. + * Supports global and application-specific bootstrappers and terminators. Components are executed according + * to their configured execution order, with lower order values executed first. * * Lifecycle components can only be registered before kernel initialization begins. * diff --git a/src/Lifecycle.php b/src/Lifecycle.php index 88a23fb..d88465d 100644 --- a/src/Lifecycle.php +++ b/src/Lifecycle.php @@ -88,7 +88,7 @@ public function completeBoot ():void { */ public function beginShutdown ():void { - if ($this->state !== State::CREATED && $this->state !== State::BOOTED && $this->state !== State::FAILED) + if ($this->state !== State::CREATED && $this->state !== State::BOOTED && $this->state !== State::BOOT_FAILED) throw new InvalidKernelStateException( 'Cannot begin kernel shutdown in the current state.', [ @@ -126,15 +126,17 @@ public function completeShutdown ():void { */ public function fail ():void { - if ($this->state !== State::BOOTING && $this->state !== State::SHUTTING_DOWN) - throw new InvalidKernelStateException( + $this->state = match ($this->state) { + State::BOOTING => State::BOOT_FAILED, + State::SHUTTING_DOWN => State::SHUTDOWN_FAILED, + default => throw new InvalidKernelStateException( 'Cannot mark the kernel as failed outside an active lifecycle operation.', [ 'state' => $this->state ] - ); + ) - $this->state = State::FAILED; + }; } diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index 7e93e1c..e20f476 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -160,9 +160,9 @@ public function boot ():void { * * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. * @uses \FireHub\Kernel\Lifecycle\Manager::terminate() To terminate. - * @uses \FireHub\Kernel\Lifecycle::beginShutdown() To begin booting. + * @uses \FireHub\Kernel\Lifecycle::beginShutdown() To begin shutdown. * @uses \FireHub\Kernel\Lifecycle::completeShutdown() To complete shutdown. - * @uses \FireHub\Kernel\Lifecycle::fail() To fail booting. + * @uses \FireHub\Kernel\Lifecycle::fail() To mark shutdown as failed. * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. * @throws \FireHub\Kernel\Exception\KernelShutdownException If a terminator fails. diff --git a/src/Lifecycle/Manager.php b/src/Lifecycle/Manager.php index 863a175..3984ad3 100644 --- a/src/Lifecycle/Manager.php +++ b/src/Lifecycle/Manager.php @@ -23,8 +23,8 @@ /** * ### Registers and executes kernel lifecycle components * - * Executes bootstrappers in registration order and terminators in reverse order. - * Shutdown attempts every terminator even if an earlier terminator fails. + * Executes bootstrappers and terminators according to their configured execution order. Components with equal order + * values retain their registration order. * * State validation belongs to the lifecycle controller, not this execution manager. * @since 1.0.0 @@ -122,8 +122,8 @@ public function initialize ():void { * * Executes the termination sequence according to its configured execution order. * - * The termination sequence is responsible for attempting all registered - * terminators and propagating execution failures. + * The termination sequence is responsible for attempting all registered terminators and propagating execution + * failures. * @since 1.0.0 * * @uses \FireHub\Kernel\Terminator\Sequence::execute() To execute registered terminators. diff --git a/src/Terminator/Sequence.php b/src/Terminator/Sequence.php index 05744c6..5fe56a6 100644 --- a/src/Terminator/Sequence.php +++ b/src/Terminator/Sequence.php @@ -31,7 +31,7 @@ final class Sequence { /** - * ### Registered bootstrappers + * ### Registered terminators * @since 1.0.0 * * @var list From 90c67a3e44b2b94444e54fd5fb7affc62456852c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 13:52:59 +0200 Subject: [PATCH 10/32] Mark `Manager` as `readonly` for stricter immutability Updated the `Manager` class to be explicitly marked as `readonly`, ensuring all properties within the class remain immutable once initialized. This change reinforces strict immutability principles and aligns with best practices for maintaining predictable state in lifecycle components. --- src/Lifecycle/Manager.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lifecycle/Manager.php b/src/Lifecycle/Manager.php index 3984ad3..984fc32 100644 --- a/src/Lifecycle/Manager.php +++ b/src/Lifecycle/Manager.php @@ -29,7 +29,7 @@ * State validation belongs to the lifecycle controller, not this execution manager. * @since 1.0.0 */ -final class Manager { +final readonly class Manager { /** * ### Registered bootstrappers in registration order From 5e6127d3a72b34f80be764d79b258f7a1fbbdf52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 09:22:35 +0200 Subject: [PATCH 11/32] Update `LifecycleTest` to reflect renamed `BOOT_FAILED` state Revised the assertion in `LifecycleTest` to validate the newly renamed `BOOT_FAILED` state, replacing the outdated `FAILED` state. This update ensures test consistency with recent changes to lifecycle state definitions and improves clarity in failure scenarios. --- tests/Unit/LifecycleTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Unit/LifecycleTest.php b/tests/Unit/LifecycleTest.php index 56fa8dc..2af1a83 100644 --- a/tests/Unit/LifecycleTest.php +++ b/tests/Unit/LifecycleTest.php @@ -92,7 +92,7 @@ public function testFailedBootCanBeShutDown ():void { $lifecycle->beginBoot(); $lifecycle->fail(); - self::assertSame(State::FAILED, $lifecycle->state()); + self::assertSame(State::BOOT_FAILED, $lifecycle->state()); $lifecycle->beginShutdown(); $lifecycle->completeShutdown(); From 2cae0445caa62199c235a16242bbac2e31cadb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 16 Oct 2026 09:07:07 +0200 Subject: [PATCH 12/32] Remove `Application` namespace and decouple kernel lifecycle from application context Revised kernel implementation to remove the `Application` namespace and entirely decouple lifecycle management from application-specific components. Key changes include: - **Eliminated `Application` Components**: Removed `Bootstrapper`, `Context`, `Environment`, `Name`, and `Terminator` classes, simplifying kernel functionality. - **Renamed `Application` to `Kernel`**: Updated the main class to focus solely on kernel lifecycle, removing dependency on the application context. - **Simplified Lifecycle Logic**: Refactored bootstrapper and terminator registration to exclude `Application` qualifiers, reducing complexity and improving clarity. - **Namespace Adjustments**: Consolidated relevant kernel lifecycle contracts under the `Kernel` namespace instead of `Kernel\Application`. This update streamlines the kernel by focusing on its core functionality, removing unnecessary dependencies while preserving backward compatibility for lifecycle executions. --- src/Application/Bootstrapper.php | 28 ---------- src/Application/Context.php | 86 ----------------------------- src/Application/Environment.php | 59 -------------------- src/Application/Name.php | 71 ------------------------ src/Application/Terminator.php | 28 ---------- src/{Application.php => Kernel.php} | 51 +++++------------ 6 files changed, 15 insertions(+), 308 deletions(-) delete mode 100644 src/Application/Bootstrapper.php delete mode 100644 src/Application/Context.php delete mode 100644 src/Application/Environment.php delete mode 100644 src/Application/Name.php delete mode 100644 src/Application/Terminator.php rename src/{Application.php => Kernel.php} (64%) diff --git a/src/Application/Bootstrapper.php b/src/Application/Bootstrapper.php deleted file mode 100644 index 681827b..0000000 --- a/src/Application/Bootstrapper.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=7.0 - * @package Kernel - */ - -namespace FireHub\Kernel\Application; - -use FireHub\Core\Boundary\Kernel\Bootstrapper as BootstrapperBoundary; - -/** - * ### Defines an application kernel bootstrapper - * - * Establishes a bootstrapper contract for components that participate specifically in - * application kernel initialization. - * - * Application bootstrappers provide application-specific initialization behavior without - * requiring the kernel to depend on concrete component implementations. - * @since 1.0.0 - */ -interface Bootstrapper extends BootstrapperBoundary {} \ No newline at end of file diff --git a/src/Application/Context.php b/src/Application/Context.php deleted file mode 100644 index 2ddecff..0000000 --- a/src/Application/Context.php +++ /dev/null @@ -1,86 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.2 - * @package Kernel - */ - -namespace FireHub\Kernel\Application; - -use FireHub\Core\Type\ValueObject; - -/** - * ### Represents the application execution context - * - * Defines the immutable identity and execution environment of an application. - * - * The context contains application-wide metadata and remains unchanged throughout the application lifecycle. - * @since 1.0.0 - * - * @extends \FireHub\Core\Type\ValueObject, - * environment: \FireHub\Kernel\Application\Environment - * }> - */ -final readonly class Context extends ValueObject { - - /** - * ### Constructor - * @since 1.0.0 - * - * @param \FireHub\Kernel\Application\Name $name

- * Application name. - *

- * @param \FireHub\Kernel\Application\Environment $environment

- * Application environment. - *

- * - * @return void - */ - public function __construct ( - public Name $name, - public Environment $environment - ) {} - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function value ():array { - - return [ - 'name' => $this->name, - 'environment' => $this->environment - ]; - - } - - /** - * @inheritDoc - * - * @since 1.0.0 - * - * @uses \FireHub\Kernel\Application\Name::value() To get the name value. - * - * @return array{ - * name: non-empty-string, - * environment: \FireHub\Kernel\Application\Environment - * } Comparison value. - */ - protected function comparisonValue ():array { - - return [ - 'name' => $this->name->value(), - 'environment' => $this->environment - ]; - - } - -} \ No newline at end of file diff --git a/src/Application/Environment.php b/src/Application/Environment.php deleted file mode 100644 index 2d058c1..0000000 --- a/src/Application/Environment.php +++ /dev/null @@ -1,59 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel - */ - -namespace FireHub\Kernel\Application; - -/** - * ### Defines the application execution environment - * - * Represents the environment in which a FireHub application is executed. - * - * Application environments allow the framework and its components to distinguish between production, staging, - * development, and testing configurations without relying on arbitrary string values. - * @since 1.0.0 - */ -enum Environment { - - /** - * ### Production environment - * - * Represents a live application environment intended for end users. - * @since 1.0.0 - */ - case PRODUCTION; - - /** - * ### Staging environment - * - * Represents a pre-production environment used for deployment verification and acceptance testing. - * @since 1.0.0 - */ - case STAGING ; - - /** - * ### Development environment - * - * Represents an environment used for application development, debugging, and local execution. - * @since 1.0.0 - */ - case DEVELOPMENT; - - /** - * ### Testing environment - * - * Represents an environment used for automated tests and application behavior verification. - * @since 1.0.0 - */ - case TESTING; - -} \ No newline at end of file diff --git a/src/Application/Name.php b/src/Application/Name.php deleted file mode 100644 index d6d7358..0000000 --- a/src/Application/Name.php +++ /dev/null @@ -1,71 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.2 - * @package Kernel - */ - -namespace FireHub\Kernel\Application; - -use FireHub\Core\Type\Str\Encoding; -use FireHub\Foundation\Identifier\Name as IdentifierName; -use FireHub\Foundation\Identifier\Exception\InvalidIdentifierValueException; -use FireHub\Runtime; - -/** - * ### Represents an application name - * - * Defines an immutable, strongly typed name used to identify an application within the FireHub ecosystem. - * - * The application name inherits its fundamental validation rules and value semantics from the Foundation identifier - * name implementation. - * - * Application names are compared by their concrete type and underlying value, ensuring that they remain distinct - * from other identifier types. - * @since 1.0.0 - * - * @template TValue of non-empty-string - * - * @extends \FireHub\Foundation\Identifier\Name - */ -final readonly class Name extends IdentifierName { - - /** - * ### Constructor - * @since 1.0.0 - * - * @uses \FireHub\Core\Type\Identifier::guard() To enforce the invariant that an identifier name cannot be empty. - * - * @param TValue $value

- * The identifier name. - *

- * - * @throws \FireHub\Foundation\Identifier\Exception\InvalidIdentifierValueException If the identifier name is - * empty or does not meet the length requirements. - * @throws \FireHub\Core\Exception\FireHubException If the condition is not met. - * @throws \FireHub\Core\Type\Exception\ValueObjectException If the exception is not a FireHubException. - * - * @return void - */ - public function __construct (string $value) { - - parent::__construct($value); - - $length = Runtime\Str\MB\Inspection::length($value, Encoding::UTF_8); - - $this->guard( - fn() => $length >= 3 && $length <= 64, - fn() => new InvalidIdentifierValueException( - 'Application name must contain between 3 and 64 characters.' - ) - ); - - } - -} \ No newline at end of file diff --git a/src/Application/Terminator.php b/src/Application/Terminator.php deleted file mode 100644 index 07f369d..0000000 --- a/src/Application/Terminator.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=7.0 - * @package Kernel - */ - -namespace FireHub\Kernel\Application; - -use FireHub\Core\Boundary\Kernel\Terminator as TerminatorBoundary; - -/** - * ### Defines an application kernel terminator - * - * Establishes a termination contract for components that participate specifically in - * application kernel shutdown. - * - * Application terminators provide application-specific cleanup behavior without requiring - * the kernel to depend on concrete component implementations. - * @since 1.0.0 - */ -interface Terminator extends TerminatorBoundary {} \ No newline at end of file diff --git a/src/Application.php b/src/Kernel.php similarity index 64% rename from src/Application.php rename to src/Kernel.php index 2d93eae..6378832 100644 --- a/src/Application.php +++ b/src/Kernel.php @@ -13,20 +13,17 @@ namespace FireHub\Kernel; -use FireHub\Core\Boundary\Kernel\Application as ApplicationBoundary; +use FireHub\Core\Boundary\Kernel as KernelBoundary; use FireHub\Core\Meta\Enum\Kernel\State; -use FireHub\Kernel\Application\ { - Bootstrapper as ApplicationBootstrapper, Context, Terminator as ApplicationTerminator, -}; use FireHub\Kernel\Lifecycle\Controller; /** - * ### Manages the application kernel lifecycle + * ### Manages the kernel lifecycle * - * Implements the application kernel contract and coordinates application initialization and termination. + * Implements the kernel contract and coordinates runtime initialization and termination. * - * Supports global and application-specific bootstrappers and terminators. Components are executed according - * to their configured execution order, with lower order values executed first. + * Supports registration of bootstrappers and terminators, which are executed according to their configured + * execution order, with lower order values executed first. * * Lifecycle components can only be registered before kernel initialization begins. * @@ -34,10 +31,10 @@ * A failed kernel cannot be rebooted, but shutdown can be attempted to release resources. * @since 1.0.0 */ -final readonly class Application implements ApplicationBoundary { +final readonly class Kernel implements KernelBoundary { /** - * ### Application lifecycle controller + * ### Kernel lifecycle controller * @since 1.0.0 */ private Controller $lifecycle; @@ -46,27 +43,21 @@ * ### Constructor * @since 1.0.0 * - * @param \FireHub\Kernel\Application\Context $context

- * Application context. - *

- * * @return void */ - public function __construct ( - private Context $context - ) { + public function __construct () { $this->lifecycle = new Controller; } /** - * ### Registers a global or application-specific bootstrapper + * ### Registers a kernel bootstrapper * @since 1.0.0 * * @uses \FireHub\Kernel\Lifecycle\Controller::registerBootstrapper() To register a bootstrapper. * - * @param \FireHub\Kernel\Bootstrapper|\FireHub\Kernel\Application\Bootstrapper $bootstrapper

+ * @param \FireHub\Kernel\Bootstrapper $bootstrapper

* Bootstrapper to register. *

* @param int $order [optional]

@@ -77,19 +68,19 @@ public function __construct ( * * @return void */ - public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $bootstrapper, int $order = 0):void { + public function registerBootstrapper (Bootstrapper $bootstrapper, int $order = 0):void { $this->lifecycle->registerBootstrapper($bootstrapper, $order); } /** - * ### Registers a global or application-specific terminator + * ### Registers a kernel terminator * @since 1.0.0 * * @uses \FireHub\Kernel\Lifecycle\Controller::registerTerminator() To register a terminator. * - * @param \FireHub\Kernel\Terminator|\FireHub\Kernel\Application\Terminator $terminator

+ * @param \FireHub\Kernel\Terminator $terminator

* Terminator to register. *

* @param int $order [optional]

@@ -100,7 +91,7 @@ public function registerBootstrapper (Bootstrapper|ApplicationBootstrapper $boot * * @return void */ - public function registerTerminator (Terminator|ApplicationTerminator $terminator, int $order = 0):void { + public function registerTerminator (Terminator $terminator, int $order = 0):void { $this->lifecycle->registerTerminator($terminator, $order); @@ -127,7 +118,7 @@ public function boot ():void { * * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Controller::shutdown() To shutdown the kernel. + * @uses \FireHub\Kernel\Lifecycle\Controller::shutdown() To shut down the kernel. * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. * @throws \FireHub\Kernel\Exception\KernelShutdownException If termination fails. @@ -151,16 +142,4 @@ public function state ():State { } - /** - * ### Gets the application execution context - * @since 1.0.0 - * - * @return \FireHub\Kernel\Application\Context Application context. - */ - public function context ():Context { - - return $this->context; - - } - } \ No newline at end of file From 851b4dfbbc2aeeed003e3b2a93d5e1ab55a14f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 16 Oct 2026 10:34:23 +0200 Subject: [PATCH 13/32] Remove application-focused kernel stubs and tests; add sequence-based lifecycle tests Replaced outdated application-focused kernel stubs (`AppEventBootTerm`, `EventBootTerm`) and unit tests (`ApplicationContextTest`) with new sequence-based tests for bootstrappers and terminators. Key updates: - **Removed Stubs and Tests**: Removed application-specific stubs and `ApplicationContextTest` to align with the decoupled kernel lifecycle. - **Sequence Tests**: Added comprehensive unit tests (`SequenceTest`, `ManagerTest`) to validate the ordered execution of bootstrappers and terminators. - **Test Enhancements**: Updated `LifecycleTest` with new scenarios for invalid transitions, lifecycle completion, and failure handling, ensuring compatibility with sequence-based execution. This commit simplifies the testing structure, focusing on kernel lifecycle behavior and emphasizing ordered component execution. --- src/Bootstrapper/Sequence.php | 2 + src/Lifecycle/Controller.php | 3 - src/Terminator/Sequence.php | 2 + tests/Stubs/AppEventBootTerm.php | 53 --- tests/Stubs/BasicBootstrapper.php | 9 +- tests/Stubs/BasicTerminator.php | 9 +- tests/Stubs/EventBootTerm.php | 53 --- tests/Unit/Application/ContextTest.php | 136 ------- tests/Unit/ApplicationTest.php | 442 ---------------------- tests/Unit/Bootstrapper/SequenceTest.php | 158 ++++++++ tests/Unit/KernelTest.php | 462 +++++++++++++++++++++++ tests/Unit/Lifecycle/ControllerTest.php | 363 ++++++++++++++---- tests/Unit/Lifecycle/ManagerTest.php | 211 +++++------ tests/Unit/LifecycleTest.php | 301 +++++++++++++-- tests/Unit/Terminator/SequenceTest.php | 328 ++++++++++++++++ 15 files changed, 1629 insertions(+), 903 deletions(-) delete mode 100644 tests/Stubs/AppEventBootTerm.php delete mode 100644 tests/Stubs/EventBootTerm.php delete mode 100644 tests/Unit/Application/ContextTest.php delete mode 100644 tests/Unit/ApplicationTest.php create mode 100644 tests/Unit/Bootstrapper/SequenceTest.php create mode 100644 tests/Unit/KernelTest.php create mode 100644 tests/Unit/Terminator/SequenceTest.php diff --git a/src/Bootstrapper/Sequence.php b/src/Bootstrapper/Sequence.php index c82a144..62ecca2 100644 --- a/src/Bootstrapper/Sequence.php +++ b/src/Bootstrapper/Sequence.php @@ -26,6 +26,8 @@ * * The sequence does not manage kernel lifecycle states. * @since 1.0.0 + * + * @internal */ final class Sequence { diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index e20f476..48f2253 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -29,9 +29,6 @@ * * Owns the lifecycle state machine and component execution manager. Validates * registration and orchestrates boot and shutdown operations. - * - * The controller is kernel-type agnostic. Application-specific registration - * restrictions are enforced by the public Application facade. * @since 1.0.0 */ final readonly class Controller implements Kernel { diff --git a/src/Terminator/Sequence.php b/src/Terminator/Sequence.php index 5fe56a6..169532f 100644 --- a/src/Terminator/Sequence.php +++ b/src/Terminator/Sequence.php @@ -27,6 +27,8 @@ * * Termination continues when individual components fail, allowing remaining components to release their resources. * @since 1.0.0 + * + * @internal */ final class Sequence { diff --git a/tests/Stubs/AppEventBootTerm.php b/tests/Stubs/AppEventBootTerm.php deleted file mode 100644 index 4fc8fdf..0000000 --- a/tests/Stubs/AppEventBootTerm.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=7.0 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Stubs; - -use FireHub\Kernel\Application\ { - Bootstrapper, Terminator -}; - -/** - * ### Application event boot and term stub - * @since 1.0.0 - */ -class AppEventBootTerm implements Bootstrapper, Terminator { - - /** - * @since 1.0.0 - */ - public function __construct (private array &$events) {} - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function bootstrap ():void { - - $this->events[] = 'application'; - - } - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function terminate ():void { - - $this->events[] = 'application'; - - } - -} \ No newline at end of file diff --git a/tests/Stubs/BasicBootstrapper.php b/tests/Stubs/BasicBootstrapper.php index bd48e6d..66e632f 100644 --- a/tests/Stubs/BasicBootstrapper.php +++ b/tests/Stubs/BasicBootstrapper.php @@ -23,8 +23,13 @@ class BasicBootstrapper implements Bootstrapper { /** * @since 1.0.0 + * + * @return void */ - public function __construct (private int &$calls) {} + public function __construct ( + private readonly mixed $data, + private array &$executed + ) {} /** * @inheritDoc @@ -33,7 +38,7 @@ public function __construct (private int &$calls) {} */ public function bootstrap ():void { - $this->calls++; + $this->executed[] = $this->data; } diff --git a/tests/Stubs/BasicTerminator.php b/tests/Stubs/BasicTerminator.php index 48b97cb..551a135 100644 --- a/tests/Stubs/BasicTerminator.php +++ b/tests/Stubs/BasicTerminator.php @@ -23,8 +23,13 @@ class BasicTerminator implements Terminator { /** * @since 1.0.0 + * + * @return void */ - public function __construct (private int &$calls) {} + public function __construct ( + private readonly mixed $data, + private array &$executed + ) {} /** * @inheritDoc @@ -33,7 +38,7 @@ public function __construct (private int &$calls) {} */ public function terminate ():void { - $this->calls++; + $this->executed[] = $this->data; } diff --git a/tests/Stubs/EventBootTerm.php b/tests/Stubs/EventBootTerm.php deleted file mode 100644 index ce7c1f3..0000000 --- a/tests/Stubs/EventBootTerm.php +++ /dev/null @@ -1,53 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=7.0 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Stubs; - -use FireHub\Kernel\ { - Bootstrapper, Terminator -}; - -/** - * ### Event boot and term stub - * @since 1.0.0 - */ -class EventBootTerm implements Bootstrapper, Terminator { - - /** - * @since 1.0.0 - */ - public function __construct (private array &$events) {} - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function bootstrap ():void { - - $this->events[] = 'global'; - - } - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function terminate ():void { - - $this->events[] = 'global'; - - } - -} \ No newline at end of file diff --git a/tests/Unit/Application/ContextTest.php b/tests/Unit/Application/ContextTest.php deleted file mode 100644 index 580f694..0000000 --- a/tests/Unit/Application/ContextTest.php +++ /dev/null @@ -1,136 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Unit\Application; - -use FireHub\Testing\FireHubTestCase; -use FireHub\Kernel\Application\ { - Context, Environment, Name -}; -use PHPUnit\Framework\Attributes\ { - CoversClass, Group, Small -}; - -/** - * ### Tests immutable application execution context - * @since 1.0.0 - */ -#[Small] -#[Group('application')] -#[CoversClass(Context::class)] -final class ApplicationContextTest extends FireHubTestCase { - - /** - * @since 1.0.0 - * - * @return void - */ - public function testProperties ():void { - - $name = new Name('FireHub'); - - $context = new Context( - name: $name, - environment: Environment::TESTING - ); - - self::assertSame($name, $context->name); - self::assertSame(Environment::TESTING, $context->environment); - - } - - /** - * @since 1.0.0 - * - * @return void - */ - public function testValue ():void { - - $name = new Name('FireHub'); - - $context = new Context( - name: $name, - environment: Environment::DEVELOPMENT - ); - - self::assertSame([ - 'name' => $name, - 'environment' => Environment::DEVELOPMENT - ], $context->value()); - - } - - /** - * @since 1.0.0 - * - * @return void - */ - public function testEquals ():void { - - $first = new Context( - new Name('FireHub'), - Environment::TESTING - ); - - $second = new Context( - new Name('FireHub'), - Environment::TESTING - ); - - self::assertTrue($first->equals($second)); - - } - - /** - * @since 1.0.0 - * - * @return void - */ - public function testDifferentNamesAreNotEqual ():void { - - $first = new Context( - new Name('FireHub'), - Environment::TESTING - ); - - $second = new Context( - new Name('OtherApplication'), - Environment::TESTING - ); - - self::assertFalse($first->equals($second)); - - } - - /** - * @since 1.0.0 - * - * @return void - */ - public function testDifferentEnvironmentsAreNotEqual ():void { - - $first = new Context( - new Name('FireHub'), - Environment::TESTING - ); - - $second = new Context( - new Name('FireHub'), - Environment::PRODUCTION - ); - - self::assertFalse($first->equals($second)); - - } - -} \ No newline at end of file diff --git a/tests/Unit/ApplicationTest.php b/tests/Unit/ApplicationTest.php deleted file mode 100644 index 31194ee..0000000 --- a/tests/Unit/ApplicationTest.php +++ /dev/null @@ -1,442 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Unit; - -use FireHub\Testing\FireHubTestCase; -use FireHub\Core\Meta\Enum\Kernel\State; -use FireHub\Kernel\Application; -use FireHub\Kernel\Application\ { - Context, Environment, Name -}; -use FireHub\Kernel\Exception\InvalidKernelStateException; -use FireHub\Tests\Kernel\Stubs\ { - AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm -}; -use PHPUnit\Framework\Attributes\ { - CoversClass, Group, Small -}; - -/** - * ### Tests application kernel lifecycle management - * @since 1.0.0 - */ -#[Small] -#[Group('application')] -#[CoversClass(Application::class)] -final class ApplicationTest extends FireHubTestCase { - - /** - * @since 1.0.0 - * - * @return void - */ - public function testInitialState ():void { - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - self::assertSame(State::CREATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @return void - */ - public function testContext ():void { - - $name = new Name('FireHub'); - - $context = new Context( - name: $name, - environment: Environment::TESTING - ); - - $kernel = new Application($context); - - self::assertSame($context, $kernel->context()); - - self::assertSame($name, $kernel->context()->name); - self::assertSame('FireHub', $kernel->context()->name->value()); - self::assertSame(Environment::TESTING, $kernel->context()->environment); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testBoot ():void { - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->boot(); - - self::assertSame(State::BOOTED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testBootIsIdempotent ():void { - - $calls = 0; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->registerBootstrapper(new BasicBootstrapper($calls)); - - $kernel->boot(); - $kernel->boot(); - - self::assertSame(1, $calls); - self::assertSame(State::BOOTED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testShutdown ():void { - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->boot(); - $kernel->shutdown(); - - self::assertSame(State::TERMINATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testShutdownFromCreatedState ():void { - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->shutdown(); - - self::assertSame(State::TERMINATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testShutdownIsIdempotent ():void { - - $calls = 0; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->registerTerminator(new BasicTerminator($calls)); - - $kernel->boot(); - $kernel->shutdown(); - $kernel->shutdown(); - - self::assertSame(1, $calls); - self::assertSame(State::TERMINATED, $kernel->state()); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testGlobalAndApplicationBootstrappersRunInRegistrationOrder ():void { - - $events = []; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->registerBootstrapper(new EventBootTerm($events)); - $kernel->registerBootstrapper(new AppEventBootTerm($events)); - - $kernel->boot(); - - self::assertSame([ - 'global', - 'application' - ], $events); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testGlobalAndApplicationTerminatorsRunInExecutionOrder ():void { - - $events = []; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->registerTerminator(new EventBootTerm($events), 100); - $kernel->registerTerminator(new AppEventBootTerm($events), -100); - - $kernel->boot(); - $kernel->shutdown(); - - self::assertSame([ - 'application', - 'global' - ], $events); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testCompleteLifecycle ():void { - - $events = []; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $global = new EventBootTerm($events); - $application = new AppEventBootTerm($events); - - $kernel->registerBootstrapper($global, -100); - $kernel->registerBootstrapper($application, 100); - - $kernel->registerTerminator($global, 100); - $kernel->registerTerminator($application, -100); - - self::assertSame(State::CREATED, $kernel->state()); - - $kernel->boot(); - - self::assertSame(State::BOOTED, $kernel->state()); - - $kernel->shutdown(); - - self::assertSame(State::TERMINATED, $kernel->state()); - - self::assertSame([ - 'global', - 'application', - 'application', - 'global' - ], $events); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - */ - public function testDefaultExecutionOrder ():void { - - $events = []; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->registerBootstrapper(new EventBootTerm($events)); - $kernel->registerBootstrapper(new AppEventBootTerm($events)); - - $kernel->boot(); - - self::assertSame([ - 'global', - 'application' - ], $events); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testCannotRegisterBootstrapperAfterBoot ():void { - - $calls = 0; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->boot(); - - $this->expectException(InvalidKernelStateException::class); - - $kernel->registerBootstrapper(new BasicBootstrapper($calls)); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * - * @return void - */ - public function testCannotRegisterTerminatorAfterBoot ():void { - - $calls = 0; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->boot(); - - $this->expectException(InvalidKernelStateException::class); - - $kernel->registerTerminator(new BasicTerminator($calls)); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testCannotRegisterBootstrapperAfterShutdown ():void { - - $calls = 0; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->shutdown(); - - $this->expectException(InvalidKernelStateException::class); - - $kernel->registerBootstrapper(new BasicBootstrapper($calls)); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testCannotRegisterTerminatorAfterShutdown ():void { - - $calls = 0; - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->shutdown(); - - $this->expectException(InvalidKernelStateException::class); - - $kernel->registerTerminator(new BasicTerminator($calls)); - - } - - /** - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException - * - * @return void - */ - public function testCannotBootAfterShutdown ():void { - - $kernel = new Application( - new Context(new Name('FireHub'), Environment::TESTING) - ); - - $kernel->shutdown(); - - $this->expectException(InvalidKernelStateException::class); - - $kernel->boot(); - - } - -} \ No newline at end of file diff --git a/tests/Unit/Bootstrapper/SequenceTest.php b/tests/Unit/Bootstrapper/SequenceTest.php new file mode 100644 index 0000000..4cc3cc7 --- /dev/null +++ b/tests/Unit/Bootstrapper/SequenceTest.php @@ -0,0 +1,158 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit\Bootstrapper; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Kernel\Bootstrapper; +use FireHub\Kernel\Bootstrapper\Sequence; +use FireHub\Tests\Kernel\Stubs\BasicBootstrapper; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Tests kernel bootstrap sequence + * @since 1.0.0 + */ +#[Small] +#[Group('bootstrapper')] +#[CoversClass(Sequence::class)] +final class SequenceTest extends FireHubTestCase { + + /** + * ### Tests execution of an empty sequence + * @since 1.0.0 + * + * @return void + */ + public function testEmptySequence ():void { + + $sequence = new Sequence; + + $sequence->execute(); + + self::assertTrue(true); + + } + + /** + * ### Tests execution order + * @since 1.0.0 + * + * @return void + */ + public function testExecutionOrder ():void { + + $sequence = new Sequence; + + $executed = []; + + foreach ([30, 10, 20] as $order) { + + $sequence->register( + new BasicBootstrapper($order, $executed), + $order + ); + + } + + $sequence->execute(); + + self::assertSame([10, 20, 30], $executed); + + } + + /** + * ### Tests stable ordering for equal priorities + * @since 1.0.0 + * + * @return void + */ + public function testStableExecutionOrder ():void { + + $sequence = new Sequence; + + $executed = []; + + foreach (['first', 'second', 'third'] as $name) { + + $sequence->register( + new BasicBootstrapper($name, $executed), + 10 + ); + + } + + $sequence->execute(); + + self::assertSame(['first', 'second', 'third'], $executed); + + } + + /** + * ### Tests that each bootstrapper executes once + * @since 1.0.0 + * + * @return void + */ + public function testBootstrapperExecutesOnce ():void { + + $sequence = new Sequence; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap'); + + $sequence->register($bootstrapper, 0); + + $sequence->execute(); + + } + + /** + * ### Tests execution stops when a bootstrapper fails + * @since 1.0.0 + * + * @return void + */ + public function testExecutionStopsOnFailure ():void { + + $sequence = new Sequence; + + $first = $this->createMock(Bootstrapper::class); + $failing = $this->createMock(Bootstrapper::class); + $last = $this->createMock(Bootstrapper::class); + + $first->expects($this->once()) + ->method('bootstrap'); + + $failing->expects($this->once()) + ->method('bootstrap') + ->willThrowException(new \RuntimeException('Bootstrap failed.')); + + $last->expects($this->never()) + ->method('bootstrap'); + + $sequence->register($first, 10); + $sequence->register($failing, 20); + $sequence->register($last, 30); + + $this->expectException(\RuntimeException::class); + + $sequence->execute(); + + } + +} \ No newline at end of file diff --git a/tests/Unit/KernelTest.php b/tests/Unit/KernelTest.php new file mode 100644 index 0000000..5f9a90d --- /dev/null +++ b/tests/Unit/KernelTest.php @@ -0,0 +1,462 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Core\Boundary\Kernel as KernelBoundary; +use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Kernel; +use FireHub\Kernel\ { + Bootstrapper, Terminator +}; +use FireHub\Kernel\Exception\ { + InvalidKernelStateException, KernelBootException, KernelShutdownException +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; + +/** + * ### Tests kernel lifecycle management + * @since 1.0.0 + */ +#[Small] +#[Group('kernel')] +#[CoversClass(Kernel::class)] +final class KernelTest extends FireHubTestCase { + + /** + * ### Tests implementation of the kernel contract + * @since 1.0.0 + * + * @return void + */ + public function testImplementsKernelContract ():void { + + $kernel = new Kernel; + + self::assertInstanceOf(KernelBoundary::class, $kernel); + + } + + /** + * ### Tests the initial kernel state + * @since 1.0.0 + * + * @return void + */ + public function testInitialState ():void { + + $kernel = new Kernel; + + self::assertSame(State::CREATED, $kernel->state()); + + } + + /** + * ### Tests bootstrapper registration + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testRegisterBootstrapper ():void { + + $kernel = new Kernel; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap'); + + $kernel->registerBootstrapper($bootstrapper); + + $kernel->boot(); + + self::assertSame(State::BOOTED, $kernel->state()); + + } + + /** + * ### Tests terminator registration + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testRegisterTerminator ():void { + + $kernel = new Kernel; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $kernel->registerTerminator($terminator); + + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * ### Tests the complete kernel lifecycle + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testLifecycle ():void { + + $kernel = new Kernel; + + self::assertSame(State::CREATED, $kernel->state()); + + $kernel->boot(); + + self::assertSame(State::BOOTED, $kernel->state()); + + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * ### Tests bootstrapper execution order + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBootstrapperOrder ():void { + + $kernel = new Kernel; + + $executed = []; + + foreach ([30, 10, 20] as $order) { + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->method('bootstrap') + ->willReturnCallback( + static function () use (&$executed, $order):void { + $executed[] = $order; + } + ); + + $kernel->registerBootstrapper($bootstrapper, $order); + + } + + $kernel->boot(); + + self::assertSame([10, 20, 30], $executed); + + } + + /** + * ### Tests terminator execution order + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testTerminatorOrder ():void { + + $kernel = new Kernel; + + $executed = []; + + foreach ([30, 10, 20] as $order) { + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willReturnCallback( + static function () use (&$executed, $order):void { + $executed[] = $order; + } + ); + + $kernel->registerTerminator($terminator, $order); + + } + + $kernel->shutdown(); + + self::assertSame([10, 20, 30], $executed); + + } + + /** + * ### Tests repeated boot calls + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testBootRepeated ():void { + + $kernel = new Kernel; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap'); + + $kernel->registerBootstrapper($bootstrapper); + + $kernel->boot(); + $kernel->boot(); + + self::assertSame(State::BOOTED, $kernel->state()); + + } + + /** + * ### Tests repeated shutdown calls + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownRepeated ():void { + + $kernel = new Kernel; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $kernel->registerTerminator($terminator); + + $kernel->shutdown(); + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * ### Tests bootstrapper registration after boot + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testRegisterBootstrapperAfterBoot ():void { + + $kernel = new Kernel; + + $kernel->boot(); + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->registerBootstrapper($bootstrapper); + + } + + /** + * ### Tests terminator registration after boot + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * + * @return void + */ + public function testRegisterTerminatorAfterBoot ():void { + + $kernel = new Kernel; + + $kernel->boot(); + + $terminator = $this->createMock(Terminator::class); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->registerTerminator($terminator); + + } + + /** + * ### Tests boot failure propagation + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testBootFailure ():void { + + $kernel = new Kernel; + + $failure = new \RuntimeException('Bootstrap failed.'); + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap') + ->willThrowException($failure); + + $kernel->registerBootstrapper($bootstrapper); + + try { + + $kernel->boot(); + + self::fail('Expected KernelBootException was not thrown.'); + + } catch (KernelBootException $exception) { + + self::assertSame($failure, $exception->getPrevious()); + self::assertSame(State::BOOT_FAILED, $kernel->state()); + + } + + } + + /** + * ### Tests shutdown failure propagation + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testShutdownFailure ():void { + + $kernel = new Kernel; + + $failure = new \RuntimeException('Termination failed.'); + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate') + ->willThrowException($failure); + + $kernel->registerTerminator($terminator); + + try { + + $kernel->shutdown(); + + self::fail('Expected KernelShutdownException was not thrown.'); + + } catch (KernelShutdownException $exception) { + + self::assertSame($failure, $exception->getPrevious()); + self::assertSame(State::SHUTDOWN_FAILED, $kernel->state()); + + } + + } + + /** + * ### Tests shutdown after boot failure + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownAfterBootFailure ():void { + + $kernel = new Kernel; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->method('bootstrap') + ->willThrowException(new \RuntimeException('Bootstrap failed.')); + + $kernel->registerBootstrapper($bootstrapper); + + try { + + $kernel->boot(); + + } catch (KernelBootException) { + + self::assertSame(State::BOOT_FAILED, $kernel->state()); + + } + + $kernel->shutdown(); + + self::assertSame(State::TERMINATED, $kernel->state()); + + } + + /** + * ### Tests boot after shutdown is prohibited + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testBootAfterShutdown ():void { + + $kernel = new Kernel; + + $kernel->shutdown(); + + $this->expectException(InvalidKernelStateException::class); + + $kernel->boot(); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Lifecycle/ControllerTest.php b/tests/Unit/Lifecycle/ControllerTest.php index 2802904..3783221 100644 --- a/tests/Unit/Lifecycle/ControllerTest.php +++ b/tests/Unit/Lifecycle/ControllerTest.php @@ -16,9 +16,11 @@ use FireHub\Testing\FireHubTestCase; use FireHub\Core\Meta\Enum\Kernel\State; use FireHub\Kernel\Lifecycle\Controller; -use FireHub\Kernel\Exception\InvalidKernelStateException; -use FireHub\Tests\Kernel\Stubs\ { - AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm +use FireHub\Kernel\ { + Bootstrapper, Terminator +}; +use FireHub\Kernel\Exception\ { + InvalidKernelStateException, KernelBootException, KernelShutdownException }; use PHPUnit\Framework\Attributes\ { CoversClass, Group, Small @@ -41,23 +43,32 @@ final class ControllerTest extends FireHubTestCase { */ public function testInitialState ():void { - $controller = new Controller(); + $controller = new Controller; self::assertSame(State::CREATED, $controller->state()); } /** + * ### Tests bootstrapper registration * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * * @return void */ - public function testBoot ():void { + public function testRegisterBootstrapper ():void { + + $controller = new Controller; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap'); - $controller = new Controller(); + $controller->registerBootstrapper($bootstrapper, 0); $controller->boot(); @@ -66,19 +77,26 @@ public function testBoot ():void { } /** + * ### Tests terminator registration * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelShutdownException * * @return void */ - public function testShutdown ():void { + public function testRegisterTerminator ():void { - $controller = new Controller(); + $controller = new Controller; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $controller->registerTerminator($terminator, 0); - $controller->boot(); $controller->shutdown(); self::assertSame(State::TERMINATED, $controller->state()); @@ -86,236 +104,431 @@ public function testShutdown ():void { } /** + * ### Tests successful boot * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelShutdownException + * @throws \FireHub\Kernel\Exception\KernelBootException * * @return void */ - public function testShutdownFromCreatedState ():void { + public function testBoot ():void { - $controller = new Controller(); + $controller = new Controller; - $controller->shutdown(); + $controller->boot(); - self::assertSame(State::TERMINATED, $controller->state()); + self::assertSame(State::BOOTED, $controller->state()); } /** + * ### Tests repeated boot calls are ignored * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * * @return void */ - public function testBootExecutesBootstrapper ():void { + public function testBootRepeated ():void { + + $controller = new Controller; - $controller = new Controller(); - $calls = 0; + $bootstrapper = $this->createMock(Bootstrapper::class); - $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); + $bootstrapper->expects($this->once()) + ->method('bootstrap'); + $controller->registerBootstrapper($bootstrapper, 0); + + $controller->boot(); $controller->boot(); - self::assertSame(1, $calls); self::assertSame(State::BOOTED, $controller->state()); } /** + * ### Tests boot failure handling + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testBootFailure ():void { + + $controller = new Controller; + + $failure = new \RuntimeException('Bootstrap failed.'); + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->method('bootstrap') + ->willThrowException($failure); + + $controller->registerBootstrapper($bootstrapper, 0); + + try { + + $controller->boot(); + + self::fail('Expected KernelBootException was not thrown.'); + + } catch (KernelBootException $exception) { + + self::assertSame($failure, $exception->getPrevious()); + self::assertSame(State::BOOT_FAILED, $controller->state()); + + } + + } + + /** + * ### Tests boot is prohibited after termination * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * @throws \FireHub\Kernel\Exception\KernelShutdownException * * @return void */ - public function testShutdownExecutesTerminator ():void { - - $controller = new Controller(); - $calls = 0; + public function testBootAfterShutdown ():void { - $controller->registerTerminator(new BasicTerminator($calls), 0); + $controller = new Controller; - $controller->boot(); $controller->shutdown(); - self::assertSame(1, $calls); self::assertSame(State::TERMINATED, $controller->state()); + $this->expectException(InvalidKernelStateException::class); + + $controller->boot(); + } /** + * ### Tests failed kernels cannot be rebooted * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * * @return void */ - public function testBootIsIdempotent ():void { + public function testBootAfterFailure ():void { - $controller = new Controller(); - $calls = 0; + $controller = new Controller; - $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); + $bootstrapper = $this->createMock(Bootstrapper::class); - $controller->boot(); - $controller->boot(); + $bootstrapper->method('bootstrap') + ->willThrowException(new \RuntimeException('Bootstrap failed.')); - self::assertSame(1, $calls); - self::assertSame(State::BOOTED, $controller->state()); + $controller->registerBootstrapper($bootstrapper, 0); + + try { + + $controller->boot(); + + } catch (KernelBootException) { + + self::assertSame(State::BOOT_FAILED, $controller->state()); + + } + + $this->expectException(InvalidKernelStateException::class); + + $controller->boot(); } /** + * ### Tests successful shutdown * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * @throws \FireHub\Kernel\Exception\KernelShutdownException * * @return void */ - public function testShutdownIsIdempotent ():void { - - $controller = new Controller(); - $calls = 0; + public function testShutdown ():void { - $controller->registerTerminator(new BasicTerminator($calls), 0); + $controller = new Controller; $controller->boot(); $controller->shutdown(); + + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * ### Tests shutdown before boot + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownBeforeBoot ():void { + + $controller = new Controller; + $controller->shutdown(); - self::assertSame(1, $calls); self::assertSame(State::TERMINATED, $controller->state()); } /** + * ### Tests repeated shutdown calls are ignored * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\KernelBootException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelShutdownException * * @return void */ - public function testLifecycleExecutionOrder ():void { + public function testShutdownRepeated ():void { - $controller = new Controller(); - $events = []; + $controller = new Controller; - $global = new EventBootTerm($events); - $application = new AppEventBootTerm($events); + $terminator = $this->createMock(Terminator::class); - $controller->registerBootstrapper($global, -100); - $controller->registerBootstrapper($application, 100); + $terminator->expects($this->once()) + ->method('terminate'); - $controller->registerTerminator($global, 100); - $controller->registerTerminator($application, -100); + $controller->registerTerminator($terminator, 0); - $controller->boot(); $controller->shutdown(); + $controller->shutdown(); + + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * ### Tests shutdown failure handling + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testShutdownFailure ():void { + + $controller = new Controller; + + $failure = new \RuntimeException('Termination failed.'); + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willThrowException($failure); + + $controller->registerTerminator($terminator, 0); + + try { + + $controller->shutdown(); + + self::fail('Expected KernelShutdownException was not thrown.'); + + } catch (KernelShutdownException $exception) { + + self::assertSame($failure, $exception->getPrevious()); + self::assertSame(State::SHUTDOWN_FAILED, $controller->state()); + + } + + } + + /** + * ### Tests shutdown after a boot failure + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testShutdownAfterBootFailure ():void { + + $controller = new Controller; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->method('bootstrap') + ->willThrowException(new \RuntimeException('Bootstrap failed.')); + + $controller->registerBootstrapper($bootstrapper, 0); - self::assertSame([ - 'global', - 'application', - 'application', - 'global' - ], $events); + try { + + $controller->boot(); + + } catch (KernelBootException) { + + self::assertSame(State::BOOT_FAILED, $controller->state()); + + } + + $controller->shutdown(); self::assertSame(State::TERMINATED, $controller->state()); } /** + * ### Tests bootstrapper registration is prohibited after boot * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * * @return void */ - public function testCannotRegisterBootstrapperAfterBoot ():void { + public function testRegisterBootstrapperAfterBoot ():void { - $controller = new Controller(); - $calls = 0; + $controller = new Controller; $controller->boot(); + $bootstrapper = $this->createMock(Bootstrapper::class); + $this->expectException(InvalidKernelStateException::class); - $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); + $controller->registerBootstrapper($bootstrapper, 0); } /** + * ### Tests terminator registration is prohibited after boot * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException * * @return void */ - public function testCannotRegisterTerminatorAfterBoot ():void { + public function testRegisterTerminatorAfterBoot ():void { - $controller = new Controller(); - $calls = 0; + $controller = new Controller; $controller->boot(); + $terminator = $this->createMock(Terminator::class); + $this->expectException(InvalidKernelStateException::class); - $controller->registerTerminator(new BasicTerminator($calls), 0); + $controller->registerTerminator($terminator, 0); } /** + * ### Tests registration is prohibited after termination * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelShutdownException * * @return void */ - public function testCannotRegisterAfterShutdown ():void { + public function testRegisterAfterShutdown ():void { - $controller = new Controller(); - $calls = 0; + $controller = new Controller; $controller->shutdown(); + $bootstrapper = $this->createMock(Bootstrapper::class); + $this->expectException(InvalidKernelStateException::class); - $controller->registerBootstrapper(new BasicBootstrapper($calls), 0); + $controller->registerBootstrapper($bootstrapper, 0); } /** + * ### Tests lifecycle state during bootstrapping * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * @throws \FireHub\Kernel\Exception\KernelBootException - * @throws \FireHub\Kernel\Exception\KernelShutdownException * * @return void */ - public function testCannotBootAfterShutdown ():void { + public function testStateDuringBoot ():void { - $controller = new Controller(); + $controller = new Controller; - $controller->shutdown(); + $bootstrapper = $this->createMock(Bootstrapper::class); - $this->expectException(InvalidKernelStateException::class); + $bootstrapper->method('bootstrap') + ->willReturnCallback( + static function () use ($controller):void { + + self::assertSame(State::BOOTING, $controller->state()); + + } + ); + + $controller->registerBootstrapper($bootstrapper, 0); $controller->boot(); + self::assertSame(State::BOOTED, $controller->state()); + + } + + /** + * ### Tests lifecycle state during termination + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\KernelShutdownException + * + * @return void + */ + public function testStateDuringShutdown ():void { + + $controller = new Controller; + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willReturnCallback( + static function () use ($controller):void { + + self::assertSame(State::SHUTTING_DOWN, $controller->state()); + + } + ); + + $controller->registerTerminator($terminator, 0); + + $controller->shutdown(); + + self::assertSame(State::TERMINATED, $controller->state()); + } } \ No newline at end of file diff --git a/tests/Unit/Lifecycle/ManagerTest.php b/tests/Unit/Lifecycle/ManagerTest.php index 83b4b4f..a96f157 100644 --- a/tests/Unit/Lifecycle/ManagerTest.php +++ b/tests/Unit/Lifecycle/ManagerTest.php @@ -15,8 +15,8 @@ use FireHub\Testing\FireHubTestCase; use FireHub\Kernel\Lifecycle\Manager; -use FireHub\Tests\Kernel\Stubs\ { - AppEventBootTerm, BasicBootstrapper, BasicTerminator, EventBootTerm +use FireHub\Kernel\ { + Bootstrapper, Terminator }; use PHPUnit\Framework\Attributes\ { CoversClass, Group, Small @@ -33,13 +33,14 @@ final class ManagerTest extends FireHubTestCase { /** + * ### Tests initialization without registered bootstrappers * @since 1.0.0 * * @return void */ - public function testInitializeWithoutBootstrappers ():void { + public function testInitializeEmpty ():void { - $manager = new Manager(); + $manager = new Manager; $manager->initialize(); @@ -48,15 +49,16 @@ public function testInitializeWithoutBootstrappers ():void { } /** + * ### Tests termination without registered terminators * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateWithoutTerminators ():void { + public function testTerminateEmpty ():void { - $manager = new Manager(); + $manager = new Manager; $manager->terminate(); @@ -65,209 +67,190 @@ public function testTerminateWithoutTerminators ():void { } /** + * ### Tests bootstrapper registration and execution * @since 1.0.0 * + * @throws Throwable + * * @return void */ - public function testInitializeExecutesBootstrapper ():void { + public function testRegisterBootstrapper ():void { - $manager = new Manager(); - $calls = 0; + $manager = new Manager; - $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); + $bootstrapper = $this->createMock(Bootstrapper::class); - $manager->initialize(); + $bootstrapper->expects($this->once()) + ->method('bootstrap'); - self::assertSame(1, $calls); + $manager->registerBootstrapper($bootstrapper, 0); + + $manager->initialize(); } /** + * ### Tests terminator registration and execution * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateExecutesTerminator ():void { + public function testRegisterTerminator ():void { - $manager = new Manager(); - $calls = 0; + $manager = new Manager; - $manager->registerTerminator(new BasicTerminator($calls), 0); + $terminator = $this->createMock(Terminator::class); - $manager->terminate(); + $terminator->expects($this->once()) + ->method('terminate'); - self::assertSame(1, $calls); + $manager->registerTerminator($terminator, 0); + + $manager->terminate(); } /** + * ### Tests bootstrapper execution order * @since 1.0.0 * + * @throws Throwable + * * @return void */ - public function testInitializeExecutesAllBootstrappers ():void { + public function testInitializeOrder ():void { + + $manager = new Manager; + + $executed = []; - $manager = new Manager(); - $calls = 0; + foreach ([30, 10, 20] as $order) { - $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); - $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); - $manager->registerBootstrapper(new BasicBootstrapper($calls), 0); + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->method('bootstrap') + ->willReturnCallback( + static function () use (&$executed, $order):void { + $executed[] = $order; + } + ); + + $manager->registerBootstrapper($bootstrapper, $order); + + } $manager->initialize(); - self::assertSame(3, $calls); + self::assertSame([10, 20, 30], $executed); } /** + * ### Tests terminator execution order * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateExecutesAllTerminators ():void { + public function testTerminateOrder ():void { - $manager = new Manager(); - $calls = 0; + $manager = new Manager; - $manager->registerTerminator(new BasicTerminator($calls), 0); - $manager->registerTerminator(new BasicTerminator($calls), 0); - $manager->registerTerminator(new BasicTerminator($calls), 0); + $executed = []; - $manager->terminate(); - - self::assertSame(3, $calls); + foreach ([30, 10, 20] as $order) { - } + $terminator = $this->createMock(Terminator::class); - /** - * @since 1.0.0 - * - * @return void - */ - public function testInitializePreservesRegistrationOrder ():void { + $terminator->method('terminate') + ->willReturnCallback( + static function () use (&$executed, $order):void { + $executed[] = $order; + } + ); - $manager = new Manager(); - $events = []; + $manager->registerTerminator($terminator, $order); - $manager->registerBootstrapper(new EventBootTerm($events), 0); - $manager->registerBootstrapper(new AppEventBootTerm($events), 0); + } - $manager->initialize(); + $manager->terminate(); - self::assertSame([ - 'global', - 'application' - ], $events); + self::assertSame([10, 20, 30], $executed); } /** + * ### Tests initialization failure propagation * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateUsesReverseRegistrationOrder ():void { + public function testInitializeFailure ():void { - $manager = new Manager(); - $events = []; + $manager = new Manager; - $manager->registerTerminator(new EventBootTerm($events), 0); - $manager->registerTerminator(new AppEventBootTerm($events), 0); + $failure = new \RuntimeException('Bootstrap failed.'); - $manager->terminate(); + $bootstrapper = $this->createMock(Bootstrapper::class); - self::assertSame([ - 'global', - 'application' - ], $events); + $bootstrapper->expects($this->once()) + ->method('bootstrap') + ->willThrowException($failure); - } + $manager->registerBootstrapper($bootstrapper, 0); - /** - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testInitializeUsesExecutionOrder ():void { + try { - $manager = new Manager(); - $events = []; + $manager->initialize(); - $manager->registerBootstrapper(new EventBootTerm($events), 100); - $manager->registerBootstrapper(new AppEventBootTerm($events), -100); + self::fail('Expected bootstrap exception was not thrown.'); - $manager->initialize(); + } catch (\RuntimeException $exception) { + + self::assertSame($failure, $exception); - self::assertSame([ - 'application', - 'global' - ], $events); + } } /** + * ### Tests termination failure propagation * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateUsesExecutionOrder ():void { + public function testTerminateFailure ():void { - $manager = new Manager(); - $events = []; + $manager = new Manager; - $manager->registerTerminator(new EventBootTerm($events), 100); - $manager->registerTerminator(new AppEventBootTerm($events), -100); + $failure = new \RuntimeException('Termination failed.'); - $manager->terminate(); - - self::assertSame([ - 'application', - 'global' - ], $events); + $terminator = $this->createMock(Terminator::class); - } + $terminator->expects($this->once()) + ->method('terminate') + ->willThrowException($failure); - /** - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testInitializeAndTerminate ():void { + $manager->registerTerminator($terminator, 0); - $manager = new Manager(); - $events = []; + try { - $global = new EventBootTerm($events); - $application = new AppEventBootTerm($events); + $manager->terminate(); - $manager->registerBootstrapper($global, -100); - $manager->registerBootstrapper($application, 100); + self::fail('Expected termination exception was not thrown.'); - $manager->registerTerminator($global, 100); - $manager->registerTerminator($application, -100); + } catch (\RuntimeException $exception) { - $manager->initialize(); - $manager->terminate(); + self::assertSame($failure, $exception); - self::assertSame([ - 'global', - 'application', - 'application', - 'global' - ], $events); + } } diff --git a/tests/Unit/LifecycleTest.php b/tests/Unit/LifecycleTest.php index 2af1a83..fa08ba3 100644 --- a/tests/Unit/LifecycleTest.php +++ b/tests/Unit/LifecycleTest.php @@ -18,7 +18,7 @@ use FireHub\Kernel\Lifecycle; use FireHub\Kernel\Exception\InvalidKernelStateException; use PHPUnit\Framework\Attributes\ { - CoversClass, Group, Small + CoversClass, DataProvider, Group, Small }; /** @@ -31,68 +31,129 @@ final class LifecycleTest extends FireHubTestCase { /** + * ### Tests the initial lifecycle state * @since 1.0.0 - * - * @return void */ public function testInitialState ():void { - self::assertSame(State::CREATED, new Lifecycle()->state()); + $lifecycle = new Lifecycle; + + self::assertSame(State::CREATED, $lifecycle->state()); } /** + * ### Tests beginning kernel initialization * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * * @return void */ - public function testBootTransitions ():void { + public function testBeginBoot ():void { - $lifecycle = new Lifecycle(); + $lifecycle = new Lifecycle; $lifecycle->beginBoot(); + self::assertSame(State::BOOTING, $lifecycle->state()); + } + + /** + * ### Tests completing kernel initialization + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testCompleteBoot ():void { + + $lifecycle = new Lifecycle; + + $lifecycle->beginBoot(); $lifecycle->completeBoot(); + self::assertSame(State::BOOTED, $lifecycle->state()); } /** + * ### Tests beginning shutdown before initialization * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * * @return void */ - public function testShutdownTransitions ():void { + public function testBeginShutdownBeforeBoot ():void { - $lifecycle = new Lifecycle(); + $lifecycle = new Lifecycle; $lifecycle->beginShutdown(); + self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); - $lifecycle->completeShutdown(); - self::assertSame(State::TERMINATED, $lifecycle->state()); + } + + /** + * ### Tests beginning shutdown after initialization + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testBeginShutdownAfterBoot ():void { + + $lifecycle = new Lifecycle; + + $lifecycle->beginBoot(); + $lifecycle->completeBoot(); + $lifecycle->beginShutdown(); + + self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); } /** + * ### Tests beginning shutdown after initialization failure * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * * @return void */ - public function testFailedBootCanBeShutDown ():void { + public function testBeginShutdownAfterBootFailure ():void { + + $lifecycle = new Lifecycle; - $lifecycle = new Lifecycle(); $lifecycle->beginBoot(); $lifecycle->fail(); + $lifecycle->beginShutdown(); - self::assertSame(State::BOOT_FAILED, $lifecycle->state()); + self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); + + } + + /** + * ### Tests completing kernel shutdown + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testCompleteShutdown ():void { + + $lifecycle = new Lifecycle; $lifecycle->beginShutdown(); $lifecycle->completeShutdown(); @@ -102,46 +163,240 @@ public function testFailedBootCanBeShutDown ():void { } /** + * ### Tests marking initialization as failed * @since 1.0.0 * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException * * @return void */ - public function testCannotBootTwice ():void { + public function testFailDuringBoot ():void { + + $lifecycle = new Lifecycle; - $lifecycle = new Lifecycle(); $lifecycle->beginBoot(); + $lifecycle->fail(); + + self::assertSame(State::BOOT_FAILED, $lifecycle->state()); - $this->expectException(InvalidKernelStateException::class); + } - $lifecycle->beginBoot(); + /** + * ### Tests marking shutdown as failed + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * + * @return void + */ + public function testFailDuringShutdown ():void { + + $lifecycle = new Lifecycle; + + $lifecycle->beginShutdown(); + $lifecycle->fail(); + + self::assertSame(State::SHUTDOWN_FAILED, $lifecycle->state()); } /** + * ### Tests the complete successful lifecycle * @since 1.0.0 * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * * @return void */ - public function testCannotCompleteBootBeforeBeginning ():void { + public function testCompleteLifecycle ():void { + + $lifecycle = new Lifecycle; - $this->expectException(InvalidKernelStateException::class); + self::assertSame(State::CREATED, $lifecycle->state()); + + $lifecycle->beginBoot(); - new Lifecycle()->completeBoot(); + self::assertSame(State::BOOTING, $lifecycle->state()); + + $lifecycle->completeBoot(); + + self::assertSame(State::BOOTED, $lifecycle->state()); + + $lifecycle->beginShutdown(); + + self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); + + $lifecycle->completeShutdown(); + + self::assertSame(State::TERMINATED, $lifecycle->state()); } /** + * ### Tests invalid lifecycle transitions * @since 1.0.0 * + * @param list $operations + * @param non-empty-string $operation + * @param State $expected_state + * + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * @throws \FireHub\Kernel\Exception\InvalidKernelStateException + * * @return void */ - public function testCannotFailWhenIdle ():void { + #[DataProvider('invalidTransitionProvider')] + public function testInvalidTransition (array $operations, string $operation, State $expected_state):void { - $this->expectException(InvalidKernelStateException::class); + $lifecycle = new Lifecycle; - new Lifecycle()->fail(); + foreach ($operations as $method) + $lifecycle->{$method}(); + + self::assertSame($expected_state, $lifecycle->state()); + + try { + + $lifecycle->{$operation}(); + + self::fail('Expected InvalidKernelStateException was not thrown.'); + + } catch (InvalidKernelStateException) { + + self::assertSame($expected_state, $lifecycle->state()); + + } + + } + + /** + * ### Provides invalid lifecycle transitions + * @since 1.0.0 + * + * @return iterable, + * operation: non-empty-string, + * expected_state: State + * }> + */ + public static function invalidTransitionProvider ():iterable { + + yield 'complete boot before beginning boot' => [ + 'operations' => [], + 'operation' => 'completeBoot', + 'expected_state' => State::CREATED + ]; + + yield 'complete shutdown before beginning shutdown' => [ + 'operations' => [], + 'operation' => 'completeShutdown', + 'expected_state' => State::CREATED + ]; + + yield 'fail before beginning lifecycle operation' => [ + 'operations' => [], + 'operation' => 'fail', + 'expected_state' => State::CREATED + ]; + + yield 'begin boot while booting' => [ + 'operations' => ['beginBoot'], + 'operation' => 'beginBoot', + 'expected_state' => State::BOOTING + ]; + + yield 'complete boot twice' => [ + 'operations' => ['beginBoot', 'completeBoot'], + 'operation' => 'completeBoot', + 'expected_state' => State::BOOTED + ]; + + yield 'begin boot after successful boot' => [ + 'operations' => ['beginBoot', 'completeBoot'], + 'operation' => 'beginBoot', + 'expected_state' => State::BOOTED + ]; + + yield 'begin shutdown while booting' => [ + 'operations' => ['beginBoot'], + 'operation' => 'beginShutdown', + 'expected_state' => State::BOOTING + ]; + + yield 'fail after successful boot' => [ + 'operations' => ['beginBoot', 'completeBoot'], + 'operation' => 'fail', + 'expected_state' => State::BOOTED + ]; + + yield 'begin boot after boot failure' => [ + 'operations' => ['beginBoot', 'fail'], + 'operation' => 'beginBoot', + 'expected_state' => State::BOOT_FAILED + ]; + + yield 'complete boot after boot failure' => [ + 'operations' => ['beginBoot', 'fail'], + 'operation' => 'completeBoot', + 'expected_state' => State::BOOT_FAILED + ]; + + yield 'fail twice after boot failure' => [ + 'operations' => ['beginBoot', 'fail'], + 'operation' => 'fail', + 'expected_state' => State::BOOT_FAILED + ]; + + yield 'begin shutdown while shutting down' => [ + 'operations' => ['beginShutdown'], + 'operation' => 'beginShutdown', + 'expected_state' => State::SHUTTING_DOWN + ]; + + yield 'complete shutdown twice' => [ + 'operations' => ['beginShutdown', 'completeShutdown'], + 'operation' => 'completeShutdown', + 'expected_state' => State::TERMINATED + ]; + + yield 'begin shutdown after termination' => [ + 'operations' => ['beginShutdown', 'completeShutdown'], + 'operation' => 'beginShutdown', + 'expected_state' => State::TERMINATED + ]; + + yield 'begin boot after termination' => [ + 'operations' => ['beginShutdown', 'completeShutdown'], + 'operation' => 'beginBoot', + 'expected_state' => State::TERMINATED + ]; + + yield 'fail after termination' => [ + 'operations' => ['beginShutdown', 'completeShutdown'], + 'operation' => 'fail', + 'expected_state' => State::TERMINATED + ]; + + yield 'begin shutdown after shutdown failure' => [ + 'operations' => ['beginShutdown', 'fail'], + 'operation' => 'beginShutdown', + 'expected_state' => State::SHUTDOWN_FAILED + ]; + + yield 'complete shutdown after shutdown failure' => [ + 'operations' => ['beginShutdown', 'fail'], + 'operation' => 'completeShutdown', + 'expected_state' => State::SHUTDOWN_FAILED + ]; + + yield 'fail twice after shutdown failure' => [ + 'operations' => ['beginShutdown', 'fail'], + 'operation' => 'fail', + 'expected_state' => State::SHUTDOWN_FAILED + ]; } diff --git a/tests/Unit/Terminator/SequenceTest.php b/tests/Unit/Terminator/SequenceTest.php new file mode 100644 index 0000000..9e04fac --- /dev/null +++ b/tests/Unit/Terminator/SequenceTest.php @@ -0,0 +1,328 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit\Terminator; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Kernel\Terminator; +use FireHub\Kernel\Terminator\Sequence; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; +use Throwable; + +/** + * ### Tests kernel termination sequence + * @since 1.0.0 + */ +#[Small] +#[Group('terminator')] +#[CoversClass(Sequence::class)] +final class SequenceTest extends FireHubTestCase { + + /** + * ### Tests execution of an empty sequence + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteEmpty ():void { + + $sequence = new Sequence; + + $sequence->execute(); + + $this->expectNotToPerformAssertions(); + + } + + /** + * ### Tests execution of a single terminator + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteSingle ():void { + + $sequence = new Sequence; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $sequence->register($terminator, 0); + + $sequence->execute(); + + } + + /** + * ### Tests execution of multiple terminators + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteMultiple ():void { + + $sequence = new Sequence; + + $terminators = []; + + for ($i = 0; $i < 3; $i++) { + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $terminators[] = $terminator; + + } + + foreach ($terminators as $terminator) + $sequence->register($terminator, 0); + + $sequence->execute(); + + } + + /** + * ### Tests ascending execution order + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteOrder ():void { + + $sequence = new Sequence; + + $executed = []; + + foreach ([30, 10, 20] as $order) { + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willReturnCallback( + static function () use (&$executed, $order):void { + $executed[] = $order; + } + ); + + $sequence->register($terminator, $order); + + } + + $sequence->execute(); + + self::assertSame([10, 20, 30], $executed); + + } + + /** + * ### Tests stable execution order + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteEqualOrder ():void { + + $sequence = new Sequence; + + $executed = []; + + foreach (['first', 'second', 'third'] as $name) { + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willReturnCallback( + static function () use (&$executed, $name):void { + $executed[] = $name; + } + ); + + $sequence->register($terminator, 10); + + } + + $sequence->execute(); + + self::assertSame(['first', 'second', 'third'], $executed); + + } + + /** + * ### Tests negative execution priorities + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteNegativeOrder ():void { + + $sequence = new Sequence; + + $executed = []; + + foreach ([10, -100, 0, -10] as $order) { + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willReturnCallback( + static function () use (&$executed, $order):void { + $executed[] = $order; + } + ); + + $sequence->register($terminator, $order); + + } + + $sequence->execute(); + + self::assertSame([-100, -10, 0, 10], $executed); + + } + + /** + * ### Tests termination continues after a failure + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteContinuesOnFailure ():void { + + $sequence = new Sequence; + + $executed = []; + + foreach ([1, 2, 3] as $order) { + + $terminator = $this->createMock(Terminator::class); + + $terminator->method('terminate') + ->willReturnCallback( + static function () use (&$executed, $order):void { + + $executed[] = $order; + + if ($order === 2) + throw new \RuntimeException('Termination failed.'); + + } + ); + + $sequence->register($terminator, $order); + + } + + try { + + $sequence->execute(); + + self::fail('Expected termination exception was not thrown.'); + + } catch (\RuntimeException $exception) { + + self::assertSame('Termination failed.', $exception->getMessage()); + + } + + self::assertSame([1, 2, 3], $executed); + + } + + /** + * ### Tests that the first exception is propagated + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteThrowsFirstFailure ():void { + + $sequence = new Sequence; + + $first_exception = new \RuntimeException('First failure.'); + $second_exception = new \RuntimeException('Second failure.'); + + $first = $this->createMock(Terminator::class); + $second = $this->createMock(Terminator::class); + + $first->expects($this->once()) + ->method('terminate') + ->willThrowException($first_exception); + + $second->expects($this->once()) + ->method('terminate') + ->willThrowException($second_exception); + + $sequence->register($first, 10); + $sequence->register($second, 20); + + try { + + $sequence->execute(); + + self::fail('Expected termination exception was not thrown.'); + + } catch (\RuntimeException $exception) { + + self::assertSame($first_exception, $exception); + + } + + } + + /** + * ### Tests repeated sequence execution + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testExecuteRepeated ():void { + + $sequence = new Sequence; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->exactly(2)) + ->method('terminate'); + + $sequence->register($terminator, 0); + + $sequence->execute(); + $sequence->execute(); + + } + +} \ No newline at end of file From e4b738bf16e42a23340a40d1c8930cc2c3a8c276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 16 Oct 2026 10:55:39 +0200 Subject: [PATCH 14/32] Mark lifecycle classes as `@internal` for restricted usage Updated the PHPDoc for `Lifecycle`, `Controller`, and `Manager` classes to include the `@internal` annotation, indicating these classes are intended for internal use only within the kernel package. This change helps prevent unintended usage outside the intended scope and reinforces clear boundaries for maintainers and contributors. --- src/Lifecycle.php | 2 ++ src/Lifecycle/Controller.php | 2 ++ src/Lifecycle/Manager.php | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Lifecycle.php b/src/Lifecycle.php index d88465d..946ecc8 100644 --- a/src/Lifecycle.php +++ b/src/Lifecycle.php @@ -22,6 +22,8 @@ * Encapsulates the kernel state machine and ensures that lifecycle transitions happen only from valid states. This * class does not execute lifecycle components. * @since 1.0.0 + * + * @internal */ final class Lifecycle { diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index 48f2253..7790ce0 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -30,6 +30,8 @@ * Owns the lifecycle state machine and component execution manager. Validates * registration and orchestrates boot and shutdown operations. * @since 1.0.0 + * + * @internal */ final readonly class Controller implements Kernel { diff --git a/src/Lifecycle/Manager.php b/src/Lifecycle/Manager.php index 984fc32..44e829b 100644 --- a/src/Lifecycle/Manager.php +++ b/src/Lifecycle/Manager.php @@ -28,6 +28,8 @@ * * State validation belongs to the lifecycle controller, not this execution manager. * @since 1.0.0 + * + * @internal */ final readonly class Manager { From 87a64c9becf805a82d09193bd9964c9922f54682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 16 Oct 2026 12:07:21 +0200 Subject: [PATCH 15/32] Fix: Reorder dependencies in composer.json files for consistent formatting Reordered "the-firehub-project/runtime" and other dependencies in multiple composer.json files across Kernel, Container, and Advanced-DataStructures-Capability directories to ensure consistent dependency order. No functional changes introduced. --- composer.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 4e28abc..428fa47 100644 --- a/composer.json +++ b/composer.json @@ -9,9 +9,8 @@ "php": "^8.5", "ext-mbstring": "*", - "the-firehub-project/runtime": ">=0.1.0 <1.0.0", "the-firehub-project/core-standard": "dev-develop", - "the-firehub-project/foundation": "dev-develop" + "the-firehub-project/runtime": ">=0.1.0 <1.0.0" }, "require-dev": { "phpstan/phpstan": "^2.0", From 608ce7920874b419ddc06f5c08cb7df0ac610eb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 08:47:03 +0200 Subject: [PATCH 16/32] Refactor kernel lifecycle with dedicated engine and state management Replaced `Bootstrapper\Sequence` and `Terminator\Sequence` with a unified, generic lifecycle engine (`Lifecycle\Engine`) and state mappings (`Lifecycle\States`). Key updates: - **Engine and State Management**: Introduced `Lifecycle\Engine` for reusable lifecycle execution across startup and shutdown phases, leveraging state mappings in `States`. - **New Exceptions**: Added `LifecycleExecutionException`, `InvalidLifecycleStateException`, and `DistinctEnumCaseException` under the `Lifecycle\Exception` namespace for granular error handling. - **State Transitions**: Defined distinct lifecycle states for initialization and cleanup phases using enumeration-based state validation. - **Simplified Controller Logic**: Refactored `Lifecycle\Controller` to delegate state transitions and execution handling to the `Engine` while preserving kernel-specific behaviors. - **Backward Compatibility**: Registered kernel bootstrappers and terminators as callbacks in the engine to maintain compatibility with existing contracts. These changes improve lifecycle maintainability, decouple kernel lifecycle phases, and introduce robust error handling and state transitions. --- src/Bootstrapper/Sequence.php | 91 ----- src/Lifecycle/Controller.php | 189 +++++----- src/Lifecycle/Engine.php | 334 ++++++++++++++++++ .../Exception/DistinctEnumCaseException.php | 26 ++ .../InvalidLifecycleStateException.php | 32 ++ .../Exception/LifecycleExecutionException.php | 31 ++ src/Lifecycle/Manager.php | 171 ++++++--- src/Lifecycle/States.php | 105 ++++++ src/Terminator/Sequence.php | 110 ------ 9 files changed, 751 insertions(+), 338 deletions(-) delete mode 100644 src/Bootstrapper/Sequence.php create mode 100644 src/Lifecycle/Engine.php create mode 100644 src/Lifecycle/Exception/DistinctEnumCaseException.php create mode 100644 src/Lifecycle/Exception/InvalidLifecycleStateException.php create mode 100644 src/Lifecycle/Exception/LifecycleExecutionException.php create mode 100644 src/Lifecycle/States.php delete mode 100644 src/Terminator/Sequence.php diff --git a/src/Bootstrapper/Sequence.php b/src/Bootstrapper/Sequence.php deleted file mode 100644 index 62ecca2..0000000 --- a/src/Bootstrapper/Sequence.php +++ /dev/null @@ -1,91 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel - */ - -namespace FireHub\Kernel\Bootstrapper; - -use FireHub\Core\Boundary\Kernel\Bootstrapper; -use FireHub\Runtime; - -/** - * ### Manages the kernel bootstrap sequence - * - * Registers and executes bootstrap components according to their configured execution order. - * - * Components with lower order values execute first. Components with equal order values retain their original - * registration order. - * - * The sequence does not manage kernel lifecycle states. - * @since 1.0.0 - * - * @internal - */ -final class Sequence { - - /** - * ### Registered bootstrappers - * @since 1.0.0 - * - * @var list - */ - private array $bootstrappers = []; - - /** - * ### Registers a bootstrapper - * @since 1.0.0 - * - * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

- * Bootstrap component. - *

- * @param int $order

- * Execution order. - *

- * - * @return void - */ - public function register (Bootstrapper $bootstrapper, int $order):void { - - $this->bootstrappers[] = [ - 'bootstrapper' => $bootstrapper, - 'order' => $order - ]; - - } - - /** - * ### Executes the bootstrap sequence - * - * Executes registered bootstrappers in ascending order. - * - * Execution stops immediately if a bootstrapper throws an exception. - * @since 1.0.0 - * - * @uses \FireHub\Runtime\Arr\Ordering::sortBy() Sorts an array by a user-defined comparison function. - * @uses \FireHub\Core\Boundary\Kernel\Bootstrapper::bootstrap() To bootstrap component. - * - * @return void - */ - public function execute ():void { - - $entries = $this->bootstrappers; - - Runtime\Arr\Ordering::sortBy( - $entries, - static fn (array $a, array $b):int => $a['order'] <=> $b['order'] - ); - - foreach ($entries as $entry) - $entry['bootstrapper']->bootstrap(); - - } - -} \ No newline at end of file diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index 7790ce0..1a5118a 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -18,34 +18,36 @@ Bootstrapper, Terminator }; use FireHub\Core\Meta\Enum\Kernel\State; -use FireHub\Kernel\Lifecycle; use FireHub\Kernel\Exception\ { InvalidKernelStateException, KernelBootException, KernelShutdownException }; -use Throwable; +use FireHub\Kernel\Lifecycle\Exception\ { + InvalidLifecycleStateException, LifecycleExecutionException +}; /** * ### Coordinates the kernel lifecycle * - * Owns the lifecycle state machine and component execution manager. Validates - * registration and orchestrates boot and shutdown operations. - * @since 1.0.0 + * Adapts the generic lifecycle engine to the kernel's initialization and termination contracts. + * + * Maintains an independent lifecycle engine configured with kernel-specific states and exposes kernel-specific + * exceptions to callers. * - * @internal + * Bootstrappers and terminators are registered as callbacks without changing their public contracts. + * + * The controller does not implement lifecycle state transitions directly. All state management and component + * execution are delegated to the engine. + * @since 1.0.0 */ final readonly class Controller implements Kernel { /** - * ### Lifecycle state machine - * @since 1.0.0 - */ - private Lifecycle $lifecycle; - - /** - * ### Lifecycle component manager + * ### Kernel lifecycle engine * @since 1.0.0 + * + * @var \FireHub\Kernel\Lifecycle\Engine */ - private Manager $manager; + private Engine $lifecycle; /** * ### Constructor @@ -55,20 +57,30 @@ */ public function __construct () { - $this->lifecycle = new Lifecycle; - - $this->manager = new Manager; + $this->lifecycle = new Engine( + new States( + State::CREATED, + State::BOOTING, + State::BOOTED, + State::SHUTTING_DOWN, + State::TERMINATED, + State::BOOT_FAILED, + State::SHUTDOWN_FAILED + ) + ); } /** - * ### Registers a bootstrapper before initialization + * ### Registers a kernel bootstrapper + * + * Registers a bootstrapper for execution during kernel initialization. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Controller::assertRegistrationAllowed() To validate registration. - * @uses \FireHub\Kernel\Lifecycle\Manager::registerBootstrapper() To register bootstrapper. + * @uses \FireHub\Kernel\Lifecycle\Engine::registerStartup() To register a bootstrapper. + * @uses \FireHub\Kernel\Bootstrapper::bootstrap() To execute the bootstrapper. * - * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

+ * @param \FireHub\Kernel\Bootstrapper $bootstrapper

* Bootstrapper to register. *

* @param int $order

@@ -81,20 +93,38 @@ public function __construct () { */ public function registerBootstrapper (Bootstrapper $bootstrapper, int $order):void { - $this->assertRegistrationAllowed(); + try { + + $this->lifecycle->registerStartup( + static function () use ($bootstrapper):void { + + $bootstrapper->bootstrap(); + + }, + $order + ); - $this->manager->registerBootstrapper($bootstrapper, $order); + } catch (InvalidLifecycleStateException $exception) { + + throw new InvalidKernelStateException( + 'Cannot register a bootstrapper in the current kernel state.', + previous: $exception + ); + + } } /** - * ### Registers a terminator before initialization + * ### Registers a kernel terminator + * + * Registers a terminator for execution during kernel shutdown. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Controller::assertRegistrationAllowed() To validate registration. - * @uses \FireHub\Kernel\Lifecycle\Manager::registerTerminator() To register terminator. + * @uses \FireHub\Kernel\Lifecycle\Engine::registerShutdown() To register a terminator. + * @uses \FireHub\Kernel\Terminator::terminate() To execute the terminator. * - * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

+ * @param \FireHub\Kernel\Terminator $terminator

* Terminator to register. *

* @param int $order

@@ -107,45 +137,59 @@ public function registerBootstrapper (Bootstrapper $bootstrapper, int $order):vo */ public function registerTerminator (Terminator $terminator, int $order):void { - $this->assertRegistrationAllowed(); + try { + + $this->lifecycle->registerShutdown( + static function () use ($terminator):void { + + $terminator->terminate(); + + }, + $order + ); + + } catch (InvalidLifecycleStateException $exception) { + + throw new InvalidKernelStateException( + 'Cannot register a terminator in the current kernel state.', + previous: $exception + ); - $this->manager->registerTerminator($terminator, $order); + } } /** - * @inheritDoc + * ### Boots the kernel * + * Executes registered bootstrappers and transitions the kernel into its booted state when initialization succeeds. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. - * @uses \FireHub\Kernel\Lifecycle\Manager::initialize() To initialize. - * @uses \FireHub\Kernel\Lifecycle::beginBoot() To begin booting. - * @uses \FireHub\Kernel\Lifecycle::completeBoot() To complete booting. - * @uses \FireHub\Kernel\Lifecycle::fail() To fail booting. + * @uses \FireHub\Kernel\Lifecycle\Engine::start() To execute registered bootstrappers. * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If booting is not allowed. * @throws \FireHub\Kernel\Exception\KernelBootException If a bootstrapper fails. + * + * @return void */ public function boot ():void { - if ($this->state() === State::BOOTED) return; - - $this->lifecycle->beginBoot(); - try { - $this->manager->initialize(); + $this->lifecycle->start(); - $this->lifecycle->completeBoot(); + } catch (InvalidLifecycleStateException $exception) { - } catch (Throwable $exception) { + throw new InvalidKernelStateException( + 'Cannot boot the kernel in the current state.', + previous: $exception + ); - $this->lifecycle->fail(); + } catch (LifecycleExecutionException $exception) { throw new KernelBootException( 'Failed to boot the kernel.', - previous: $exception + previous: $exception->getPrevious() ?? $exception ); } @@ -153,38 +197,36 @@ public function boot ():void { } /** - * @inheritDoc + * ### Shuts down the kernel * + * Executes registered terminators and transitions the kernel into its terminated state when shutdown succeeds. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. - * @uses \FireHub\Kernel\Lifecycle\Manager::terminate() To terminate. - * @uses \FireHub\Kernel\Lifecycle::beginShutdown() To begin shutdown. - * @uses \FireHub\Kernel\Lifecycle::completeShutdown() To complete shutdown. - * @uses \FireHub\Kernel\Lifecycle::fail() To mark shutdown as failed. + * @uses \FireHub\Kernel\Lifecycle\Engine::stop() To execute registered terminators. * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. * @throws \FireHub\Kernel\Exception\KernelShutdownException If a terminator fails. + * + * @return void */ public function shutdown ():void { - if ($this->state() === State::TERMINATED) return; - - $this->lifecycle->beginShutdown(); - try { - $this->manager->terminate(); + $this->lifecycle->stop(); - $this->lifecycle->completeShutdown(); + } catch (InvalidLifecycleStateException $exception) { - } catch (Throwable $exception) { + throw new InvalidKernelStateException( + 'Cannot shut down the kernel in the current state.', + previous: $exception + ); - $this->lifecycle->fail(); + } catch (LifecycleExecutionException $exception) { throw new KernelShutdownException( 'Failed to shut down the kernel.', - previous: $exception + previous: $exception->getPrevious() ?? $exception ); } @@ -192,11 +234,14 @@ public function shutdown ():void { } /** - * @inheritDoc + * ### Gets the current kernel lifecycle state * + * Returns the kernel's own lifecycle state without conversion. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle::state() To get the current state. + * @uses \FireHub\Kernel\Lifecycle\Engine::state() To get the current state. + * + * @return \FireHub\Core\Meta\Enum\Kernel\State Current kernel lifecycle state. */ public function state ():State { @@ -204,26 +249,4 @@ public function state ():State { } - /** - * ### Validates lifecycle component registration - * @since 1.0.0 - * - * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If registration is not allowed. - * - * @return void - */ - private function assertRegistrationAllowed ():void { - - if ($this->state() !== State::CREATED) - throw new InvalidKernelStateException( - 'Cannot register lifecycle components after kernel initialization has started.', - [ - 'state' => $this->state() - ] - ); - - } - } \ No newline at end of file diff --git a/src/Lifecycle/Engine.php b/src/Lifecycle/Engine.php new file mode 100644 index 0000000..9c74b57 --- /dev/null +++ b/src/Lifecycle/Engine.php @@ -0,0 +1,334 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle; + + +use FireHub\Kernel\Lifecycle\Exception\ { + InvalidLifecycleStateException, LifecycleExecutionException +}; +use Closure, Throwable, UnitEnum; + +/** + * ### Coordinates a configurable lifecycle + * + * Provides a reusable lifecycle state machine and execution controller independent of the component owning the + * lifecycle. + * + * Each engine instance maintains its own state, startup callbacks, and shutdown callbacks. + * + * Lifecycle states are supplied during construction and remain associated with the engine throughout its lifetime. + * + * Startup and shutdown operations are guarded by explicit state transitions. Startup failures prevent further + * initialization, while shutdown remains available after a startup failure to allow resource cleanup. + * + * Repeated startup calls after a successful startup and repeated shutdown calls after a successful shutdown are + * treated as no-ops. + * + * The engine does not expose domain-specific exceptions. Lifecycle owners are responsible for translating generic + * failures at their public API boundaries. + * + * @template TState of UnitEnum + * + * @internal + */ +final class Engine { + + /** + * ### Current lifecycle state + * @since 1.0.0 + * + * @var TState + */ + private UnitEnum $state; + + /** + * ### Lifecycle component manager + * @since 1.0.0 + */ + private Manager $manager; + + /** + * ### Lifecycle state storage + * @since 1.0.0 + * + * @var object{state: TState} + */ + private object $storage; + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Kernel\Lifecycle\States $states

+ * Lifecycle state configuration. + *

+ * + * @return void + */ + public function __construct ( + private readonly States $states + ) { + + $this->manager = new Manager; + + $this->state = $states->created; + + $this->storage = new class($states->created) { + + public function __construct ( + public UnitEnum $state + ) {} + + }; + + } + + /** + * ### Registers a startup callback + * + * Registers a callback that executes during lifecycle startup. + * + * Registration is permitted only before the lifecycle has started. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Engine::assertRegistrationAllowed() To validate registration. + * @uses \FireHub\Kernel\Lifecycle\Manager::registerStartup() To register a startup callback. + * + * @param Closure():void $callback

+ * Startup callback. + *

+ * @param int $order [optional]

+ * Execution order. + *

+ * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If registration is not allowed. + * + * @return void + */ + public function registerStartup (Closure $callback, int $order = 0):void { + + $this->assertRegistrationAllowed(); + + $this->manager->registerStartup($callback, $order); + + } + + /** + * ### Registers a shutdown callback + * + * Registers a callback that executes during lifecycle shutdown. + * + * Registration is permitted only before the lifecycle has started. + * @since 1.0.0 + g + * @uses \FireHub\Kernel\Lifecycle\Engine::assertRegistrationAllowed() To validate registration. + * @uses \FireHub\Kernel\Lifecycle\Manager::registerShutdown() To register a shutdown callback. + * + * @param Closure():void $callback

+ * Shutdown callback. + *

+ * @param int $order [optional]

+ * Execution order. + *

+ * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If registration is not allowed. + * + * @return void + */ + public function registerShutdown (Closure $callback, int $order = 0):void { + + $this->assertRegistrationAllowed(); + + $this->manager->registerShutdown($callback, $order); + + } + + /** + * ### Starts the lifecycle + * + * Transitions the lifecycle from its created state into the starting state and executes registered startup + * callbacks. + * + * Successful execution transitions the lifecycle into the started state. + * + * If execution fails, the lifecycle enters the start-failed state and the original failure is preserved as the + * previous exception. + * + * Calling this method when the lifecycle is already started has no effect. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Engine::transition() To perform a guarded lifecycle transition. + * @uses \FireHub\Kernel\Lifecycle\Manager::start() To execute startup callbacks. + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If startup is not allowed. + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If a startup callback fails. + * + * @return void + */ + public function start ():void { + + if ($this->state === $this->states->started) return; + + $this->transition( + $this->states->created, + $this->states->starting + ); + + try { + + $this->manager->start(); + + $this->transition( + $this->states->starting, + $this->states->started + ); + + } catch (Throwable $exception) { + + $this->state = $this->states->start_failed; + + throw new LifecycleExecutionException( + 'Lifecycle startup failed.', + previous: $exception + ); + + } + + } + + /** + * ### Stops the lifecycle + * + * Begins lifecycle shutdown and executes registered shutdown callbacks. + * + * Shutdown is permitted from the created, started, and start-failed states. This allows cleanup even when the + * startup did not complete successfully. + * + * Successful execution transitions the lifecycle into the stopped state. + * + * If shutdown execution fails, the lifecycle enters the stop-failed state. + * + * Calling this method when the lifecycle is already stopped has no effect. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Manager::stop() To execute shutdown callbacks. + * @uses \FireHub\Kernel\Lifecycle\Engine::transition() To perform a guarded lifecycle transition. + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If shutdown is not allowed. + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If a shutdown callback fails. + * + * @return void + */ + public function stop ():void { + + if ($this->state === $this->states->stopped) return; + + if ( + $this->state !== $this->states->created + && $this->state !== $this->states->started + && $this->state !== $this->states->start_failed + ) + throw new InvalidLifecycleStateException( + 'Lifecycle shutdown is not allowed in the current state.' + ); + + $this->state = $this->states->stopping; + + try { + + $this->manager->stop(); + + $this->transition( + $this->states->stopping, + $this->states->stopped + ); + + } catch (Throwable $exception) { + + $this->state = $this->states->stop_failed; + + throw new LifecycleExecutionException( + 'Lifecycle shutdown failed.', + previous: $exception + ); + + } + + } + + /** + * ### Gets the current lifecycle state + * + * Returns the concrete enumeration case supplied by the lifecycle owner. + * + * No state conversion or domain-specific mapping is performed. + * @since 1.0.0 + * + * @return TState Current lifecycle state. + */ + public function state ():UnitEnum { + + return $this->state; + + } + + /** + * ### Validates lifecycle component registration + * + * Ensures that lifecycle components can only be registered while the lifecycle remains in its initially created + * state. + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If registration is not allowed. + * + * @return void + */ + private function assertRegistrationAllowed ():void { + + if ($this->state !== $this->states->created) + throw new InvalidLifecycleStateException( + 'Lifecycle components cannot be registered after startup has begun.' + ); + + } + + /** + * ### Performs a guarded lifecycle transition + * + * Changes the lifecycle state only when its current state matches the expected state. + * @since 1.0.0 + * + * @param TState $expected

+ * Expected current state. + *

+ * @param TState $next

+ * Next lifecycle state. + *

+ * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If the current state does not match. + * + * @return void + */ + private function transition (UnitEnum $expected, UnitEnum $next):void { + + if ($this->state !== $expected) + throw new InvalidLifecycleStateException( + 'The requested lifecycle transition is not allowed.' + ); + + $this->state = $next; + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Exception/DistinctEnumCaseException.php b/src/Lifecycle/Exception/DistinctEnumCaseException.php new file mode 100644 index 0000000..27b2498 --- /dev/null +++ b/src/Lifecycle/Exception/DistinctEnumCaseException.php @@ -0,0 +1,26 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Represents an invalid lifecycle phase + * @since 1.0.0 + */ +final class DistinctEnumCaseException extends InvalidArgumentException { + + protected const string DEFAULT_MESSAGE = 'Each lifecycle phase must have a distinct enumeration case'; + +} \ No newline at end of file diff --git a/src/Lifecycle/Exception/InvalidLifecycleStateException.php b/src/Lifecycle/Exception/InvalidLifecycleStateException.php new file mode 100644 index 0000000..14b0674 --- /dev/null +++ b/src/Lifecycle/Exception/InvalidLifecycleStateException.php @@ -0,0 +1,32 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Represents an invalid lifecycle state transition + * + * Indicates that a lifecycle operation was requested while the lifecycle + * engine was in a state that does not permit the operation. + * + * This exception is independent of the lifecycle owner and may be translated + * into a domain-specific exception by the component exposing the lifecycle. + * @since 1.0.0 + */ +final class InvalidLifecycleStateException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'The lifecycle is in an invalid state'; + +} \ No newline at end of file diff --git a/src/Lifecycle/Exception/LifecycleExecutionException.php b/src/Lifecycle/Exception/LifecycleExecutionException.php new file mode 100644 index 0000000..e1c8412 --- /dev/null +++ b/src/Lifecycle/Exception/LifecycleExecutionException.php @@ -0,0 +1,31 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Represents a lifecycle execution failure + * + * Indicates that one or more registered lifecycle parts failed during startup or shutdown execution. + * + * The original failure is preserved as the previous exception, allowing lifecycle owners to expose domain-specific + * exceptions without losing the underlying cause. + * @since 1.0.0 + */ +final class LifecycleExecutionException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'One or more lifecycle parts failed during execution'; + +} \ No newline at end of file diff --git a/src/Lifecycle/Manager.php b/src/Lifecycle/Manager.php index 44e829b..a54483e 100644 --- a/src/Lifecycle/Manager.php +++ b/src/Lifecycle/Manager.php @@ -7,66 +7,71 @@ * @copyright 2026-present The FireHub Project - All rights reserved * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 * - * @php-version >=8.5 + * @php-version >=8.1 * @package Kernel */ namespace FireHub\Kernel\Lifecycle; -use FireHub\Core\Boundary\Kernel\ { - Bootstrapper, Terminator -}; -use FireHub\Kernel\Bootstrapper\Sequence as BootstrapperSequence; -use FireHub\Kernel\Terminator\Sequence as TerminatorSequence; -use Throwable; +use FireHub\Runtime; +use Closure, Throwable; /** - * ### Registers and executes kernel lifecycle components + * ### Manages lifecycle component execution * - * Executes bootstrappers and terminators according to their configured execution order. Components with equal order - * values retain their registration order. + * Registers startup and shutdown callbacks and execute them according to their configured execution order. * - * State validation belongs to the lifecycle controller, not this execution manager. + * Components with lower order values execute first. Components with equal order values retain their original + * registration order. + * + * Startup execution stops immediately when a component fails. + * + * Shutdown execution attempts every registered component even when one or more parts fail. The first captured + * failure is rethrown after all shutdown callbacks have been attempted. + * + * This manager does not maintain lifecycle states or enforce lifecycle transitions. These responsibilities belong to + * the lifecycle engine. * @since 1.0.0 * * @internal */ -final readonly class Manager { +final class Manager { /** - * ### Registered bootstrappers in registration order + * ### Registered startup callbacks * @since 1.0.0 + * + * @var list */ - private BootstrapperSequence $bootstrap_sequence; + private array $startup = []; /** - * ### Registered terminators in shutdown order + * ### Registered shutdown callbacks * @since 1.0.0 + * + * @var list */ - private TerminatorSequence $termination_sequence; + private array $shutdown = []; /** - * ### Constructor - * @since 1.0.0 + * ### Registration sequence * - * @return void + * Preserves deterministic execution order for components having identical order values. + * @since 1.0.0 */ - public function __construct () { - - $this->bootstrap_sequence = new BootstrapperSequence; - - $this->termination_sequence = new TerminatorSequence; - - } + private int $sequence = 0; /** - * ### Registers a bootstrapper - * @since 1.0.0 + * ### Registers a startup callback * - * @uses \FireHub\Kernel\Bootstrapper\Sequence::register() To register bootstrapper. + * Adds a callback to the startup execution sequence. + * + * Lower order values execute before higher order values. Callbacks have equal order values executed in their + * registration order. + * @since 1.0.0 * - * @param \FireHub\Core\Boundary\Kernel\Bootstrapper $bootstrapper

- * Bootstrapper to register. + * @param Closure():void $callback

+ * Startup callback. *

* @param int $order

* Execution order. @@ -74,20 +79,27 @@ public function __construct () { * * @return void */ - public function registerBootstrapper (Bootstrapper $bootstrapper, int $order):void { + public function registerStartup (Closure $callback, int $order):void { - $this->bootstrap_sequence->register($bootstrapper, $order); + $this->startup[] = [ + 'callback' => $callback, + 'order' => $order, + 'index' => $this->sequence++ + ]; } /** - * ### Registers a terminator - * @since 1.0.0 + * ### Registers a shutdown callback + * + * Adds a callback to the shutdown execution sequence. * - * @uses \FireHub\Kernel\Terminator\Sequence::register() To register terminator. + * Lower order values execute before higher order values. Callbacks have equal order values executed in their + * registration order. + * @since 1.0.0 * - * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

- * Terminator to register. + * @param Closure():void $callback

+ * Shutdown callback. *

* @param int $order

* Execution order. @@ -95,48 +107,99 @@ public function registerBootstrapper (Bootstrapper $bootstrapper, int $order):vo * * @return void */ - public function registerTerminator (Terminator $terminator, int $order):void { + public function registerShutdown (Closure $callback, int $order):void { - $this->termination_sequence->register($terminator, $order); + $this->shutdown[] = [ + 'callback' => $callback, + 'order' => $order, + 'index' => $this->sequence++ + ]; } /** - * ### Initializes registered components + * ### Executes startup callbacks * - * Executes the bootstrap sequence according to its configured execution order. + * Executes all registered startup callbacks in ascending order. * - * Initialization stops immediately if a bootstrapper throws an exception. + * Execution stops immediately when a callback throws an exception. The original exception is propagated to the + * caller. * @since 1.0.0 * - * @uses \FireHub\Kernel\Bootstrapper\Sequence::execute() To execute registered bootstrappers. + * @uses \FireHub\Kernel\Lifecycle\Manager::ordered() To order callbacks. + * + * @throws Throwable If a startup callback fails. * * @return void */ - public function initialize ():void { + public function start ():void { - $this->bootstrap_sequence->execute(); + foreach ($this->ordered($this->startup) as $entry) + ($entry['callback'])(); } /** - * ### Terminates registered components + * ### Executes shutdown callbacks * - * Executes the termination sequence according to its configured execution order. + * Executes all registered shutdown callbacks in ascending order. * - * The termination sequence is responsible for attempting all registered terminators and propagating execution - * failures. + * If a callback fails, execution continues with the remaining callbacks. The first captured failure is rethrown + * after every registered callback has been attempted. * @since 1.0.0 * - * @uses \FireHub\Kernel\Terminator\Sequence::execute() To execute registered terminators. + * @uses \FireHub\Kernel\Lifecycle\Manager::ordered() To order callbacks. * - * @throws Throwable If a terminator fails. + * @throws Throwable If a shutdown callback fails. * * @return void */ - public function terminate ():void { + public function stop ():void { + + $failure = null; + + foreach ($this->ordered($this->shutdown) as $entry) { + + try { + + ($entry['callback'])(); + + } catch (Throwable $exception) { + + $failure ??= $exception; + + } + + } + + if ($failure !== null) throw $failure; + + } + + /** + * ### Orders lifecycle callbacks + * + * Sorts callbacks by their configured execution order and uses their registration index to preserve + * deterministic ordering when priorities are equal. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Ordering::sortBy() To sort callbacks. + * + * @param list $entries + * Lifecycle callbacks. + * + * @return list Ordered lifecycle callbacks. + */ + private function ordered (array $entries):array { + + Runtime\Arr\Ordering::sortBy( + $entries, + static fn (array $a, array $b):int => ( + $a['order'] <=> $b['order']) ?: ($a['index'] <=> $b['index'] + ) + ); - $this->termination_sequence->execute(); + return $entries; } diff --git a/src/Lifecycle/States.php b/src/Lifecycle/States.php new file mode 100644 index 0000000..da169f2 --- /dev/null +++ b/src/Lifecycle/States.php @@ -0,0 +1,105 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle; + +use FireHub\Kernel\Lifecycle\Exception\DistinctEnumCaseException; +use UnitEnum; + +/** + * ### Defines lifecycle state mappings + * + * Associates the fundamental phases of a lifecycle with the states provided by a specific lifecycle owner. + * + * The mapping allows the lifecycle engine to operate independently of concrete state names, namespaces, and + * domain-specific terminology. + * + * Each lifecycle owner supplies its own enumeration values when constructing the lifecycle engine. The engine then + * uses these values directly without converting or mapping states during execution. + * + * The supplied states must belong to the same enumeration and represent distinct lifecycle phases. + * @since 1.0.0 + * + * @template TState of UnitEnum + */ +final readonly class States { + + /** + * ### Constructor + * + * Defines the states associated with the seven fundamental lifecycle phases. + * @since 1.0.0 + * + * @param TState $created

+ * Initial lifecycle state. + *

+ * @param TState $starting

+ * State while startup components are executing. + *

+ * @param TState $started

+ * State after successful startup. + *

+ * @param TState $stopping

+ * State while shutdown components are executing. + *

+ * @param TState $stopped

+ * State after successful shutdown. + *

+ * @param TState $start_failed

+ * State after unsuccessful startup. + *

+ * @param TState $stop_failed

+ * State after unsuccessful shutdown. + *

+ * + * @throws \FireHub\Kernel\Lifecycle\Exception\DistinctEnumCaseException If enum case is assigned to multiple + * phases. + * + * @return void + */ + public function __construct ( + public UnitEnum $created, + public UnitEnum $starting, + public UnitEnum $started, + public UnitEnum $stopping, + public UnitEnum $stopped, + public UnitEnum $start_failed, + public UnitEnum $stop_failed + ) { + + $states = [ + $created, + $starting, + $started, + $stopping, + $stopped, + $start_failed, + $stop_failed + ]; + + $names = []; + + foreach ($states as $state) { + + $key = $state::class.'::'.$state->name; + + if (isset($names[$key])) + throw new DistinctEnumCaseException; + + $names[$key] = true; + + } + + } + +} \ No newline at end of file diff --git a/src/Terminator/Sequence.php b/src/Terminator/Sequence.php deleted file mode 100644 index 169532f..0000000 --- a/src/Terminator/Sequence.php +++ /dev/null @@ -1,110 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel - */ - -namespace FireHub\Kernel\Terminator; - -use FireHub\Core\Boundary\Kernel\Terminator; -use FireHub\Runtime; -use Throwable; - -/** - * ### Manages the kernel termination sequence - * - * Registers and executes termination components, according to their configured execution order. - * - * Components with lower order values execute first. Components with equal order values retain their original - * registration order. - * - * Termination continues when individual components fail, allowing remaining components to release their resources. - * @since 1.0.0 - * - * @internal - */ -final class Sequence { - - /** - * ### Registered terminators - * @since 1.0.0 - * - * @var list - */ - private array $terminators = []; - - /** - * ### Registers a terminator - * @since 1.0.0 - * - * @param \FireHub\Core\Boundary\Kernel\Terminator $terminator

- * Termination component. - *

- * @param int $order

- * Execution order. - *

- * - * @return void - */ - public function register (Terminator $terminator, int $order):void { - - $this->terminators[] = [ - 'terminator' => $terminator, - 'order' => $order - ]; - - } - - /** - * ### Executes the termination sequence - * - * Executes terminators in ascending order. - * - * Attempts to execute every registered terminator even when one fails. The first captured exception is rethrown - * after execution completes. - * @since 1.0.0 - * - * @uses \FireHub\Runtime\Arr\Ordering::sortBy() Sorts an array by a user-defined comparison function. - * @uses \FireHub\Core\Boundary\Kernel\Terminator::terminate() To terminate component. - * - * @throws Throwable If a terminator fails. - * - * @return void - */ - public function execute ():void { - - $entries = $this->terminators; - - Runtime\Arr\Ordering::sortBy( - $entries, - static fn (array $a, array $b):int => $a['order'] <=> $b['order'] - ); - - $failure = null; - - foreach ($entries as $entry) { - - try { - - $entry['terminator']->terminate(); - - } catch (Throwable $exception) { - - $failure ??= $exception; - - } - - } - - if ($failure !== null) throw $failure; - - } - -} \ No newline at end of file From f85c9ededf8dafda24ce76e764b68acff8d0e540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 09:09:12 +0200 Subject: [PATCH 17/32] Remove redundant `storage` property and simplify lifecycle state handling Refactored the `Engine` class by removing the unused and unnecessary `storage` property, along with its associated anonymous class. Key updates: - **Lifecycle Simplification**: Streamlined the constructor by directly assigning the initial state without wrapping it in an intermediary object. - **Documentation Updates**: Removed the `@internal` annotation from `Engine` and added it to `Controller` for consistency with intended usage restrictions. These changes reduce complexity in the `Engine` implementation and align annotations with proper internal usage scopes. --- src/Lifecycle/Controller.php | 2 ++ src/Lifecycle/Engine.php | 18 ------------------ 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index 1a5118a..06190bb 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -38,6 +38,8 @@ * The controller does not implement lifecycle state transitions directly. All state management and component * execution are delegated to the engine. * @since 1.0.0 + * + * @internal */ final readonly class Controller implements Kernel { diff --git a/src/Lifecycle/Engine.php b/src/Lifecycle/Engine.php index 9c74b57..cf02e29 100644 --- a/src/Lifecycle/Engine.php +++ b/src/Lifecycle/Engine.php @@ -39,8 +39,6 @@ * failures at their public API boundaries. * * @template TState of UnitEnum - * - * @internal */ final class Engine { @@ -58,14 +56,6 @@ final class Engine { */ private Manager $manager; - /** - * ### Lifecycle state storage - * @since 1.0.0 - * - * @var object{state: TState} - */ - private object $storage; - /** * ### Constructor * @since 1.0.0 @@ -84,14 +74,6 @@ public function __construct ( $this->state = $states->created; - $this->storage = new class($states->created) { - - public function __construct ( - public UnitEnum $state - ) {} - - }; - } /** From 53ad96cb6751d35131e46313d451b5364a60eea2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 10:07:56 +0200 Subject: [PATCH 18/32] Integrate lifecycle dependency management and parent-child lifecycles Introduced support for declarative lifecycle dependency handling to enable better orchestration of parent-child lifecycle relationships. Key updates: - **New `Dependency` Classes**: Added `Lease`, `Registry`, and `Boundary` to manage lifecycle dependencies, enabling active tracking and safe termination of parent lifecycles. - **Dependency Constraints**: Prevented lifecycle termination while active dependencies exist, introducing `ActiveLifecycleDependenciesException` for enforcement. - **Parent Lifecycle Leasing**: Enabled dependent lifecycles to acquire and release parent lifecycle ties using the `Boundary` class, ensuring proper resource ownership. - **API Enhancements**: Added `dependencyBoundary()` in `Engine`, `Kernel`, and `Controller` for exposing dependency boundaries to clients. - **Simplified State Transitions**: Removed the `transition()` helper to streamline lifecycle state changes, directly updating states where applicable. These changes provide a robust dependency model, streamlining parent-child orchestration and improving lifecycle management safety and extensibility. --- src/Kernel.php | 20 +++ src/Lifecycle/Controller.php | 17 +++ src/Lifecycle/Dependency/Boundary.php | 74 ++++++++++ src/Lifecycle/Dependency/Lease.php | 88 ++++++++++++ src/Lifecycle/Dependency/Registry.php | 130 ++++++++++++++++++ src/Lifecycle/Engine.php | 117 +++++++++------- .../ActiveLifecycleDependenciesException.php | 28 ++++ 7 files changed, 427 insertions(+), 47 deletions(-) create mode 100644 src/Lifecycle/Dependency/Boundary.php create mode 100644 src/Lifecycle/Dependency/Lease.php create mode 100644 src/Lifecycle/Dependency/Registry.php create mode 100644 src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php diff --git a/src/Kernel.php b/src/Kernel.php index 6378832..0c2601e 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -16,6 +16,10 @@ use FireHub\Core\Boundary\Kernel as KernelBoundary; use FireHub\Core\Meta\Enum\Kernel\State; use FireHub\Kernel\Lifecycle\Controller; +use FireHub\Kernel\Lifecycle\Dependency\ { + Boundary, Registry +}; +use FireHub\Kernel\Exception\InvalidKernelStateException; /** * ### Manages the kernel lifecycle @@ -51,6 +55,22 @@ public function __construct () { } + /** + * ### Gets the kernel lifecycle dependency boundary + * + * Exposes the kernel as a parent lifecycle for dependent runtime components. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Kernel::dependencyBoundary() To get the dependency boundary. + * + * @return \FireHub\Kernel\Lifecycle\Dependency\Boundary Kernel lifecycle dependency boundary. + */ + public function dependencyBoundary ():Boundary { + + return $this->lifecycle->dependencyBoundary(); + + } + /** * ### Registers a kernel bootstrapper * @since 1.0.0 diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index 06190bb..eb418e4 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -18,6 +18,7 @@ Bootstrapper, Terminator }; use FireHub\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Lifecycle\Dependency\Boundary; use FireHub\Kernel\Exception\ { InvalidKernelStateException, KernelBootException, KernelShutdownException }; @@ -73,6 +74,22 @@ public function __construct () { } + /** + * ### Gets the kernel lifecycle dependency boundary + * + * Exposes the kernel as a parent lifecycle for dependent runtime components. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Engine::dependencyBoundary() To get the dependency boundary. + * + * @return \FireHub\Kernel\Lifecycle\Dependency\Boundary Kernel lifecycle dependency boundary. + */ + public function dependencyBoundary ():Boundary { + + return $this->lifecycle->dependencyBoundary(); + + } + /** * ### Registers a kernel bootstrapper * diff --git a/src/Lifecycle/Dependency/Boundary.php b/src/Lifecycle/Dependency/Boundary.php new file mode 100644 index 0000000..7e0f32b --- /dev/null +++ b/src/Lifecycle/Dependency/Boundary.php @@ -0,0 +1,74 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Dependency; + +use FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException; +use Closure; + +/** + * ### Provides lifecycle dependency access + * + * Exposes a restricted interface for acquiring dependencies on an active lifecycle without exposing its execution or + * state mutation operations. + * + * Dependent lifecycle engines use this boundary to acquire ownership of their parent lifecycle during execution. + * @since 1.0.0 + * + * @internal + */ +final readonly class Boundary { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param Closure():bool $isActive

+ * Parent lifecycle availability check. + *

+ * @param \FireHub\Kernel\Lifecycle\Dependency\Registry $registry

+ * Parent lifecycle dependency registry. + *

+ * + * @return void + */ + public function __construct ( + private Closure $isActive, + private Registry $registry + ) {} + + /** + * ### Acquires a lifecycle dependency + * + * Registers a dependency only when the owning lifecycle is active. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Dependency\Boundary::isActive() To check the parent lifecycle availability. + * @uses \FireHub\Kernel\Lifecycle\Dependency\Registry::acquire() To acquire a dependency lease. + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If the parent lifecycle is not active. + * + * @return \FireHub\Kernel\Lifecycle\Dependency\Lease Acquired dependency lease. + */ + public function acquire ():Lease { + + if (!($this->isActive)()) + throw new InvalidLifecycleStateException( + 'The parent lifecycle must be active before starting a dependent lifecycle.' + ); + + return $this->registry->acquire(); + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Dependency/Lease.php b/src/Lifecycle/Dependency/Lease.php new file mode 100644 index 0000000..3998b18 --- /dev/null +++ b/src/Lifecycle/Dependency/Lease.php @@ -0,0 +1,88 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Dependency; + +/** + * ### Represents an active lifecycle dependency + * + * Holds an active dependency registration until explicitly released. + * + * Releasing a lease is idempotent. + * @since 1.0.0 + * + * @internal + */ +final class Lease { + + /** + * ### Whether the lease has been released + * @since 1.0.0 + */ + private bool $released = false; + + /** + * ### Constructor + * @since 1.0.0 + * + * @param int $id

+ * Dependency identifier. + *

+ * @param \FireHub\Kernel\Lifecycle\Dependency\Registry $registry

+ * Owning dependency registry. + *

+ * + * @void + * + * @internal + */ + public function __construct ( + private readonly int $id, + private readonly Registry $registry + ) {} + + /** + * ### Releases the dependency + * + * Removes this dependency from its owning registry. + * + * Repeated calls have no effect. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Dependency\Registry::release() To release the dependency. + * + * @return void + */ + public function release ():void { + + if ($this->released) return; + + $this->registry->release($this->id); + + $this->released = true; + + } + + /** + * ### Checks whether the lease has been released + * @since 1.0.0 + * + * @return bool True when released. + */ + public function isReleased ():bool { + + return $this->released; + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Dependency/Registry.php b/src/Lifecycle/Dependency/Registry.php new file mode 100644 index 0000000..664e219 --- /dev/null +++ b/src/Lifecycle/Dependency/Registry.php @@ -0,0 +1,130 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Dependency; + +use FireHub\Runtime; +use FireHub\Kernel\Lifecycle\Exception\ActiveLifecycleDependenciesException; + +/** + * ### Tracks active lifecycle dependencies + * + * Maintains a registry of components that depend on an active lifecycle. + * + * Each registered dependency receives a lease that must be released when the dependent component has completed its + * lifecycle. + * + * The registry prevents the owning lifecycle from terminating while active dependencies remain. + * @since 1.0.0 + * + * @internal + */ +final class Registry { + + /** + * ### Active dependency leases + * @since 1.0.0 + * + * @var array + */ + private array $leases = []; + + /** + * ### Next lease identifier + * @since 1.0.0 + */ + private int $next_id = 0; + + /** + * ### Acquires a dependency lease + * + * Registers an active dependency and returns a lease representing its ownership. + * @since 1.0.0 + * + * @return \FireHub\Kernel\Lifecycle\Dependency\Lease Acquired dependency lease. + */ + public function acquire ():Lease { + + $id = ++$this->next_id; + + return $this->leases[$id] = new Lease( + $id, + $this + ); + + } + + /** + * ### Releases a dependency lease + * + * Removes the specified lease from the active dependency registry. + * @since 1.0.0 + * + * @param int $id Dependency lease identifier. + * + * @return void + */ + public function release (int $id):void { + + unset($this->leases[$id]); + + } + + /** + * ### Checks for active dependencies + * @since 1.0.0 + * + * @return bool True when at least one dependency remains active. + */ + public function hasActive ():bool { + + return $this->leases !== []; + + } + + /** + * ### Gets the number of active dependencies + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Inspection::count() To count the number of active dependencies. + * + * @return int Number of active dependencies. + */ + public function count ():int { + + return Runtime\Arr\Inspection::count($this->leases); + + } + + /** + * ### Ensures all dependencies have been released + * + * Prevents lifecycle termination while dependent components remain active. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Dependency\Registry::hasActive() To check for active dependencies. + * + * @throws \FireHub\Kernel\Lifecycle\Exception\ActiveLifecycleDependenciesException If active dependencies remain. + * + * @return void + */ + public function assertEmpty ():void { + + if ($this->hasActive()) + throw new ActiveLifecycleDependenciesException( + 'Cannot terminate lifecycle while dependent components are active.' + ); + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Engine.php b/src/Lifecycle/Engine.php index cf02e29..b091b2b 100644 --- a/src/Lifecycle/Engine.php +++ b/src/Lifecycle/Engine.php @@ -13,7 +13,9 @@ namespace FireHub\Kernel\Lifecycle; - +use FireHub\Kernel\Lifecycle\Dependency\ { + Boundary, Lease, Registry +}; use FireHub\Kernel\Lifecycle\Exception\ { InvalidLifecycleStateException, LifecycleExecutionException }; @@ -56,6 +58,18 @@ final class Engine { */ private Manager $manager; + /** + * ### Dependent lifecycle registry + * @since 1.0.0 + */ + private readonly Registry $dependencies; + + /** + * ### Active parent lifecycle dependency + * @since 1.0.0 + */ + private ?Lease $parent_lease = null; + /** * ### Constructor * @since 1.0.0 @@ -63,19 +77,43 @@ final class Engine { * @param \FireHub\Kernel\Lifecycle\States $states

* Lifecycle state configuration. *

+ * @param null|\FireHub\Kernel\Lifecycle\Dependency\Boundary $parent [optional]

+ * Parent lifecycle dependency boundary. + *

* * @return void */ public function __construct ( - private readonly States $states + private readonly States $states, + private readonly ?Boundary $parent = null ) { $this->manager = new Manager; + $this->dependencies = new Registry; + $this->state = $states->created; } + /** + * ### Gets the lifecycle dependency boundary + * + * Returns a restricted boundary that allows dependent lifecycle engines to acquire ownership while this + * lifecycle is active. + * @since 1.0.0 + * + * @return \FireHub\Kernel\Lifecycle\Dependency\Boundary Lifecycle dependency boundary. + */ + public function dependencyBoundary ():Boundary { + + return new Boundary( + fn ():bool => $this->state === $this->states->started, + $this->dependencies + ); + + } + /** * ### Registers a startup callback * @@ -150,7 +188,7 @@ public function registerShutdown (Closure $callback, int $order = 0):void { * Calling this method when the lifecycle is already started has no effect. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Engine::transition() To perform a guarded lifecycle transition. + * @uses \FireHub\Kernel\Lifecycle\Dependency\Boundary::acquire() To acquire a dependency lease. * @uses \FireHub\Kernel\Lifecycle\Manager::start() To execute startup callbacks. * * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If startup is not allowed. @@ -162,20 +200,19 @@ public function start ():void { if ($this->state === $this->states->started) return; - $this->transition( - $this->states->created, - $this->states->starting - ); + if ($this->state !== $this->states->created) + throw new InvalidLifecycleStateException( + 'Cannot start the lifecycle in the current state.' + ); + + $this->parent_lease = $this->parent?->acquire(); + + $this->state = $this->states->starting; try { $this->manager->start(); - $this->transition( - $this->states->starting, - $this->states->started - ); - } catch (Throwable $exception) { $this->state = $this->states->start_failed; @@ -187,6 +224,8 @@ public function start ():void { } + $this->state = $this->states->started; + } /** @@ -204,8 +243,9 @@ public function start ():void { * Calling this method when the lifecycle is already stopped has no effect. * @since 1.0.0 * + * @uses \FireHub\Kernel\Lifecycle\Dependency\Registry::hasActive() To check for active dependencies. * @uses \FireHub\Kernel\Lifecycle\Manager::stop() To execute shutdown callbacks. - * @uses \FireHub\Kernel\Lifecycle\Engine::transition() To perform a guarded lifecycle transition. + * @uses \FireHub\Kernel\Lifecycle\Dependency\Lease::release() To release the dependency lease. * * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If shutdown is not allowed. * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If a shutdown callback fails. @@ -222,7 +262,17 @@ public function stop ():void { && $this->state !== $this->states->start_failed ) throw new InvalidLifecycleStateException( - 'Lifecycle shutdown is not allowed in the current state.' + 'Cannot stop the lifecycle in the current state.' + ); + + if ($this->dependencies->hasActive()) + throw new InvalidLifecycleStateException( + 'Cannot stop the lifecycle while dependent lifecycles are active.' + ); + + if ($this->parent !== null && $this->parent_lease === null) + throw new InvalidLifecycleStateException( + 'Cannot stop a dependent lifecycle before its parent dependency has been acquired.' ); $this->state = $this->states->stopping; @@ -231,11 +281,6 @@ public function stop ():void { $this->manager->stop(); - $this->transition( - $this->states->stopping, - $this->states->stopped - ); - } catch (Throwable $exception) { $this->state = $this->states->stop_failed; @@ -247,6 +292,12 @@ public function stop ():void { } + $this->state = $this->states->stopped; + + $this->parent_lease?->release(); + + $this->parent_lease = null; + } /** @@ -285,32 +336,4 @@ private function assertRegistrationAllowed ():void { } - /** - * ### Performs a guarded lifecycle transition - * - * Changes the lifecycle state only when its current state matches the expected state. - * @since 1.0.0 - * - * @param TState $expected

- * Expected current state. - *

- * @param TState $next

- * Next lifecycle state. - *

- * - * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If the current state does not match. - * - * @return void - */ - private function transition (UnitEnum $expected, UnitEnum $next):void { - - if ($this->state !== $expected) - throw new InvalidLifecycleStateException( - 'The requested lifecycle transition is not allowed.' - ); - - $this->state = $next; - - } - } \ No newline at end of file diff --git a/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php b/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php new file mode 100644 index 0000000..50a6c08 --- /dev/null +++ b/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php @@ -0,0 +1,28 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel + */ + +namespace FireHub\Kernel\Lifecycle\Exception; + +use FireHub\Core\Exception\Runtime\InvalidArgumentException; + +/** + * ### Indicates active lifecycle dependencies + * + * Thrown when lifecycle termination is attempted while dependent components remain active. + * @since 1.0.0 + */ +final class ActiveLifecycleDependenciesException extends InvalidArgumentException { + + protected const string DEFAULT_MESSAGE = 'Cannot terminate lifecycle while dependent components are active'; + +} \ No newline at end of file From b2b67602611907dc7c98e7d3fdb08a4e4e89f1f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:15:54 +0200 Subject: [PATCH 19/32] fix(lifecycle): enforce parent dependencies and consistent shutdown semantics --- src/Lifecycle/Engine.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Lifecycle/Engine.php b/src/Lifecycle/Engine.php index b091b2b..f9a6224 100644 --- a/src/Lifecycle/Engine.php +++ b/src/Lifecycle/Engine.php @@ -151,7 +151,7 @@ public function registerStartup (Closure $callback, int $order = 0):void { * * Registration is permitted only before the lifecycle has started. * @since 1.0.0 - g + * * @uses \FireHub\Kernel\Lifecycle\Engine::assertRegistrationAllowed() To validate registration. * @uses \FireHub\Kernel\Lifecycle\Manager::registerShutdown() To register a shutdown callback. * @@ -238,7 +238,8 @@ public function start ():void { * * Successful execution transitions the lifecycle into the stopped state. * - * If shutdown execution fails, the lifecycle enters the stop-failed state. + * If shutdown execution fails, the lifecycle enters the stop-failed state. All shutdown callbacks are + * attempted, and the parent dependency is released even if one fails. * * Calling this method when the lifecycle is already stopped has no effect. * @since 1.0.0 @@ -265,15 +266,10 @@ public function stop ():void { 'Cannot stop the lifecycle in the current state.' ); - if ($this->dependencies->hasActive()) - throw new InvalidLifecycleStateException( - 'Cannot stop the lifecycle while dependent lifecycles are active.' - ); + $this->dependencies->assertEmpty(); if ($this->parent !== null && $this->parent_lease === null) - throw new InvalidLifecycleStateException( - 'Cannot stop a dependent lifecycle before its parent dependency has been acquired.' - ); + $this->parent_lease = $this->parent->acquire(); $this->state = $this->states->stopping; @@ -285,6 +281,10 @@ public function stop ():void { $this->state = $this->states->stop_failed; + // All shutdown callbacks were attempted; release the parent dependency. + $this->parent_lease?->release(); + $this->parent_lease = null; + throw new LifecycleExecutionException( 'Lifecycle shutdown failed.', previous: $exception From 5f1bed386ea2459cbf689cd9300b06a0d25d2b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:15:58 +0200 Subject: [PATCH 20/32] fix(lifecycle): enforce parent dependencies and consistent shutdown semantics --- .../Exception/ActiveLifecycleDependenciesException.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php b/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php index 50a6c08..fd81ed8 100644 --- a/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php +++ b/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php @@ -13,7 +13,6 @@ namespace FireHub\Kernel\Lifecycle\Exception; -use FireHub\Core\Exception\Runtime\InvalidArgumentException; /** * ### Indicates active lifecycle dependencies @@ -21,7 +20,7 @@ * Thrown when lifecycle termination is attempted while dependent components remain active. * @since 1.0.0 */ -final class ActiveLifecycleDependenciesException extends InvalidArgumentException { +final class ActiveLifecycleDependenciesException extends InvalidLifecycleStateException { protected const string DEFAULT_MESSAGE = 'Cannot terminate lifecycle while dependent components are active'; From 3cf099f8fbf02d0e83456d9db7aa5761d157cdb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:16:01 +0200 Subject: [PATCH 21/32] fix(lifecycle): enforce parent dependencies and consistent shutdown semantics --- src/Lifecycle/States.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Lifecycle/States.php b/src/Lifecycle/States.php index da169f2..66ec779 100644 --- a/src/Lifecycle/States.php +++ b/src/Lifecycle/States.php @@ -27,7 +27,8 @@ * Each lifecycle owner supplies its own enumeration values when constructing the lifecycle engine. The engine then * uses these values directly without converting or mapping states during execution. * - * The supplied states must belong to the same enumeration and represent distinct lifecycle phases. + * The supplied states must represent distinct lifecycle phases. Each lifecycle owner normally supplies cases + * from a single enumeration. * @since 1.0.0 * * @template TState of UnitEnum From a502058ef5917749baf71056216d20b1f3164660 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:16:04 +0200 Subject: [PATCH 22/32] fix(lifecycle): enforce parent dependencies and consistent shutdown semantics --- src/Kernel.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Kernel.php b/src/Kernel.php index 0c2601e..510aa50 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -17,9 +17,8 @@ use FireHub\Core\Meta\Enum\Kernel\State; use FireHub\Kernel\Lifecycle\Controller; use FireHub\Kernel\Lifecycle\Dependency\ { - Boundary, Registry + Boundary }; -use FireHub\Kernel\Exception\InvalidKernelStateException; /** * ### Manages the kernel lifecycle @@ -61,7 +60,7 @@ public function __construct () { * Exposes the kernel as a parent lifecycle for dependent runtime components. * @since 1.0.0 * - * @uses \FireHub\Kernel\Kernel::dependencyBoundary() To get the dependency boundary. + * @uses \FireHub\Kernel\Lifecycle\Controller::dependencyBoundary() To get the dependency boundary. * * @return \FireHub\Kernel\Lifecycle\Dependency\Boundary Kernel lifecycle dependency boundary. */ From ac92d743552dbba4c4af747df1827b759d345d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:16:12 +0200 Subject: [PATCH 23/32] fix(lifecycle): allow specialized invalid state exceptions --- src/Lifecycle/Exception/InvalidLifecycleStateException.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lifecycle/Exception/InvalidLifecycleStateException.php b/src/Lifecycle/Exception/InvalidLifecycleStateException.php index 14b0674..16bf380 100644 --- a/src/Lifecycle/Exception/InvalidLifecycleStateException.php +++ b/src/Lifecycle/Exception/InvalidLifecycleStateException.php @@ -25,7 +25,7 @@ * into a domain-specific exception by the component exposing the lifecycle. * @since 1.0.0 */ -final class InvalidLifecycleStateException extends RuntimeException { +class InvalidLifecycleStateException extends RuntimeException { protected const string DEFAULT_MESSAGE = 'The lifecycle is in an invalid state'; From 3487e7d049cc533012b1a7d8ed05339d65c9a721 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Mon, 19 Oct 2026 10:16:28 +0200 Subject: [PATCH 24/32] refactor(kernel): remove superseded lifecycle state machine --- src/Lifecycle.php | 176 ---------------------------------------------- 1 file changed, 176 deletions(-) delete mode 100644 src/Lifecycle.php diff --git a/src/Lifecycle.php b/src/Lifecycle.php deleted file mode 100644 index 946ecc8..0000000 --- a/src/Lifecycle.php +++ /dev/null @@ -1,176 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel - */ - -namespace FireHub\Kernel; - -use FireHub\Core\Meta\Enum\Kernel\State; -use FireHub\Kernel\Exception\InvalidKernelStateException; - -/** - * ### Manages kernel lifecycle state transitions - * - * Encapsulates the kernel state machine and ensures that lifecycle transitions happen only from valid states. This - * class does not execute lifecycle components. - * @since 1.0.0 - * - * @internal - */ -final class Lifecycle { - - /** - * ### Current lifecycle state - * @since 1.0.0 - */ - private State $state = State::CREATED; - - /** - * ### Gets the current lifecycle state - * @since 1.0.0 - * - * @return State Current lifecycle state. - */ - public function state ():State { - - return $this->state; - - } - - /** - * ### Begins kernel initialization - * @since 1.0.0 - * - * @uses \FireHub\Kernel\Lifecycle::transition() Performs a guarded lifecycle transition. - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel cannot begin booting. - * - * @return void - */ - public function beginBoot ():void { - - $this->transition(State::CREATED, State::BOOTING); - - } - - /** - * ### Completes kernel initialization - * @since 1.0.0 - * - * @uses \FireHub\Kernel\Lifecycle::transition() Performs a guarded lifecycle transition. - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is not booting. - * - * @return void - */ - public function completeBoot ():void { - - $this->transition(State::BOOTING, State::BOOTED); - - } - - /** - * ### Begins kernel termination - * - * Shutdown may begin before initialization, after a successful initialization, - * or after an initialization failure. - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. - * - * @return void - */ - public function beginShutdown ():void { - - if ($this->state !== State::CREATED && $this->state !== State::BOOTED && $this->state !== State::BOOT_FAILED) - throw new InvalidKernelStateException( - 'Cannot begin kernel shutdown in the current state.', - [ - 'state' => $this->state - ] - ); - - $this->state = State::SHUTTING_DOWN; - - } - - /** - * ### Completes kernel termination - * @since 1.0.0 - * - * @uses \FireHub\Kernel\Lifecycle::transition() Performs a guarded lifecycle transition. - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the kernel is not shutting down. - * - * @return void - */ - public function completeShutdown ():void { - - $this->transition(State::SHUTTING_DOWN, State::TERMINATED); - - } - - /** - * ### Marks an active lifecycle operation as failed - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If no lifecycle operation is active. - * - * @return void - */ - public function fail ():void { - - $this->state = match ($this->state) { - State::BOOTING => State::BOOT_FAILED, - State::SHUTTING_DOWN => State::SHUTDOWN_FAILED, - default => throw new InvalidKernelStateException( - 'Cannot mark the kernel as failed outside an active lifecycle operation.', - [ - 'state' => $this->state - ] - ) - - }; - - } - - /** - * ### Performs a guarded lifecycle transition - * @since 1.0.0 - * - * @param State $expected

- * Expected current state. - *

- * @param State $next

- * Next state. - *

- * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If the current state does not match. - * - * @return void - */ - private function transition (State $expected, State $next):void { - - if ($this->state !== $expected) - throw new InvalidKernelStateException( - 'Cannot perform the requested kernel lifecycle transition.', - [ - 'state' => $this->state, - 'expected' => $expected, - 'next' => $next - ] - ); - - $this->state = $next; - - } - -} \ No newline at end of file From 622478ea93c84dbb0f5a13665feeb2d68b891516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 10:24:00 +0200 Subject: [PATCH 25/32] Remove unused `isReleased` method and redundant comments; streamline imports Simplified code by removing the unused `isReleased()` method from the `Lease` class and cleaning up related comments in the `Engine`. Updated `Kernel` to streamline `use` statements by collapsing multi-line imports. These changes enhance code readability and maintainability without altering functionality. --- src/Kernel.php | 4 +--- src/Lifecycle/Dependency/Lease.php | 12 ------------ src/Lifecycle/Engine.php | 2 -- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/Kernel.php b/src/Kernel.php index 510aa50..06d77fa 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -16,9 +16,7 @@ use FireHub\Core\Boundary\Kernel as KernelBoundary; use FireHub\Core\Meta\Enum\Kernel\State; use FireHub\Kernel\Lifecycle\Controller; -use FireHub\Kernel\Lifecycle\Dependency\ { - Boundary -}; +use FireHub\Kernel\Lifecycle\Dependency\Boundary; /** * ### Manages the kernel lifecycle diff --git a/src/Lifecycle/Dependency/Lease.php b/src/Lifecycle/Dependency/Lease.php index 3998b18..cd8b36e 100644 --- a/src/Lifecycle/Dependency/Lease.php +++ b/src/Lifecycle/Dependency/Lease.php @@ -73,16 +73,4 @@ public function release ():void { } - /** - * ### Checks whether the lease has been released - * @since 1.0.0 - * - * @return bool True when released. - */ - public function isReleased ():bool { - - return $this->released; - - } - } \ No newline at end of file diff --git a/src/Lifecycle/Engine.php b/src/Lifecycle/Engine.php index f9a6224..173deed 100644 --- a/src/Lifecycle/Engine.php +++ b/src/Lifecycle/Engine.php @@ -280,8 +280,6 @@ public function stop ():void { } catch (Throwable $exception) { $this->state = $this->states->stop_failed; - - // All shutdown callbacks were attempted; release the parent dependency. $this->parent_lease?->release(); $this->parent_lease = null; From 67fd12698aa146b11330a0989203844e11bcfaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 10:55:01 +0200 Subject: [PATCH 26/32] Refactor lifecycle engine to simplify state handling and extend operation support Revised `Engine` to streamline lifecycle state management and allow flexible operation execution. Key updates: - **State Simplification**: Replaced the `States` wrapper with direct enumeration-based state handling, reducing constructor complexity. - **New Execution Method**: Added `execute()` in `Engine` to handle state validation, transitions, and callbacks with support for parent dependencies and dependent lifecycle checks. - **Dependency API Enhancements**: Updated `dependencyBoundary()` to include state-based availability checks. - **Parent Lifecycle Logic**: Consolidated parent lifecycle lease acquisition and release logic into dedicated methods. - **Operation Safeguards**: Introduced guards to prevent concurrent operations and ensure registration remains immutable after execution starts. Adjusted `Controller` to adopt these changes, leveraging the unified `execute()` method for managing kernel startup and shutdown. These enhancements improve code clarity, maintainability, and lifecycle orchestration accuracy. --- src/Lifecycle/Controller.php | 45 ++++-- src/Lifecycle/Engine.php | 266 +++++++++++++++++++++-------------- 2 files changed, 194 insertions(+), 117 deletions(-) diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index eb418e4..149ce6b 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -60,17 +60,7 @@ */ public function __construct () { - $this->lifecycle = new Engine( - new States( - State::CREATED, - State::BOOTING, - State::BOOTED, - State::SHUTTING_DOWN, - State::TERMINATED, - State::BOOT_FAILED, - State::SHUTDOWN_FAILED - ) - ); + $this->lifecycle = new Engine(State::CREATED); } @@ -86,7 +76,9 @@ public function __construct () { */ public function dependencyBoundary ():Boundary { - return $this->lifecycle->dependencyBoundary(); + return $this->lifecycle->dependencyBoundary( + static fn (State $state):bool => $state === State::BOOTED + ); } @@ -184,7 +176,9 @@ static function () use ($terminator):void { * Executes registered bootstrappers and transitions the kernel into its booted state when initialization succeeds. * @since 1.0.0 * + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To check the current kernel state. * @uses \FireHub\Kernel\Lifecycle\Engine::start() To execute registered bootstrappers. + * @uses \FireHub\Kernel\Lifecycle\Engine::execute() To execute a callback. * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If booting is not allowed. * @throws \FireHub\Kernel\Exception\KernelBootException If a bootstrapper fails. @@ -193,9 +187,17 @@ static function () use ($terminator):void { */ public function boot ():void { + if ($this->state() === State::BOOTED) return; + try { - $this->lifecycle->start(); + $this->lifecycle->execute( + callback: fn() => $this->lifecycle->start(), + from: [State::CREATED], + entering: State::BOOTING, + success: State::BOOTED, + failure: State::BOOT_FAILED + ); } catch (InvalidLifecycleStateException $exception) { @@ -221,6 +223,8 @@ public function boot ():void { * Executes registered terminators and transitions the kernel into its terminated state when shutdown succeeds. * @since 1.0.0 * + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To check the current kernel state. + * @uses \FireHub\Kernel\Lifecycle\Engine::execute() To execute a callback. * @uses \FireHub\Kernel\Lifecycle\Engine::stop() To execute registered terminators. * * @throws \FireHub\Kernel\Exception\InvalidKernelStateException If shutdown is not allowed. @@ -230,9 +234,22 @@ public function boot ():void { */ public function shutdown ():void { + if ($this->state() === State::TERMINATED) return; + try { - $this->lifecycle->stop(); + $this->lifecycle->execute( + callback: fn() => $this->lifecycle->stop(), + from: [ + State::CREATED, + State::BOOTED, + State::BOOT_FAILED + ], + entering: State::SHUTTING_DOWN, + success: State::TERMINATED, + failure: State::SHUTDOWN_FAILED, + require_no_dependents: true + ); } catch (InvalidLifecycleStateException $exception) { diff --git a/src/Lifecycle/Engine.php b/src/Lifecycle/Engine.php index 173deed..3a243fe 100644 --- a/src/Lifecycle/Engine.php +++ b/src/Lifecycle/Engine.php @@ -19,26 +19,21 @@ use FireHub\Kernel\Lifecycle\Exception\ { InvalidLifecycleStateException, LifecycleExecutionException }; +use FireHub\Runtime; use Closure, Throwable, UnitEnum; /** * ### Coordinates a configurable lifecycle * - * Provides a reusable lifecycle state machine and execution controller independent of the component owning the - * lifecycle. + * Provides generic lifecycle execution and state management independently of the component owning the lifecycle. * - * Each engine instance maintains its own state, startup callbacks, and shutdown callbacks. + * Lifecycle states are represented by an arbitrary enumeration. * - * Lifecycle states are supplied during construction and remain associated with the engine throughout its lifetime. + * Each operation defines its permitted source states, successful state, failure state, and optional execution state. * - * Startup and shutdown operations are guarded by explicit state transitions. Startup failures prevent further - * initialization, while shutdown remains available after a startup failure to allow resource cleanup. - * - * Repeated startup calls after a successful startup and repeated shutdown calls after a successful shutdown are - * treated as no-ops. - * - * The engine does not expose domain-specific exceptions. Lifecycle owners are responsible for translating generic - * failures at their public API boundaries. + * The engine also manages parent lifecycle dependencies and prevents termination while dependent lifecycles remain + * active. + * @since 1.0.0 * * @template TState of UnitEnum */ @@ -70,12 +65,24 @@ final class Engine { */ private ?Lease $parent_lease = null; + /** + * ### Whether an operation is executing + * @since 1.0.0 + */ + private bool $executing = false; + + /** + * ### Whether component registration is closed + * @since 1.0.0 + */ + private bool $registration_closed = false; + /** * ### Constructor * @since 1.0.0 * - * @param \FireHub\Kernel\Lifecycle\States $states

- * Lifecycle state configuration. + * @param TState $initial

+ * Initial lifecycle state. *

* @param null|\FireHub\Kernel\Lifecycle\Dependency\Boundary $parent [optional]

* Parent lifecycle dependency boundary. @@ -84,31 +91,34 @@ final class Engine { * @return void */ public function __construct ( - private readonly States $states, + private readonly UnitEnum $initial, private readonly ?Boundary $parent = null ) { + $this->state = $initial; + $this->manager = new Manager; $this->dependencies = new Registry; - $this->state = $states->created; - } /** * ### Gets the lifecycle dependency boundary * - * Returns a restricted boundary that allows dependent lifecycle engines to acquire ownership while this - * lifecycle is active. + * Exposes dependency acquisition while the owning lifecycle is available to dependent components. * @since 1.0.0 * + * @param Closure(TState):bool $is_available

+ * Determines whether the current state accepts new dependencies. + *

+ * * @return \FireHub\Kernel\Lifecycle\Dependency\Boundary Lifecycle dependency boundary. */ - public function dependencyBoundary ():Boundary { + public function dependencyBoundary (Closure $is_available):Boundary { return new Boundary( - fn ():bool => $this->state === $this->states->started, + fn ():bool => $is_available($this->state), $this->dependencies ); @@ -175,135 +185,151 @@ public function registerShutdown (Closure $callback, int $order = 0):void { } /** - * ### Starts the lifecycle - * - * Transitions the lifecycle from its created state into the starting state and executes registered startup - * callbacks. - * - * Successful execution transitions the lifecycle into the started state. - * - * If execution fails, the lifecycle enters the start-failed state and the original failure is preserved as the - * previous exception. - * - * Calling this method when the lifecycle is already started has no effect. + * ### Executes registered startup callbacks * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Dependency\Boundary::acquire() To acquire a dependency lease. * @uses \FireHub\Kernel\Lifecycle\Manager::start() To execute startup callbacks. * - * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If startup is not allowed. - * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If a startup callback fails. + * @throws Throwable If a startup callback fails. * * @return void */ public function start ():void { - if ($this->state === $this->states->started) return; - - if ($this->state !== $this->states->created) - throw new InvalidLifecycleStateException( - 'Cannot start the lifecycle in the current state.' - ); - - $this->parent_lease = $this->parent?->acquire(); - - $this->state = $this->states->starting; - - try { + $this->manager->start(); - $this->manager->start(); - - } catch (Throwable $exception) { - - $this->state = $this->states->start_failed; - - throw new LifecycleExecutionException( - 'Lifecycle startup failed.', - previous: $exception - ); + } - } + /** + * ### Executes registered shutdown callbacks + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Manager::stop() To execute shutdown callbacks. + * + * @throws Throwable If a shutdown callback fails. + * + * @return void + */ + public function stop ():void { - $this->state = $this->states->started; + $this->manager->stop(); } /** - * ### Stops the lifecycle + * ### Executes a lifecycle operation * - * Begins lifecycle shutdown and executes registered shutdown callbacks. + * Validates the current state and executes the supplied callback. * - * Shutdown is permitted from the created, started, and start-failed states. This allows cleanup even when the - * startup did not complete successfully. + * An optional entering state represents an operation in progress. * - * Successful execution transitions the lifecycle into the stopped state. + * On successful execution, the lifecycle transitions to the success state. On failure, it transitions to the + * failure state and propagates the original exception through LifecycleExecutionException. * - * If shutdown execution fails, the lifecycle enters the stop-failed state. All shutdown callbacks are - * attempted, and the parent dependency is released even if one fails. - * - * Calling this method when the lifecycle is already stopped has no effect. + * Parent dependency acquisition and release are explicitly controlled by the operation rather than inferred from + * enumeration case names. * @since 1.0.0 * - * @uses \FireHub\Kernel\Lifecycle\Dependency\Registry::hasActive() To check for active dependencies. - * @uses \FireHub\Kernel\Lifecycle\Manager::stop() To execute shutdown callbacks. - * @uses \FireHub\Kernel\Lifecycle\Dependency\Lease::release() To release the dependency lease. + * @uses \FireHub\Kernel\Lifecycle\Engine::assertStateType() To validate state types. + * @uses \FireHub\Kernel\Lifecycle\Engine::releaseParent() To release the parent dependency. + * @uses \FireHub\Kernel\Lifecycle\Dependency\Registry::assertEmpty() To validate no dependent lifecycles remain. + * @uses \FireHub\Kernel\Lifecycle\Dependency\Boundary::acquire() To acquire a parent dependency lease. + * @uses \FireHub\Runtime\Arr\Inspection::inArray() To check if the current state is in the allowed states. + * + * @param Closure():void $callback

+ * Operation callback. + *

+ * @param non-empty-list $from

+ * States from which execution is permitted. + *

+ * @param TState $success

+ * State after successful execution. + *

+ * @param TState $failure

+ * State after failed execution. + *

+ * @param null|TState $entering [optional]

+ * State while the operation is executing. + *

+ * @param bool $acquire_parent [optional]

+ * Acquire a parent dependency before execution. + *

+ * @param bool $release_parent [optional]

+ * Release the parent dependency after execution, including failure. + *

+ * @param bool $require_no_dependents [optional]

+ * Reject execution while dependent lifecycles remain active. + *

* - * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If shutdown is not allowed. - * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If a shutdown callback fails. + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If execution is not permitted. + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If the operation callback fails. * * @return void */ - public function stop ():void { + public function execute (Closure $callback, array $from, UnitEnum $success, UnitEnum $failure, ?UnitEnum $entering = null, bool $acquire_parent = false, bool $release_parent = false, bool $require_no_dependents = false):void { - if ($this->state === $this->states->stopped) return; + if ($this->executing) + throw new InvalidLifecycleStateException( + 'A lifecycle operation is already executing.' + ); + + $this->assertStateType($success); + $this->assertStateType($failure); - if ( - $this->state !== $this->states->created - && $this->state !== $this->states->started - && $this->state !== $this->states->start_failed - ) + if ($entering !== null) + $this->assertStateType($entering); + + foreach ($from as $allowed) + $this->assertStateType($allowed); + + if ($from === [] || !Runtime\Arr\Inspection::inArray($from, $this->state)) throw new InvalidLifecycleStateException( - 'Cannot stop the lifecycle in the current state.' + 'The lifecycle operation is not allowed in the current state.' ); - $this->dependencies->assertEmpty(); + if ($require_no_dependents) + $this->dependencies->assertEmpty(); + + if ($acquire_parent && $this->parent_lease === null) + $this->parent_lease = $this->parent?->acquire(); - if ($this->parent !== null && $this->parent_lease === null) - $this->parent_lease = $this->parent->acquire(); + $this->registration_closed = true; + $this->executing = true; - $this->state = $this->states->stopping; + if ($entering !== null) + $this->state = $entering; try { - $this->manager->stop(); + $callback(); } catch (Throwable $exception) { - $this->state = $this->states->stop_failed; - $this->parent_lease?->release(); - $this->parent_lease = null; + $this->state = $failure; + + if ($release_parent) + $this->releaseParent(); throw new LifecycleExecutionException( - 'Lifecycle shutdown failed.', + 'Lifecycle operation failed.', previous: $exception ); - } + } finally { - $this->state = $this->states->stopped; + $this->executing = false; - $this->parent_lease?->release(); + } - $this->parent_lease = null; + $this->state = $success; + + if ($release_parent) + $this->releaseParent(); } /** * ### Gets the current lifecycle state - * - * Returns the concrete enumeration case supplied by the lifecycle owner. - * - * No state conversion or domain-specific mapping is performed. * @since 1.0.0 * * @return TState Current lifecycle state. @@ -316,9 +342,6 @@ public function state ():UnitEnum { /** * ### Validates lifecycle component registration - * - * Ensures that lifecycle components can only be registered while the lifecycle remains in its initially created - * state. * @since 1.0.0 * * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If registration is not allowed. @@ -327,11 +350,48 @@ public function state ():UnitEnum { */ private function assertRegistrationAllowed ():void { - if ($this->state !== $this->states->created) + if ($this->registration_closed || $this->executing) throw new InvalidLifecycleStateException( - 'Lifecycle components cannot be registered after startup has begun.' + 'Lifecycle components cannot be registered after execution has begun.' ); } + /** + * ### Validates the lifecycle state enumeration + * @since 1.0.0 + * + * @param UnitEnum $state

+ * State to validate. + *

+ * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If the state is not of the expected type. + * + * @return void + */ + private function assertStateType (UnitEnum $state):void { + + if ($state::class !== $this->initial::class) + throw new InvalidLifecycleStateException( + 'Lifecycle states must belong to the same enumeration.' + ); + + } + + /** + * ### Releases the parent lifecycle dependency + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Dependency\Lease::release() To release the parent lease. + * + * @return void + */ + private function releaseParent ():void { + + $this->parent_lease?->release(); + + $this->parent_lease = null; + + } + } \ No newline at end of file From ed15bd77f1128f37ae73cb307a4d4663877d7217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 13:00:27 +0200 Subject: [PATCH 27/32] Remove unit tests for kernel sequence and lifecycle; introduce `AnotherState` stub Deleted the outdated sequence and lifecycle unit tests (`Kernel/tests/Unit/Bootstrapper/SequenceTest.php`, `Kernel/tests/Unit/Terminator/SequenceTest.php`, `Kernel/tests/Unit/LifecycleTest.php`) to reduce redundant coverage and align the test suite with the latest architecture changes. Added a lightweight `AnotherState` stub (`Kernel/tests/Stubs/AnotherState.php`) for validating alternative lifecycle state scenarios. Updated `ManagerTest` for streamlined startup/shutdown methods, ensuring alignment with revised lifecycle execution logic. --- tests/Stubs/AnotherState.php | 24 ++ tests/Stubs/BasicBootstrapper.php | 45 --- tests/Stubs/BasicTerminator.php | 45 --- tests/Stubs/State.php | 28 ++ tests/Unit/Bootstrapper/SequenceTest.php | 158 --------- tests/Unit/Lifecycle/EngineTest.php | 368 +++++++++++++++++++++ tests/Unit/Lifecycle/ManagerTest.php | 209 ++++++------ tests/Unit/LifecycleTest.php | 403 ----------------------- tests/Unit/Terminator/SequenceTest.php | 328 ------------------ 9 files changed, 527 insertions(+), 1081 deletions(-) create mode 100644 tests/Stubs/AnotherState.php delete mode 100644 tests/Stubs/BasicBootstrapper.php delete mode 100644 tests/Stubs/BasicTerminator.php create mode 100644 tests/Stubs/State.php delete mode 100644 tests/Unit/Bootstrapper/SequenceTest.php create mode 100644 tests/Unit/Lifecycle/EngineTest.php delete mode 100644 tests/Unit/LifecycleTest.php delete mode 100644 tests/Unit/Terminator/SequenceTest.php diff --git a/tests/Stubs/AnotherState.php b/tests/Stubs/AnotherState.php new file mode 100644 index 0000000..69631c4 --- /dev/null +++ b/tests/Stubs/AnotherState.php @@ -0,0 +1,24 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Stubs; + +/** + * ### Alternative lifecycle states + * @since 1.0.0 + */ +enum AnotherState { + + case DONE; + +} \ No newline at end of file diff --git a/tests/Stubs/BasicBootstrapper.php b/tests/Stubs/BasicBootstrapper.php deleted file mode 100644 index 66e632f..0000000 --- a/tests/Stubs/BasicBootstrapper.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=7.0 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Stubs; - -use FireHub\Kernel\Bootstrapper; - -/** - * ### Basic bootstrapper - * @since 1.0.0 - */ -class BasicBootstrapper implements Bootstrapper { - - /** - * @since 1.0.0 - * - * @return void - */ - public function __construct ( - private readonly mixed $data, - private array &$executed - ) {} - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function bootstrap ():void { - - $this->executed[] = $this->data; - - } - -} \ No newline at end of file diff --git a/tests/Stubs/BasicTerminator.php b/tests/Stubs/BasicTerminator.php deleted file mode 100644 index 551a135..0000000 --- a/tests/Stubs/BasicTerminator.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=7.0 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Stubs; - -use FireHub\Kernel\Terminator; - -/** - * ### Basic terminator - * @since 1.0.0 - */ -class BasicTerminator implements Terminator { - - /** - * @since 1.0.0 - * - * @return void - */ - public function __construct ( - private readonly mixed $data, - private array &$executed - ) {} - - /** - * @inheritDoc - * - * @since 1.0.0 - */ - public function terminate ():void { - - $this->executed[] = $this->data; - - } - -} \ No newline at end of file diff --git a/tests/Stubs/State.php b/tests/Stubs/State.php new file mode 100644 index 0000000..2d597b2 --- /dev/null +++ b/tests/Stubs/State.php @@ -0,0 +1,28 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Stubs; + +/** + * ### Lifecycle states + * @since 1.0.0 + */ +enum State { + + case CREATED; + case STARTING; + case RUNNING; + case STOPPED; + case FAILED; + +} \ No newline at end of file diff --git a/tests/Unit/Bootstrapper/SequenceTest.php b/tests/Unit/Bootstrapper/SequenceTest.php deleted file mode 100644 index 4cc3cc7..0000000 --- a/tests/Unit/Bootstrapper/SequenceTest.php +++ /dev/null @@ -1,158 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Unit\Bootstrapper; - -use FireHub\Testing\FireHubTestCase; -use FireHub\Kernel\Bootstrapper; -use FireHub\Kernel\Bootstrapper\Sequence; -use FireHub\Tests\Kernel\Stubs\BasicBootstrapper; -use PHPUnit\Framework\Attributes\ { - CoversClass, Group, Small -}; - -/** - * ### Tests kernel bootstrap sequence - * @since 1.0.0 - */ -#[Small] -#[Group('bootstrapper')] -#[CoversClass(Sequence::class)] -final class SequenceTest extends FireHubTestCase { - - /** - * ### Tests execution of an empty sequence - * @since 1.0.0 - * - * @return void - */ - public function testEmptySequence ():void { - - $sequence = new Sequence; - - $sequence->execute(); - - self::assertTrue(true); - - } - - /** - * ### Tests execution order - * @since 1.0.0 - * - * @return void - */ - public function testExecutionOrder ():void { - - $sequence = new Sequence; - - $executed = []; - - foreach ([30, 10, 20] as $order) { - - $sequence->register( - new BasicBootstrapper($order, $executed), - $order - ); - - } - - $sequence->execute(); - - self::assertSame([10, 20, 30], $executed); - - } - - /** - * ### Tests stable ordering for equal priorities - * @since 1.0.0 - * - * @return void - */ - public function testStableExecutionOrder ():void { - - $sequence = new Sequence; - - $executed = []; - - foreach (['first', 'second', 'third'] as $name) { - - $sequence->register( - new BasicBootstrapper($name, $executed), - 10 - ); - - } - - $sequence->execute(); - - self::assertSame(['first', 'second', 'third'], $executed); - - } - - /** - * ### Tests that each bootstrapper executes once - * @since 1.0.0 - * - * @return void - */ - public function testBootstrapperExecutesOnce ():void { - - $sequence = new Sequence; - - $bootstrapper = $this->createMock(Bootstrapper::class); - - $bootstrapper->expects($this->once()) - ->method('bootstrap'); - - $sequence->register($bootstrapper, 0); - - $sequence->execute(); - - } - - /** - * ### Tests execution stops when a bootstrapper fails - * @since 1.0.0 - * - * @return void - */ - public function testExecutionStopsOnFailure ():void { - - $sequence = new Sequence; - - $first = $this->createMock(Bootstrapper::class); - $failing = $this->createMock(Bootstrapper::class); - $last = $this->createMock(Bootstrapper::class); - - $first->expects($this->once()) - ->method('bootstrap'); - - $failing->expects($this->once()) - ->method('bootstrap') - ->willThrowException(new \RuntimeException('Bootstrap failed.')); - - $last->expects($this->never()) - ->method('bootstrap'); - - $sequence->register($first, 10); - $sequence->register($failing, 20); - $sequence->register($last, 30); - - $this->expectException(\RuntimeException::class); - - $sequence->execute(); - - } - -} \ No newline at end of file diff --git a/tests/Unit/Lifecycle/EngineTest.php b/tests/Unit/Lifecycle/EngineTest.php new file mode 100644 index 0000000..0c1a1b3 --- /dev/null +++ b/tests/Unit/Lifecycle/EngineTest.php @@ -0,0 +1,368 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Kernel\Tests + */ + +namespace FireHub\Tests\Kernel\Unit\Lifecycle; + +use FireHub\Testing\FireHubTestCase; +use FireHub\Kernel\Lifecycle\Engine; +use FireHub\Kernel\Lifecycle\Exception\ { + InvalidLifecycleStateException, LifecycleExecutionException +}; +use FireHub\Tests\Kernel\Stubs\ { + AnotherState, State +}; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; +use RuntimeException; + +/** + * ### Test configurable lifecycle + * @since 1.0.0 + */ +#[Small] +#[Group('lifecycle')] +#[CoversClass(Engine::class)] +final class EngineTest extends FireHubTestCase { + + /** + * ### Tests the initial lifecycle state + * @since 1.0.0 + * + * @return void + */ + public function testInitialState ():void { + + $engine = new Engine(State::CREATED); + + self::assertSame( + State::CREATED, + $engine->state() + ); + + } + + /** + * ### Tests successful execution and intermediate state + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testSuccessfulTransitionAndEnteringState ():void { + + $engine = new Engine(State::CREATED); + + $seen = null; + + $engine->execute( + callback: static function () use ($engine, &$seen):void { + + $seen = $engine->state(); + + }, + from: [State::CREATED], + success: State::RUNNING, + failure: State::FAILED, + entering: State::STARTING + ); + + self::assertSame(State::STARTING, $seen); + + self::assertSame( + State::RUNNING, + $engine->state() + ); + + } + + /** + * ### Tests failure propagation and state transition + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testFailurePreservesPreviousException ():void { + + $engine = new Engine(State::CREATED); + + $failure = new RuntimeException('Failed.'); + + try { + + $engine->execute( + callback: static function () use ($failure):void { + + throw $failure; + + }, + from: [State::CREATED], + success: State::RUNNING, + failure: State::FAILED + ); + + self::fail('Expected lifecycle execution failure.'); + + } catch (LifecycleExecutionException $exception) { + + self::assertSame( + $failure, + $exception->getPrevious() + ); + + self::assertSame( + State::FAILED, + $engine->state() + ); + + } + + } + + /** + * ### Tests invalid source state rejection + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testInvalidSourceStateDoesNotExecuteCallback ():void { + + $engine = new Engine(State::CREATED); + + $called = false; + + try { + + $engine->execute( + callback: static function () use (&$called):void { + + $called = true; + + }, + from: [State::RUNNING], + success: State::STOPPED, + failure: State::FAILED + ); + + self::fail('Expected invalid lifecycle state.'); + + } catch (InvalidLifecycleStateException) { + + self::assertFalse($called); + + self::assertSame( + State::CREATED, + $engine->state() + ); + + } + + } + + /** + * ### Tests rejection of states from another enumeration + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testRejectsStatesFromAnotherEnum ():void { + + $engine = new Engine(State::CREATED); + + $this->expectException( + InvalidLifecycleStateException::class + ); + + $engine->execute( + callback: static function ():void {}, + from: [State::CREATED], + success: AnotherState::DONE, + failure: State::FAILED + ); + + } + + /** + * ### Tests registration is prohibited after execution + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testRegistrationClosesAfterExecution ():void { + + $engine = new Engine(State::CREATED); + + $engine->execute( + callback: static function ():void {}, + from: [State::CREATED], + success: State::RUNNING, + failure: State::FAILED + ); + + $this->expectException( + InvalidLifecycleStateException::class + ); + + $engine->registerStartup( + static function ():void {} + ); + + } + + /** + * ### Tests nested lifecycle operations are prohibited + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testRejectsNestedExecution ():void { + + $engine = new Engine(State::CREATED); + + $nested_rejected = false; + + $engine->execute( + callback: static function () use ( + $engine, + &$nested_rejected + ):void { + + try { + + $engine->execute( + callback: static function ():void {}, + from: [State::STARTING], + success: State::RUNNING, + failure: State::FAILED + ); + + } catch (InvalidLifecycleStateException) { + + $nested_rejected = true; + + } + + }, + from: [State::CREATED], + success: State::RUNNING, + failure: State::FAILED, + entering: State::STARTING + ); + + self::assertTrue($nested_rejected); + + self::assertSame( + State::RUNNING, + $engine->state() + ); + + } + + /** + * ### Tests parent shutdown is blocked by active dependencies + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException + * + * @return void + */ + public function testParentDependencyBlocksShutdownUntilReleased ():void { + + $parent = new Engine(State::CREATED); + + $parent->execute( + callback: static function ():void {}, + from: [State::CREATED], + success: State::RUNNING, + failure: State::FAILED + ); + + $child = new Engine( + State::CREATED, + $parent->dependencyBoundary( + static fn (State $state):bool => + $state === State::RUNNING + ) + ); + + $child->execute( + callback: static function ():void {}, + from: [State::CREATED], + success: State::RUNNING, + failure: State::FAILED, + acquire_parent: true + ); + + try { + + $parent->execute( + callback: static function ():void {}, + from: [State::RUNNING], + success: State::STOPPED, + failure: State::FAILED, + require_no_dependents: true + ); + + self::fail('Expected active dependency rejection.'); + + } catch (InvalidLifecycleStateException) { + + self::assertSame( + State::RUNNING, + $parent->state() + ); + + } + + $child->execute( + callback: static function ():void {}, + from: [State::RUNNING], + success: State::STOPPED, + failure: State::FAILED, + release_parent: true + ); + + $parent->execute( + callback: static function ():void {}, + from: [State::RUNNING], + success: State::STOPPED, + failure: State::FAILED, + require_no_dependents: true + ); + + self::assertSame( + State::STOPPED, + $parent->state() + ); + + } + +} \ No newline at end of file diff --git a/tests/Unit/Lifecycle/ManagerTest.php b/tests/Unit/Lifecycle/ManagerTest.php index a96f157..49c694a 100644 --- a/tests/Unit/Lifecycle/ManagerTest.php +++ b/tests/Unit/Lifecycle/ManagerTest.php @@ -15,13 +15,10 @@ use FireHub\Testing\FireHubTestCase; use FireHub\Kernel\Lifecycle\Manager; -use FireHub\Kernel\ { - Bootstrapper, Terminator -}; use PHPUnit\Framework\Attributes\ { CoversClass, Group, Small }; -use Throwable; +use RuntimeException, Throwable; /** * ### Test kernel lifecycle component execution @@ -33,222 +30,230 @@ final class ManagerTest extends FireHubTestCase { /** - * ### Tests initialization without registered bootstrappers + * ### Tests startup execution without callbacks * @since 1.0.0 * + * @throws Throwable + * * @return void */ - public function testInitializeEmpty ():void { + public function testStartWithoutCallbacks ():void { $manager = new Manager; - $manager->initialize(); + $manager->start(); self::assertTrue(true); } /** - * ### Tests termination without registered terminators + * ### Tests shutdown execution without callbacks * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateEmpty ():void { + public function testStopWithoutCallbacks ():void { $manager = new Manager; - $manager->terminate(); + $manager->stop(); self::assertTrue(true); } /** - * ### Tests bootstrapper registration and execution + * ### Tests startup ordering and stable priorities * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testRegisterBootstrapper ():void { + public function testStartupOrderAndStablePriority ():void { $manager = new Manager; + $executed = []; - $bootstrapper = $this->createMock(Bootstrapper::class); - - $bootstrapper->expects($this->once()) - ->method('bootstrap'); + foreach ([['third', 30], ['first', 10], ['second', 10], ['fourth', 20]] as [$name, $order]) { - $manager->registerBootstrapper($bootstrapper, 0); + $manager->registerStartup( + static function () use (&$executed, $name):void { - $manager->initialize(); + $executed[] = $name; - } + }, + $order + ); - /** - * ### Tests terminator registration and execution - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testRegisterTerminator ():void { - - $manager = new Manager; - - $terminator = $this->createMock(Terminator::class); - - $terminator->expects($this->once()) - ->method('terminate'); + } - $manager->registerTerminator($terminator, 0); + $manager->start(); - $manager->terminate(); + self::assertSame( + ['first', 'second', 'fourth', 'third'], + $executed + ); } /** - * ### Tests bootstrapper execution order + * ### Tests shutdown ordering and stable priorities * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testInitializeOrder ():void { + public function testShutdownOrderAndStablePriority ():void { $manager = new Manager; - $executed = []; - foreach ([30, 10, 20] as $order) { + foreach ([['third', 30], ['first', 10], ['second', 10], ['fourth', 20]] as [$name, $order]) { - $bootstrapper = $this->createMock(Bootstrapper::class); + $manager->registerShutdown( + static function () use (&$executed, $name):void { - $bootstrapper->method('bootstrap') - ->willReturnCallback( - static function () use (&$executed, $order):void { - $executed[] = $order; - } - ); + $executed[] = $name; - $manager->registerBootstrapper($bootstrapper, $order); + }, + $order + ); } - $manager->initialize(); + $manager->stop(); - self::assertSame([10, 20, 30], $executed); + self::assertSame( + ['first', 'second', 'fourth', 'third'], + $executed + ); } /** - * ### Tests terminator execution order + * ### Tests startup stops at the first failure * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateOrder ():void { + public function testStartupStopsAtFirstFailure ():void { $manager = new Manager; + $failure = new RuntimeException('Startup failed.'); $executed = []; - foreach ([30, 10, 20] as $order) { - - $terminator = $this->createMock(Terminator::class); - - $terminator->method('terminate') - ->willReturnCallback( - static function () use (&$executed, $order):void { - $executed[] = $order; - } - ); - - $manager->registerTerminator($terminator, $order); - - } - - $manager->terminate(); + $manager->registerStartup( + static function () use (&$executed):void { - self::assertSame([10, 20, 30], $executed); + $executed[] = 'first'; - } + }, + 0 + ); - /** - * ### Tests initialization failure propagation - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testInitializeFailure ():void { + $manager->registerStartup( + static function () use ($failure):void { - $manager = new Manager; + throw $failure; - $failure = new \RuntimeException('Bootstrap failed.'); + }, + 1 + ); - $bootstrapper = $this->createMock(Bootstrapper::class); + $manager->registerStartup( + static function () use (&$executed):void { - $bootstrapper->expects($this->once()) - ->method('bootstrap') - ->willThrowException($failure); + $executed[] = 'never'; - $manager->registerBootstrapper($bootstrapper, 0); + }, + 2 + ); try { - $manager->initialize(); + $manager->start(); - self::fail('Expected bootstrap exception was not thrown.'); + self::fail('Expected startup failure.'); - } catch (\RuntimeException $exception) { + } catch (RuntimeException $exception) { self::assertSame($failure, $exception); + self::assertSame(['first'], $executed); } } /** - * ### Tests termination failure propagation + * ### Tests shutdown executes all callbacks after failures * @since 1.0.0 * * @throws Throwable * * @return void */ - public function testTerminateFailure ():void { + public function testShutdownRunsAllCallbacksAndRethrowsFirstFailure ():void { $manager = new Manager; - $failure = new \RuntimeException('Termination failed.'); + $first = new RuntimeException('First failure.'); + $second = new RuntimeException('Second failure.'); - $terminator = $this->createMock(Terminator::class); + $executed = []; - $terminator->expects($this->once()) - ->method('terminate') - ->willThrowException($failure); + $manager->registerShutdown( + static function () use (&$executed, $first):void { - $manager->registerTerminator($terminator, 0); + $executed[] = 'first'; + + throw $first; + + }, + 0 + ); + + $manager->registerShutdown( + static function () use (&$executed, $second):void { + + $executed[] = 'second'; + + throw $second; + + }, + 1 + ); + + $manager->registerShutdown( + static function () use (&$executed):void { + + $executed[] = 'third'; + + }, + 2 + ); try { - $manager->terminate(); + $manager->stop(); - self::fail('Expected termination exception was not thrown.'); + self::fail('Expected shutdown failure.'); - } catch (\RuntimeException $exception) { + } catch (RuntimeException $exception) { - self::assertSame($failure, $exception); + self::assertSame($first, $exception); + + self::assertSame( + ['first', 'second', 'third'], + $executed + ); } diff --git a/tests/Unit/LifecycleTest.php b/tests/Unit/LifecycleTest.php deleted file mode 100644 index fa08ba3..0000000 --- a/tests/Unit/LifecycleTest.php +++ /dev/null @@ -1,403 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Unit; - -use FireHub\Testing\FireHubTestCase; -use FireHub\Core\Meta\Enum\Kernel\State; -use FireHub\Kernel\Lifecycle; -use FireHub\Kernel\Exception\InvalidKernelStateException; -use PHPUnit\Framework\Attributes\ { - CoversClass, DataProvider, Group, Small -}; - -/** - * ### Test kernel lifecycle state transitions - * @since 1.0.0 - */ -#[Small] -#[Group('lifecycle')] -#[CoversClass(Lifecycle::class)] -final class LifecycleTest extends FireHubTestCase { - - /** - * ### Tests the initial lifecycle state - * @since 1.0.0 - */ - public function testInitialState ():void { - - $lifecycle = new Lifecycle; - - self::assertSame(State::CREATED, $lifecycle->state()); - - } - - /** - * ### Tests beginning kernel initialization - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testBeginBoot ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginBoot(); - - self::assertSame(State::BOOTING, $lifecycle->state()); - - } - - /** - * ### Tests completing kernel initialization - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testCompleteBoot ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginBoot(); - $lifecycle->completeBoot(); - - self::assertSame(State::BOOTED, $lifecycle->state()); - - } - - /** - * ### Tests beginning shutdown before initialization - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testBeginShutdownBeforeBoot ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginShutdown(); - - self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); - - } - - /** - * ### Tests beginning shutdown after initialization - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testBeginShutdownAfterBoot ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginBoot(); - $lifecycle->completeBoot(); - $lifecycle->beginShutdown(); - - self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); - - } - - /** - * ### Tests beginning shutdown after initialization failure - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testBeginShutdownAfterBootFailure ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginBoot(); - $lifecycle->fail(); - $lifecycle->beginShutdown(); - - self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); - - } - - /** - * ### Tests completing kernel shutdown - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testCompleteShutdown ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginShutdown(); - $lifecycle->completeShutdown(); - - self::assertSame(State::TERMINATED, $lifecycle->state()); - - } - - /** - * ### Tests marking initialization as failed - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testFailDuringBoot ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginBoot(); - $lifecycle->fail(); - - self::assertSame(State::BOOT_FAILED, $lifecycle->state()); - - } - - /** - * ### Tests marking shutdown as failed - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testFailDuringShutdown ():void { - - $lifecycle = new Lifecycle; - - $lifecycle->beginShutdown(); - $lifecycle->fail(); - - self::assertSame(State::SHUTDOWN_FAILED, $lifecycle->state()); - - } - - /** - * ### Tests the complete successful lifecycle - * @since 1.0.0 - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - public function testCompleteLifecycle ():void { - - $lifecycle = new Lifecycle; - - self::assertSame(State::CREATED, $lifecycle->state()); - - $lifecycle->beginBoot(); - - self::assertSame(State::BOOTING, $lifecycle->state()); - - $lifecycle->completeBoot(); - - self::assertSame(State::BOOTED, $lifecycle->state()); - - $lifecycle->beginShutdown(); - - self::assertSame(State::SHUTTING_DOWN, $lifecycle->state()); - - $lifecycle->completeShutdown(); - - self::assertSame(State::TERMINATED, $lifecycle->state()); - - } - - /** - * ### Tests invalid lifecycle transitions - * @since 1.0.0 - * - * @param list $operations - * @param non-empty-string $operation - * @param State $expected_state - * - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * @throws \FireHub\Kernel\Exception\InvalidKernelStateException - * - * @return void - */ - #[DataProvider('invalidTransitionProvider')] - public function testInvalidTransition (array $operations, string $operation, State $expected_state):void { - - $lifecycle = new Lifecycle; - - foreach ($operations as $method) - $lifecycle->{$method}(); - - self::assertSame($expected_state, $lifecycle->state()); - - try { - - $lifecycle->{$operation}(); - - self::fail('Expected InvalidKernelStateException was not thrown.'); - - } catch (InvalidKernelStateException) { - - self::assertSame($expected_state, $lifecycle->state()); - - } - - } - - /** - * ### Provides invalid lifecycle transitions - * @since 1.0.0 - * - * @return iterable, - * operation: non-empty-string, - * expected_state: State - * }> - */ - public static function invalidTransitionProvider ():iterable { - - yield 'complete boot before beginning boot' => [ - 'operations' => [], - 'operation' => 'completeBoot', - 'expected_state' => State::CREATED - ]; - - yield 'complete shutdown before beginning shutdown' => [ - 'operations' => [], - 'operation' => 'completeShutdown', - 'expected_state' => State::CREATED - ]; - - yield 'fail before beginning lifecycle operation' => [ - 'operations' => [], - 'operation' => 'fail', - 'expected_state' => State::CREATED - ]; - - yield 'begin boot while booting' => [ - 'operations' => ['beginBoot'], - 'operation' => 'beginBoot', - 'expected_state' => State::BOOTING - ]; - - yield 'complete boot twice' => [ - 'operations' => ['beginBoot', 'completeBoot'], - 'operation' => 'completeBoot', - 'expected_state' => State::BOOTED - ]; - - yield 'begin boot after successful boot' => [ - 'operations' => ['beginBoot', 'completeBoot'], - 'operation' => 'beginBoot', - 'expected_state' => State::BOOTED - ]; - - yield 'begin shutdown while booting' => [ - 'operations' => ['beginBoot'], - 'operation' => 'beginShutdown', - 'expected_state' => State::BOOTING - ]; - - yield 'fail after successful boot' => [ - 'operations' => ['beginBoot', 'completeBoot'], - 'operation' => 'fail', - 'expected_state' => State::BOOTED - ]; - - yield 'begin boot after boot failure' => [ - 'operations' => ['beginBoot', 'fail'], - 'operation' => 'beginBoot', - 'expected_state' => State::BOOT_FAILED - ]; - - yield 'complete boot after boot failure' => [ - 'operations' => ['beginBoot', 'fail'], - 'operation' => 'completeBoot', - 'expected_state' => State::BOOT_FAILED - ]; - - yield 'fail twice after boot failure' => [ - 'operations' => ['beginBoot', 'fail'], - 'operation' => 'fail', - 'expected_state' => State::BOOT_FAILED - ]; - - yield 'begin shutdown while shutting down' => [ - 'operations' => ['beginShutdown'], - 'operation' => 'beginShutdown', - 'expected_state' => State::SHUTTING_DOWN - ]; - - yield 'complete shutdown twice' => [ - 'operations' => ['beginShutdown', 'completeShutdown'], - 'operation' => 'completeShutdown', - 'expected_state' => State::TERMINATED - ]; - - yield 'begin shutdown after termination' => [ - 'operations' => ['beginShutdown', 'completeShutdown'], - 'operation' => 'beginShutdown', - 'expected_state' => State::TERMINATED - ]; - - yield 'begin boot after termination' => [ - 'operations' => ['beginShutdown', 'completeShutdown'], - 'operation' => 'beginBoot', - 'expected_state' => State::TERMINATED - ]; - - yield 'fail after termination' => [ - 'operations' => ['beginShutdown', 'completeShutdown'], - 'operation' => 'fail', - 'expected_state' => State::TERMINATED - ]; - - yield 'begin shutdown after shutdown failure' => [ - 'operations' => ['beginShutdown', 'fail'], - 'operation' => 'beginShutdown', - 'expected_state' => State::SHUTDOWN_FAILED - ]; - - yield 'complete shutdown after shutdown failure' => [ - 'operations' => ['beginShutdown', 'fail'], - 'operation' => 'completeShutdown', - 'expected_state' => State::SHUTDOWN_FAILED - ]; - - yield 'fail twice after shutdown failure' => [ - 'operations' => ['beginShutdown', 'fail'], - 'operation' => 'fail', - 'expected_state' => State::SHUTDOWN_FAILED - ]; - - } - -} \ No newline at end of file diff --git a/tests/Unit/Terminator/SequenceTest.php b/tests/Unit/Terminator/SequenceTest.php deleted file mode 100644 index 9e04fac..0000000 --- a/tests/Unit/Terminator/SequenceTest.php +++ /dev/null @@ -1,328 +0,0 @@ - - * @copyright 2026-present The FireHub Project - All rights reserved - * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 - * - * @php-version >=8.1 - * @package Kernel\Tests - */ - -namespace FireHub\Tests\Kernel\Unit\Terminator; - -use FireHub\Testing\FireHubTestCase; -use FireHub\Kernel\Terminator; -use FireHub\Kernel\Terminator\Sequence; -use PHPUnit\Framework\Attributes\ { - CoversClass, Group, Small -}; -use Throwable; - -/** - * ### Tests kernel termination sequence - * @since 1.0.0 - */ -#[Small] -#[Group('terminator')] -#[CoversClass(Sequence::class)] -final class SequenceTest extends FireHubTestCase { - - /** - * ### Tests execution of an empty sequence - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteEmpty ():void { - - $sequence = new Sequence; - - $sequence->execute(); - - $this->expectNotToPerformAssertions(); - - } - - /** - * ### Tests execution of a single terminator - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteSingle ():void { - - $sequence = new Sequence; - - $terminator = $this->createMock(Terminator::class); - - $terminator->expects($this->once()) - ->method('terminate'); - - $sequence->register($terminator, 0); - - $sequence->execute(); - - } - - /** - * ### Tests execution of multiple terminators - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteMultiple ():void { - - $sequence = new Sequence; - - $terminators = []; - - for ($i = 0; $i < 3; $i++) { - - $terminator = $this->createMock(Terminator::class); - - $terminator->expects($this->once()) - ->method('terminate'); - - $terminators[] = $terminator; - - } - - foreach ($terminators as $terminator) - $sequence->register($terminator, 0); - - $sequence->execute(); - - } - - /** - * ### Tests ascending execution order - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteOrder ():void { - - $sequence = new Sequence; - - $executed = []; - - foreach ([30, 10, 20] as $order) { - - $terminator = $this->createMock(Terminator::class); - - $terminator->method('terminate') - ->willReturnCallback( - static function () use (&$executed, $order):void { - $executed[] = $order; - } - ); - - $sequence->register($terminator, $order); - - } - - $sequence->execute(); - - self::assertSame([10, 20, 30], $executed); - - } - - /** - * ### Tests stable execution order - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteEqualOrder ():void { - - $sequence = new Sequence; - - $executed = []; - - foreach (['first', 'second', 'third'] as $name) { - - $terminator = $this->createMock(Terminator::class); - - $terminator->method('terminate') - ->willReturnCallback( - static function () use (&$executed, $name):void { - $executed[] = $name; - } - ); - - $sequence->register($terminator, 10); - - } - - $sequence->execute(); - - self::assertSame(['first', 'second', 'third'], $executed); - - } - - /** - * ### Tests negative execution priorities - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteNegativeOrder ():void { - - $sequence = new Sequence; - - $executed = []; - - foreach ([10, -100, 0, -10] as $order) { - - $terminator = $this->createMock(Terminator::class); - - $terminator->method('terminate') - ->willReturnCallback( - static function () use (&$executed, $order):void { - $executed[] = $order; - } - ); - - $sequence->register($terminator, $order); - - } - - $sequence->execute(); - - self::assertSame([-100, -10, 0, 10], $executed); - - } - - /** - * ### Tests termination continues after a failure - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteContinuesOnFailure ():void { - - $sequence = new Sequence; - - $executed = []; - - foreach ([1, 2, 3] as $order) { - - $terminator = $this->createMock(Terminator::class); - - $terminator->method('terminate') - ->willReturnCallback( - static function () use (&$executed, $order):void { - - $executed[] = $order; - - if ($order === 2) - throw new \RuntimeException('Termination failed.'); - - } - ); - - $sequence->register($terminator, $order); - - } - - try { - - $sequence->execute(); - - self::fail('Expected termination exception was not thrown.'); - - } catch (\RuntimeException $exception) { - - self::assertSame('Termination failed.', $exception->getMessage()); - - } - - self::assertSame([1, 2, 3], $executed); - - } - - /** - * ### Tests that the first exception is propagated - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteThrowsFirstFailure ():void { - - $sequence = new Sequence; - - $first_exception = new \RuntimeException('First failure.'); - $second_exception = new \RuntimeException('Second failure.'); - - $first = $this->createMock(Terminator::class); - $second = $this->createMock(Terminator::class); - - $first->expects($this->once()) - ->method('terminate') - ->willThrowException($first_exception); - - $second->expects($this->once()) - ->method('terminate') - ->willThrowException($second_exception); - - $sequence->register($first, 10); - $sequence->register($second, 20); - - try { - - $sequence->execute(); - - self::fail('Expected termination exception was not thrown.'); - - } catch (\RuntimeException $exception) { - - self::assertSame($first_exception, $exception); - - } - - } - - /** - * ### Tests repeated sequence execution - * @since 1.0.0 - * - * @throws Throwable - * - * @return void - */ - public function testExecuteRepeated ():void { - - $sequence = new Sequence; - - $terminator = $this->createMock(Terminator::class); - - $terminator->expects($this->exactly(2)) - ->method('terminate'); - - $sequence->register($terminator, 0); - - $sequence->execute(); - $sequence->execute(); - - } - -} \ No newline at end of file From 3b7922f9e055dfb391ab57797d922d9e9a5b1755 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 13:20:00 +0200 Subject: [PATCH 28/32] Add inline usage examples to `Kernel` lifecycle methods for improved clarity Enhanced the PHPDoc in the `Kernel` class by embedding inline usage examples for lifecycle methods (`registerBootstrapper`, `registerTerminator`, `boot`, `shutdown`, `state`) to improve developer understanding of their usage. These examples demonstrate proper method invocation and sequence within a kernel lifecycle, providing practical guidance for integration. No functional changes introduced. --- src/Kernel.php | 78 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 75 insertions(+), 3 deletions(-) diff --git a/src/Kernel.php b/src/Kernel.php index 06d77fa..bdfd8f1 100644 --- a/src/Kernel.php +++ b/src/Kernel.php @@ -30,6 +30,22 @@ * * Repeated boot calls on a booted kernel and repeated shutdown calls on a terminated kernel are no-ops. * A failed kernel cannot be rebooted, but shutdown can be attempted to release resources. + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->registerBootstrapper(new ClassThatImplementsBootstrapper); + * $kernel->registerTerminator(new ClassThatImplementsTerminator); + * + * $kernel->boot(); + * + * // write code here + * + * $kernel->shutdown(); + * +* * @since 1.0.0 */ final readonly class Kernel implements KernelBoundary { @@ -70,6 +86,15 @@ public function dependencyBoundary ():Boundary { /** * ### Registers a kernel bootstrapper + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->registerBootstrapper(new ClassThatImplementsBootstrapper); + * + * * @since 1.0.0 * * @uses \FireHub\Kernel\Lifecycle\Controller::registerBootstrapper() To register a bootstrapper. @@ -93,6 +118,15 @@ public function registerBootstrapper (Bootstrapper $bootstrapper, int $order = 0 /** * ### Registers a kernel terminator + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->registerTerminator(new ClassThatImplementsTerminator); + * + * * @since 1.0.0 * * @uses \FireHub\Kernel\Lifecycle\Controller::registerTerminator() To register a terminator. @@ -115,7 +149,15 @@ public function registerTerminator (Terminator $terminator, int $order = 0):void } /** - * @inheritDoc + * {@inheritDoc} + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->boot(); + * * * @since 1.0.0 * @@ -131,7 +173,17 @@ public function boot ():void { } /** - * @inheritDoc + * {@inheritDoc} + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->boot(); + * + * $kernel->shutdown(); + * * * @since 1.0.0 * @@ -147,7 +199,27 @@ public function shutdown ():void { } /** - * @inheritDoc + * {@inheritDoc} + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->state(); + * + * // FireHub\Core\Meta\Enum\Kernel\State::CREATED + * + * $kernel->boot(); + * $kernel->state(); + * + * // FireHub\Core\Meta\Enum\Kernel\State::BOOTED + * + * $kernel->shutdown(); + * $kernel->state(); + * + * // FireHub\Core\Meta\Enum\Kernel\State::TERMINATED + * * * @since 1.0.0 * From ded902797f09e45f6df72ddf0895700622d7d3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 15:40:17 +0200 Subject: [PATCH 29/32] Add dependency boundary and isolated operation execution to Application Introduced two new functionalities in `Application`: 1. `dependencyBoundary()`: Provides lifecycle dependency boundary access, enabling dependent runtimes during the application's initialized state. 2. `execute(Closure $callback)`: Allows executing operations within an isolated dependency injection scope while maintaining lifecycle dependency to prevent disposal during execution. Enhanced lifecycle management and isolation ensures safer runtime operations and promotes better dependency control. Removed internal marker from `Boundary` class to make it accessible. --- src/Lifecycle/Dependency/Boundary.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Lifecycle/Dependency/Boundary.php b/src/Lifecycle/Dependency/Boundary.php index 7e0f32b..4b605b2 100644 --- a/src/Lifecycle/Dependency/Boundary.php +++ b/src/Lifecycle/Dependency/Boundary.php @@ -24,8 +24,6 @@ * * Dependent lifecycle engines use this boundary to acquire ownership of their parent lifecycle during execution. * @since 1.0.0 - * - * @internal */ final readonly class Boundary { From eb7a6a870d804126d9bbcb31f27bd97813712fef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 21 Oct 2026 08:42:50 +0200 Subject: [PATCH 30/32] Fix lifecycle state transitions in Controller for consistency --- src/Lifecycle/Controller.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php index 149ce6b..87c5e7b 100644 --- a/src/Lifecycle/Controller.php +++ b/src/Lifecycle/Controller.php @@ -194,9 +194,9 @@ public function boot ():void { $this->lifecycle->execute( callback: fn() => $this->lifecycle->start(), from: [State::CREATED], - entering: State::BOOTING, success: State::BOOTED, - failure: State::BOOT_FAILED + failure: State::BOOT_FAILED, + entering: State::BOOTING ); } catch (InvalidLifecycleStateException $exception) { @@ -245,9 +245,9 @@ public function shutdown ():void { State::BOOTED, State::BOOT_FAILED ], - entering: State::SHUTTING_DOWN, success: State::TERMINATED, failure: State::SHUTDOWN_FAILED, + entering: State::SHUTTING_DOWN, require_no_dependents: true ); From c6fd2bbe0c273b37202ac5dc60373233e114f56a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 21 Oct 2026 10:41:15 +0200 Subject: [PATCH 31/32] Update README.md to reflect branch changes and improve clarity --- CHANGELOG.md | 7 ++++++- README.md | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 751d2d4..98a3cf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,4 +3,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). \ No newline at end of file +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v0.1.0] – 2026-10-21 + +### Added +* Implement initial functionality by @dieselxxx in https://github.com/The-FireHub-Project/Kernel/pull/1 \ No newline at end of file diff --git a/README.md b/README.md index 34bfc77..3f3a800 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ /> version From 0e6732ba3a76a5e171fe070e3d41a9f0c928ac7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:42:28 +0200 Subject: [PATCH 32/32] Update README.md --- README.md | 132 +++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 90 insertions(+), 42 deletions(-) diff --git a/README.md b/README.md index 3f3a800..059f86c 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ version + /> -[![PHPStan](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPStan.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPStan.yml) -[![PHPUnit](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPUnit.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Kernel/actions/workflows/PHPUnit.yml) -[![Codecov](https://codecov.io/gh/The-FireHub-Project/Kernel/branch/develop/graph/badge.svg?token=XW2YEONF51)](https://app.codecov.io/gh/The-FireHub-Project/Kernel/tree/develop) -

- + + GitHub created at + GitHub last commit (branch) - + GitHub activity (branch) - + GitHub commit difference between two branches + + GitHub language count + + GitHub top language + + GitHub repo size + + GitHub code size

@@ -49,47 +70,74 @@ Designed as a shared, edition-agnostic runtime component for Standard, Professio --- -## FireHub Icon Development Branch +## FireHub Icon About FireHub + +**FireHub** is a modular PHP framework built around: -⚠️ **This is the `develop` branch** +- Clean Architecture +- Domain-Driven Design (DDD) +- Explicit boundaries between Core, Adapters, and Modules +- Long-term maintainability and scalability -- Unstable -- APIs may change without notice -- Not intended for production use +FireHub is designed for building **enterprise-grade PHP applications** without sacrificing clarity or developer experience. -## FireHub Icon Branch Purpose +## FireHub Icon Repository Purpose -The `develop` branch is the **primary integration branch** for all ongoing development. +FireHub Kernel provides the application-level orchestration required to initialize, operate, and shut down FireHub applications consistently. It implements stable contracts defined by Core Standard and remains independent of edition-specific features. -It serves as the staging area where: -- Feature branches are merged -- Bug fixes are integrated -- Experimental work is stabilized -- Code is prepared for upcoming releases +Its responsibilities include: +- Application lifecycle — managing initialization, readiness, failure, and shutdown. +- Bootstrap orchestration — executing initialization steps in a controlled order. +- Service integration — coordinating service registration and initialization through explicit contracts. +- Configuration initialization — making application configuration available during startup. +- Module integration — coordinating optional modules and capabilities without hard-coded edition checks. +- Application context — providing the environment and state needed for application initialization. -All **release branches** are created **from `develop`**. +The Kernel does not implement HTTP routing, console commands, business logic, or individual capability features. Those responsibilities belong to adapters, applications, and dedicated capability packages. -## Stability Guarantee +The same Kernel is used with all three FireHub editions. Core Standard is the minimum required edition; Professional and Enterprise extend the ecosystem through their own contracts and components, not separate Kernel implementations. -❌ No backward compatibility guarantee -❌ APIs may change without notice -❌ Behavior may be incomplete or inconsistent -❌ Breaking changes are expected +## FireHub Icon Security -This branch is intended **only for contributors and advanced testers**. +For security reporting and guidelines, please refer to our [SECURITY.md](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file. -## FireHub Icon Composer Usage (Not Recommended) +## FireHub Icon Community + +![GitHub watchers](https://img.shields.io/github/watchers/The-FireHub-Project/Kernel?style=social) +![GitHub Repo stars](https://img.shields.io/github/stars/The-FireHub-Project/Kernel?style=social) +![GitHub forks](https://img.shields.io/github/forks/The-FireHub-Project/Kernel?style=social) + +- Be sure to review the [Code of Conduct](https://github.com/The-FireHub-Project/.github/blob/master/.github/CODE_OF_CONDUCT.md). +- Read a [Contributing](https://github.com/The-FireHub-Project/.github/blob/master/.github/CONTRIBUTING.md) file to see how you can contribute to this project. +- Read our [Security policy](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file to see how you can report a vulnerability. +- Review the project [Governance model](https://github.com/The-FireHub-Project/.github/blob/master/.github/GOVERNANCE.md), including roles, decision-making, RFC process, and releases. + +## FireHub Icon Support + +

+ + GitHub in-progress issues + + GitHub total issues + + GitHub opened pull requests +

-For internal testing only: +For help with FireHub products, please refer to the following resources: -```json -{ - "require": { - "the-firehub-project/kernel": "dev-develop" - } -} -``` -⚠️ Never use dev-develop in production. +- **General questions & discussions**: [GitHub Discussions](https://github.com/orgs/The-FireHub-Project/discussions) +- **Bug reports & issues**: [Support](https://github.com/The-FireHub-Project/Kernel/issues) +- **Documentation & guides**: [FireHub Docs](https://the-firehub-project.github.io) +- **Commercial support** (Pro/Enterprise): contact your account manager ## FireHub Icon Authors and Contributors @@ -106,4 +154,4 @@ Architecture guidelines, design principles, and ecosystem documentation are avai This software is licensed under the Apache-2.0 License. -For more details, read the full license [here](./LICENSE). \ No newline at end of file +For more details, read the full license [here](./LICENSE).