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 @@
+ * 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+ * 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+ * 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+ * 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+ * 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+ * 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