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 7faf576..059f86c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@

FireHub Core Logo -
+ FireHub Kernel
version @@ -154,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). diff --git a/composer.json b/composer.json index 48eb886..428fa47 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,11 @@ "homepage": "https://the-firehub-project.github.io/kernel", "license": "Apache-2.0", "require": { - "php": "^8.5" + "php": "^8.5", + "ext-mbstring": "*", + + "the-firehub-project/core-standard": "dev-develop", + "the-firehub-project/runtime": ">=0.1.0 <1.0.0" }, "require-dev": { "phpstan/phpstan": "^2.0", 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..0a9d015 --- /dev/null +++ b/src/Exception/KernelBootException.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\Exception; + +use FireHub\Core\Exception\Runtime\KernelException; + +/** + * ### ### Represents a kernel boot failure + * + * Indicates that application kernel initialization could not be completed successfully. + * @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..26c52bc --- /dev/null +++ b/src/Exception/KernelShutdownException.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\Exception; + +use FireHub\Core\Exception\Runtime\KernelException; + +/** + * ### Represents a kernel shutdown failure + * + * Indicates that application kernel termination could not be completed successfully. + * @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/Kernel.php b/src/Kernel.php new file mode 100644 index 0000000..bdfd8f1 --- /dev/null +++ b/src/Kernel.php @@ -0,0 +1,234 @@ + + * @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; + +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; + +/** + * ### Manages the kernel lifecycle + * + * Implements the kernel contract and coordinates runtime initialization and termination. + * + * 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. + * + * 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 { + + /** + * ### Kernel lifecycle controller + * @since 1.0.0 + */ + private Controller $lifecycle; + + /** + * ### Constructor + * @since 1.0.0 + * + * @return void + */ + public function __construct () { + + $this->lifecycle = new Controller; + + } + + /** + * ### 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\Controller::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 + * + * + * 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. + * + * @param \FireHub\Kernel\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 $bootstrapper, int $order = 0):void { + + $this->lifecycle->registerBootstrapper($bootstrapper, $order); + + } + + /** + * ### 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. + * + * @param \FireHub\Kernel\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 $terminator, int $order = 0):void { + + $this->lifecycle->registerTerminator($terminator, $order); + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->boot(); + * + * + * @since 1.0.0 + * + * @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 { + + $this->lifecycle->boot(); + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Kernel\Kernel; + * + * $kernel = new Kernel; + * + * $kernel->boot(); + * + * $kernel->shutdown(); + * + * + * @since 1.0.0 + * + * @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. + */ + public function shutdown ():void { + + $this->lifecycle->shutdown(); + + } + + /** + * {@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 + * + * @uses \FireHub\Kernel\Lifecycle\Controller::state() To get the current state. + */ + public function state ():State { + + return $this->lifecycle->state(); + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Controller.php b/src/Lifecycle/Controller.php new file mode 100644 index 0000000..87c5e7b --- /dev/null +++ b/src/Lifecycle/Controller.php @@ -0,0 +1,288 @@ + + * @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\Dependency\Boundary; +use FireHub\Kernel\Exception\ { + InvalidKernelStateException, KernelBootException, KernelShutdownException +}; +use FireHub\Kernel\Lifecycle\Exception\ { + InvalidLifecycleStateException, LifecycleExecutionException +}; + +/** + * ### Coordinates the kernel lifecycle + * + * 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. + * + * 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 + * + * @internal + */ +final readonly class Controller implements Kernel { + + /** + * ### Kernel lifecycle engine + * @since 1.0.0 + * + * @var \FireHub\Kernel\Lifecycle\Engine + */ + private Engine $lifecycle; + + /** + * ### Constructor + * @since 1.0.0 + * + * @return void + */ + public function __construct () { + + $this->lifecycle = new Engine(State::CREATED); + + } + + /** + * ### 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( + static fn (State $state):bool => $state === State::BOOTED + ); + + } + + /** + * ### Registers a kernel bootstrapper + * + * Registers a bootstrapper for execution during kernel initialization. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Engine::registerStartup() To register a bootstrapper. + * @uses \FireHub\Kernel\Bootstrapper::bootstrap() To execute the bootstrapper. + * + * @param \FireHub\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, int $order):void { + + try { + + $this->lifecycle->registerStartup( + static function () use ($bootstrapper):void { + + $bootstrapper->bootstrap(); + + }, + $order + ); + + } catch (InvalidLifecycleStateException $exception) { + + throw new InvalidKernelStateException( + 'Cannot register a bootstrapper in the current kernel state.', + previous: $exception + ); + + } + + } + + /** + * ### Registers a kernel terminator + * + * Registers a terminator for execution during kernel shutdown. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Engine::registerShutdown() To register a terminator. + * @uses \FireHub\Kernel\Terminator::terminate() To execute the terminator. + * + * @param \FireHub\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, int $order):void { + + 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 + ); + + } + + } + + /** + * ### 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 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. + * + * @return void + */ + public function boot ():void { + + if ($this->state() === State::BOOTED) return; + + try { + + $this->lifecycle->execute( + callback: fn() => $this->lifecycle->start(), + from: [State::CREATED], + success: State::BOOTED, + failure: State::BOOT_FAILED, + entering: State::BOOTING + ); + + } catch (InvalidLifecycleStateException $exception) { + + throw new InvalidKernelStateException( + 'Cannot boot the kernel in the current state.', + previous: $exception + ); + + } catch (LifecycleExecutionException $exception) { + + throw new KernelBootException( + 'Failed to boot the kernel.', + previous: $exception->getPrevious() ?? $exception + ); + + } + + } + + /** + * ### 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 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. + * @throws \FireHub\Kernel\Exception\KernelShutdownException If a terminator fails. + * + * @return void + */ + public function shutdown ():void { + + if ($this->state() === State::TERMINATED) return; + + try { + + $this->lifecycle->execute( + callback: fn() => $this->lifecycle->stop(), + from: [ + State::CREATED, + State::BOOTED, + State::BOOT_FAILED + ], + success: State::TERMINATED, + failure: State::SHUTDOWN_FAILED, + entering: State::SHUTTING_DOWN, + require_no_dependents: true + ); + + } catch (InvalidLifecycleStateException $exception) { + + throw new InvalidKernelStateException( + 'Cannot shut down the kernel in the current state.', + previous: $exception + ); + + } catch (LifecycleExecutionException $exception) { + + throw new KernelShutdownException( + 'Failed to shut down the kernel.', + previous: $exception->getPrevious() ?? $exception + ); + + } + + } + + /** + * ### Gets the current kernel lifecycle state + * + * Returns the kernel's own lifecycle state without conversion. + * @since 1.0.0 + * + * @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 { + + return $this->lifecycle->state(); + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/Dependency/Boundary.php b/src/Lifecycle/Dependency/Boundary.php new file mode 100644 index 0000000..4b605b2 --- /dev/null +++ b/src/Lifecycle/Dependency/Boundary.php @@ -0,0 +1,72 @@ + + * @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 + */ +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..cd8b36e --- /dev/null +++ b/src/Lifecycle/Dependency/Lease.php @@ -0,0 +1,76 @@ + + * @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; + + } + +} \ 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 new file mode 100644 index 0000000..3a243fe --- /dev/null +++ b/src/Lifecycle/Engine.php @@ -0,0 +1,397 @@ + + * @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\Dependency\ { + Boundary, Lease, Registry +}; +use FireHub\Kernel\Lifecycle\Exception\ { + InvalidLifecycleStateException, LifecycleExecutionException +}; +use FireHub\Runtime; +use Closure, Throwable, UnitEnum; + +/** + * ### Coordinates a configurable lifecycle + * + * Provides generic lifecycle execution and state management independently of the component owning the lifecycle. + * + * Lifecycle states are represented by an arbitrary enumeration. + * + * Each operation defines its permitted source states, successful state, failure state, and optional execution state. + * + * The engine also manages parent lifecycle dependencies and prevents termination while dependent lifecycles remain + * active. + * @since 1.0.0 + * + * @template TState of UnitEnum + */ +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; + + /** + * ### Dependent lifecycle registry + * @since 1.0.0 + */ + private readonly Registry $dependencies; + + /** + * ### Active parent lifecycle dependency + * @since 1.0.0 + */ + 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 TState $initial

+ * Initial lifecycle state. + *

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

+ * Parent lifecycle dependency boundary. + *

+ * + * @return void + */ + public function __construct ( + private readonly UnitEnum $initial, + private readonly ?Boundary $parent = null + ) { + + $this->state = $initial; + + $this->manager = new Manager; + + $this->dependencies = new Registry; + + } + + /** + * ### Gets the lifecycle dependency boundary + * + * 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 (Closure $is_available):Boundary { + + return new Boundary( + fn ():bool => $is_available($this->state), + $this->dependencies + ); + + } + + /** + * ### 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 + * + * @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); + + } + + /** + * ### Executes registered startup callbacks + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Manager::start() To execute startup callbacks. + * + * @throws Throwable If a startup callback fails. + * + * @return void + */ + public function start ():void { + + $this->manager->start(); + + } + + /** + * ### 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->manager->stop(); + + } + + /** + * ### Executes a lifecycle operation + * + * Validates the current state and executes the supplied callback. + * + * An optional entering state represents an operation in progress. + * + * 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. + * + * 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\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 execution is not permitted. + * @throws \FireHub\Kernel\Lifecycle\Exception\LifecycleExecutionException If the operation callback fails. + * + * @return 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->executing) + throw new InvalidLifecycleStateException( + 'A lifecycle operation is already executing.' + ); + + $this->assertStateType($success); + $this->assertStateType($failure); + + 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( + 'The lifecycle operation is not allowed in the current state.' + ); + + if ($require_no_dependents) + $this->dependencies->assertEmpty(); + + if ($acquire_parent && $this->parent_lease === null) + $this->parent_lease = $this->parent?->acquire(); + + $this->registration_closed = true; + $this->executing = true; + + if ($entering !== null) + $this->state = $entering; + + try { + + $callback(); + + } catch (Throwable $exception) { + + $this->state = $failure; + + if ($release_parent) + $this->releaseParent(); + + throw new LifecycleExecutionException( + 'Lifecycle operation failed.', + previous: $exception + ); + + } finally { + + $this->executing = false; + + } + + $this->state = $success; + + if ($release_parent) + $this->releaseParent(); + + } + + /** + * ### Gets the current lifecycle state + * @since 1.0.0 + * + * @return TState Current lifecycle state. + */ + public function state ():UnitEnum { + + return $this->state; + + } + + /** + * ### Validates lifecycle component registration + * @since 1.0.0 + * + * @throws \FireHub\Kernel\Lifecycle\Exception\InvalidLifecycleStateException If registration is not allowed. + * + * @return void + */ + private function assertRegistrationAllowed ():void { + + if ($this->registration_closed || $this->executing) + throw new InvalidLifecycleStateException( + '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 diff --git a/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php b/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php new file mode 100644 index 0000000..fd81ed8 --- /dev/null +++ b/src/Lifecycle/Exception/ActiveLifecycleDependenciesException.php @@ -0,0 +1,27 @@ + + * @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; + + +/** + * ### Indicates active lifecycle dependencies + * + * Thrown when lifecycle termination is attempted while dependent components remain active. + * @since 1.0.0 + */ +final class ActiveLifecycleDependenciesException extends InvalidLifecycleStateException { + + protected const string DEFAULT_MESSAGE = 'Cannot terminate lifecycle while dependent components are active'; + +} \ 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..16bf380 --- /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 + */ +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 new file mode 100644 index 0000000..a54483e --- /dev/null +++ b/src/Lifecycle/Manager.php @@ -0,0 +1,206 @@ + + * @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\Runtime; +use Closure, Throwable; + +/** + * ### Manages lifecycle component execution + * + * Registers startup and shutdown callbacks and execute them according to their configured execution order. + * + * 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 class Manager { + + /** + * ### Registered startup callbacks + * @since 1.0.0 + * + * @var list + */ + private array $startup = []; + + /** + * ### Registered shutdown callbacks + * @since 1.0.0 + * + * @var list + */ + private array $shutdown = []; + + /** + * ### Registration sequence + * + * Preserves deterministic execution order for components having identical order values. + * @since 1.0.0 + */ + private int $sequence = 0; + + /** + * ### Registers a startup callback + * + * 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 Closure():void $callback

+ * Startup callback. + *

+ * @param int $order

+ * Execution order. + *

+ * + * @return void + */ + public function registerStartup (Closure $callback, int $order):void { + + $this->startup[] = [ + 'callback' => $callback, + 'order' => $order, + 'index' => $this->sequence++ + ]; + + } + + /** + * ### Registers a shutdown callback + * + * Adds a callback to the shutdown 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 Closure():void $callback

+ * Shutdown callback. + *

+ * @param int $order

+ * Execution order. + *

+ * + * @return void + */ + public function registerShutdown (Closure $callback, int $order):void { + + $this->shutdown[] = [ + 'callback' => $callback, + 'order' => $order, + 'index' => $this->sequence++ + ]; + + } + + /** + * ### Executes startup callbacks + * + * Executes all registered startup callbacks in ascending order. + * + * Execution stops immediately when a callback throws an exception. The original exception is propagated to the + * caller. + * @since 1.0.0 + * + * @uses \FireHub\Kernel\Lifecycle\Manager::ordered() To order callbacks. + * + * @throws Throwable If a startup callback fails. + * + * @return void + */ + public function start ():void { + + foreach ($this->ordered($this->startup) as $entry) + ($entry['callback'])(); + + } + + /** + * ### Executes shutdown callbacks + * + * Executes all registered shutdown callbacks in ascending order. + * + * 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\Lifecycle\Manager::ordered() To order callbacks. + * + * @throws Throwable If a shutdown callback fails. + * + * @return 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'] + ) + ); + + return $entries; + + } + +} \ No newline at end of file diff --git a/src/Lifecycle/States.php b/src/Lifecycle/States.php new file mode 100644 index 0000000..66ec779 --- /dev/null +++ b/src/Lifecycle/States.php @@ -0,0 +1,106 @@ + + * @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 represent distinct lifecycle phases. Each lifecycle owner normally supplies cases + * from a single enumeration. + * @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.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 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/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/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 new file mode 100644 index 0000000..3783221 --- /dev/null +++ b/tests/Unit/Lifecycle/ControllerTest.php @@ -0,0 +1,534 @@ + + * @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\Core\Meta\Enum\Kernel\State; +use FireHub\Kernel\Lifecycle\Controller; +use FireHub\Kernel\ { + Bootstrapper, Terminator +}; +use FireHub\Kernel\Exception\ { + InvalidKernelStateException, KernelBootException, KernelShutdownException +}; +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()); + + } + + /** + * ### 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 { + + $controller = new Controller; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap'); + + $controller->registerBootstrapper($bootstrapper, 0); + + $controller->boot(); + + self::assertSame(State::BOOTED, $controller->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 { + + $controller = new Controller; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $controller->registerTerminator($terminator, 0); + + $controller->shutdown(); + + self::assertSame(State::TERMINATED, $controller->state()); + + } + + /** + * ### Tests successful boot + * @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()); + + } + + /** + * ### 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 testBootRepeated ():void { + + $controller = new Controller; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->expects($this->once()) + ->method('bootstrap'); + + $controller->registerBootstrapper($bootstrapper, 0); + + $controller->boot(); + $controller->boot(); + + 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 testBootAfterShutdown ():void { + + $controller = new Controller; + + $controller->shutdown(); + + 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 testBootAfterFailure ():void { + + $controller = new Controller; + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $bootstrapper->method('bootstrap') + ->willThrowException(new \RuntimeException('Bootstrap failed.')); + + $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 testShutdown ():void { + + $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(State::TERMINATED, $controller->state()); + + } + + /** + * ### Tests repeated shutdown calls are ignored + * @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 { + + $controller = new Controller; + + $terminator = $this->createMock(Terminator::class); + + $terminator->expects($this->once()) + ->method('terminate'); + + $controller->registerTerminator($terminator, 0); + + $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); + + 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 testRegisterBootstrapperAfterBoot ():void { + + $controller = new Controller; + + $controller->boot(); + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $this->expectException(InvalidKernelStateException::class); + + $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 testRegisterTerminatorAfterBoot ():void { + + $controller = new Controller; + + $controller->boot(); + + $terminator = $this->createMock(Terminator::class); + + $this->expectException(InvalidKernelStateException::class); + + $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 testRegisterAfterShutdown ():void { + + $controller = new Controller; + + $controller->shutdown(); + + $bootstrapper = $this->createMock(Bootstrapper::class); + + $this->expectException(InvalidKernelStateException::class); + + $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 + * + * @return void + */ + public function testStateDuringBoot ():void { + + $controller = new Controller; + + $bootstrapper = $this->createMock(Bootstrapper::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/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 new file mode 100644 index 0000000..49c694a --- /dev/null +++ b/tests/Unit/Lifecycle/ManagerTest.php @@ -0,0 +1,262 @@ + + * @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\Manager; +use PHPUnit\Framework\Attributes\ { + CoversClass, Group, Small +}; +use RuntimeException, Throwable; + +/** + * ### Test kernel lifecycle component execution + * @since 1.0.0 + */ +#[Small] +#[Group('lifecycle')] +#[CoversClass(Manager::class)] +final class ManagerTest extends FireHubTestCase { + + /** + * ### Tests startup execution without callbacks + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testStartWithoutCallbacks ():void { + + $manager = new Manager; + + $manager->start(); + + self::assertTrue(true); + + } + + /** + * ### Tests shutdown execution without callbacks + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testStopWithoutCallbacks ():void { + + $manager = new Manager; + + $manager->stop(); + + self::assertTrue(true); + + } + + /** + * ### Tests startup ordering and stable priorities + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testStartupOrderAndStablePriority ():void { + + $manager = new Manager; + $executed = []; + + foreach ([['third', 30], ['first', 10], ['second', 10], ['fourth', 20]] as [$name, $order]) { + + $manager->registerStartup( + static function () use (&$executed, $name):void { + + $executed[] = $name; + + }, + $order + ); + + } + + $manager->start(); + + self::assertSame( + ['first', 'second', 'fourth', 'third'], + $executed + ); + + } + + /** + * ### Tests shutdown ordering and stable priorities + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testShutdownOrderAndStablePriority ():void { + + $manager = new Manager; + $executed = []; + + foreach ([['third', 30], ['first', 10], ['second', 10], ['fourth', 20]] as [$name, $order]) { + + $manager->registerShutdown( + static function () use (&$executed, $name):void { + + $executed[] = $name; + + }, + $order + ); + + } + + $manager->stop(); + + self::assertSame( + ['first', 'second', 'fourth', 'third'], + $executed + ); + + } + + /** + * ### Tests startup stops at the first failure + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testStartupStopsAtFirstFailure ():void { + + $manager = new Manager; + + $failure = new RuntimeException('Startup failed.'); + $executed = []; + + $manager->registerStartup( + static function () use (&$executed):void { + + $executed[] = 'first'; + + }, + 0 + ); + + $manager->registerStartup( + static function () use ($failure):void { + + throw $failure; + + }, + 1 + ); + + $manager->registerStartup( + static function () use (&$executed):void { + + $executed[] = 'never'; + + }, + 2 + ); + + try { + + $manager->start(); + + self::fail('Expected startup failure.'); + + } catch (RuntimeException $exception) { + + self::assertSame($failure, $exception); + self::assertSame(['first'], $executed); + + } + + } + + /** + * ### Tests shutdown executes all callbacks after failures + * @since 1.0.0 + * + * @throws Throwable + * + * @return void + */ + public function testShutdownRunsAllCallbacksAndRethrowsFirstFailure ():void { + + $manager = new Manager; + + $first = new RuntimeException('First failure.'); + $second = new RuntimeException('Second failure.'); + + $executed = []; + + $manager->registerShutdown( + static function () use (&$executed, $first):void { + + $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->stop(); + + self::fail('Expected shutdown failure.'); + + } catch (RuntimeException $exception) { + + self::assertSame($first, $exception); + + self::assertSame( + ['first', 'second', 'third'], + $executed + ); + + } + + } + +} \ No newline at end of file