diff --git a/CHANGELOG.md b/CHANGELOG.md index 751d2d4..6828060 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/Module/pull/1 \ No newline at end of file diff --git a/README.md b/README.md index 3d710fe..84a10d5 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ /> version diff --git a/composer.json b/composer.json index 9e918c9..7ca7fd3 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Modular application composition and lifecycle integration for The FireHub Project.", "type": "library", "keywords": ["php", "framework", "firehub", "application-modules", "modular-architecture", "module-system"], - "homepage": "https://the-firehub-project.github.io/kernel", + "homepage": "https://the-firehub-project.github.io/module", "license": "Apache-2.0", "require": { "php": "^8.5", diff --git a/src/Exception/CircularModuleDependencyException.php b/src/Exception/CircularModuleDependencyException.php new file mode 100644 index 0000000..e4df5a9 --- /dev/null +++ b/src/Exception/CircularModuleDependencyException.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 Module + */ + +namespace FireHub\Module\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Circular module dependency exception + * + * Thrown when a circular dependency is detected between registered application modules. + * @since 1.0.0 + */ +final class CircularModuleDependencyException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'Circular module dependency detected'; + +} \ No newline at end of file diff --git a/src/Exception/ModuleAlreadyRegisteredException.php b/src/Exception/ModuleAlreadyRegisteredException.php new file mode 100644 index 0000000..6cdab93 --- /dev/null +++ b/src/Exception/ModuleAlreadyRegisteredException.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 Module + */ + +namespace FireHub\Module\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Module already registered exception + * @since 1.0.0 + */ +final class ModuleAlreadyRegisteredException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'The module identifier is already registered'; + +} \ No newline at end of file diff --git a/src/Exception/ModuleDependencyNotFoundException.php b/src/Exception/ModuleDependencyNotFoundException.php new file mode 100644 index 0000000..bdbd0ff --- /dev/null +++ b/src/Exception/ModuleDependencyNotFoundException.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 Module + */ + +namespace FireHub\Module\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Required module dependency not found + * + * Thrown when a module declares a dependency that has not been registered in the module manager. + * @since 1.0.0 + */ +final class ModuleDependencyNotFoundException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'The module dependency was not found'; + +} \ No newline at end of file diff --git a/src/Exception/ModuleManagerFrozenException.php b/src/Exception/ModuleManagerFrozenException.php new file mode 100644 index 0000000..2fb2664 --- /dev/null +++ b/src/Exception/ModuleManagerFrozenException.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 Module + */ + +namespace FireHub\Module\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Module manager frozen exception + * + * Thrown when module registration is attempted after the module registry has been frozen. + * @since 1.0.0 + */ +final class ModuleManagerFrozenException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'The module manager is frozen'; + +} \ No newline at end of file diff --git a/src/Exception/ModuleNotFoundException.php b/src/Exception/ModuleNotFoundException.php new file mode 100644 index 0000000..91623b7 --- /dev/null +++ b/src/Exception/ModuleNotFoundException.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 Module + */ + +namespace FireHub\Module\Exception; + +use FireHub\Core\Exception\Runtime\OutOfBoundsException; + +/** + * ### Module was not found exception + * @since 1.0.0 + */ +final class ModuleNotFoundException extends OutOfBoundsException { + + protected const string DEFAULT_MESSAGE = 'The module identifier was not found'; + +} \ No newline at end of file diff --git a/src/Manager.php b/src/Manager.php new file mode 100644 index 0000000..2db6aae --- /dev/null +++ b/src/Manager.php @@ -0,0 +1,265 @@ + + * @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 Module + */ + +namespace FireHub\Module; + +use FireHub\Core\Boundary\Module; +use FireHub\Core\Boundary\Module\ { + Dependencies, RegistersServices +}; +use FireHub\Core\Boundary\Container\Registrar; +use FireHub\Module\Exception\ { + CircularModuleDependencyException, ModuleAlreadyRegisteredException, ModuleDependencyNotFoundException, + ModuleManagerFrozenException, ModuleNotFoundException +}; + +/** + * ### Manages registered application modules + * + * Maintains an ordered registry of modules without controlling application execution or introducing an independent + * module lifecycle. + * @since 1.0.0 + */ +final class Manager { + + /** + * ### Registered modules + * @var array, \FireHub\Core\Boundary\Module> + */ + private array $modules = []; + + /** + * ### Module registry freeze state + * @since 1.0.0 + */ + private bool $frozen = false; + + /** + * ### Registers a module + * @since 1.0.0 + * + * @param \FireHub\Core\Boundary\Module $module

+ * Module to register. + *

+ * + * @throws \FireHub\Module\Exception\ModuleAlreadyRegisteredException If the module is already registered. + * @throws \FireHub\Module\Exception\ModuleManagerFrozenException If the manager is frozen. + * + * @return self The manager instance. + */ + public function register (Module $module):self { + + if ($this->frozen) + throw new ModuleManagerFrozenException; + + $class = $module::class; + + if (isset($this->modules[$class])) + throw new ModuleAlreadyRegisteredException; + + $this->modules[$class] = $module; + + return $this; + + + } + + /** + * ### Checks whether a module is registered + * @since 1.0.0 + * + * @param class-string<\FireHub\Core\Boundary\Module> $class

+ * Module class name. + *

+ * + * @return bool True if the module is registered. + */ + public function has (string $class):bool { + + return isset($this->modules[$class]); + + } + + /** + * ### Gets a registered module + * @since 1.0.0 + * + * @param class-string<\FireHub\Core\Boundary\Module> $class

+ * Module class name. + *

+ * + * @throws \FireHub\Module\Exception\ModuleNotFoundException If the module is not registered. + * + * @return \FireHub\Core\Boundary\Module Registered module. + */ + public function get (string $class):Module { + + return $this->modules[$class] + ?? throw new ModuleNotFoundException; + + } + + /** + * ### Gets all registered modules + * @since 1.0.0 + * + * @return array, \FireHub\Core\Boundary\Module> Registered modules. + */ + public function all ():array { + + return $this->modules; + + } + + /** + * ### Gets modules in dependency order + * + * Resolves module dependencies using topological sorting. + * + * Dependencies are placed before the modules that require them. Modules without dependency relationships + * preserve their registration order. + * @since 1.0.0 + * + * @uses \FireHub\Module\Manager::visit() Visits a module dependency graph. + * + * @throws \FireHub\Module\Exception\ModuleDependencyNotFoundException If a required module is not registered. + * @throws \FireHub\Module\Exception\CircularModuleDependencyException If circular dependencies are detected. + * + * @return list<\FireHub\Core\Boundary\Module> Modules in dependency order. + */ + public function ordered ():array { + + /** @var array, int> $states */ + $states = []; + + /** @var list<\FireHub\Core\Boundary\Module> $ordered */ + $ordered = []; + + foreach ($this->modules as $module) + $this->visit($module, $states, $ordered); + + return $ordered; + + } + + /** + * ### Registers services provided by modules + * + * Invokes service registration for all registered modules in dependency order. + * + * Modules that do not implement the registration capability are skipped. + * + * Service registration must occur before the container is frozen. + * @since 1.0.0 + * + * @uses \FireHub\Module\Manager::ordered() Gets modules in dependency order. + * @uses \FireHub\Core\Boundary\Module\RegistersServices::register() To register services. + * + * @param \FireHub\Core\Boundary\Container\Registrar $registrar

+ * Service registrar. + *

+ * + * @throws \FireHub\Module\Exception\ModuleDependencyNotFoundException If a required module is not registered. + * @throws \FireHub\Module\Exception\CircularModuleDependencyException If circular dependencies are detected. + * + * @return void + */ + public function registerServices (Registrar $registrar):void { + + foreach ($this->ordered() as $module) { + + if ($module instanceof RegistersServices) + $module->register($registrar); + + } + + } + + /** + * ### Freezes the module registry + * + * Prevents further module registration while preserving access to previously registered modules. + * @since 1.0.0 + * + * @return void + */ + public function freeze ():void { + + $this->frozen = true; + + } + + /** + * ### Visits a module dependency graph + * + * Performs a depth-first traversal to resolve module ordering and detect circular dependencies. + * @since 1.0.0 + * + * @uses \FireHub\Core\Boundary\Module\Dependencies::dependencies() Gets the module dependencies. + * + * @param \FireHub\Core\Boundary\Module $module

+ * Module to visit. + *

+ * @param array, int> $states

+ * Module visitation states. + *

+ * @param list<\FireHub\Core\Boundary\Module> $ordered

+ * Resolved module order. + *

+ * + * @throws \FireHub\Module\Exception\ModuleDependencyNotFoundException If a required module is not registered. + * @throws \FireHub\Module\Exception\CircularModuleDependencyException If circular dependencies are detected. + * + * @return void + */ + private function visit (Module $module, array &$states, array &$ordered):void { + + $class = $module::class; + + $state = $states[$class] ?? 0; + + if ($state === 2) return; + + if ($state === 1) + throw new CircularModuleDependencyException( + context: [ + 'module' => $class, + ] + ); + + $states[$class] = 1; + + if ($module instanceof Dependencies) { + + foreach ($module->dependencies() as $dependency) { + + if (!isset($this->modules[$dependency])) + throw new ModuleDependencyNotFoundException( + context: [ + 'dependency' => $dependency, + ] + ); + + $this->visit($this->modules[$dependency], $states, $ordered); + + } + + } + + $states[$class] = 2; + + $ordered[] = $module; + + } + +} \ No newline at end of file