From 922483cdbc85f42681495a8cd2dd84de9c825652 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 13:43:46 +0200 Subject: [PATCH 01/26] Enhanced README structure and added workflow badges. Improved clarity by reorganizing content, removed redundant sections, and introduced explicit guidelines for the `develop` branch usage. --- README.md | 86 ++++++++++++++++++++++--------------------------------- 1 file changed, 34 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index eb1a2c9..6bcfe06 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ /> +[![PHPStan](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPStan.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPStan.yml) +[![PHPUnit](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPUnit.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPUnit.yml) +[![Codecov](https://codecov.io/gh/The-FireHub-Project/Container/branch/develop/graph/badge.svg?token=XW2YEONF51)](https://app.codecov.io/gh/The-FireHub-Project/Container/tree/develop) +

FireHub Icon Repository Purpose - -FireHub Container is the dependency injection and service resolution component of the FireHub ecosystem. - -Built on the contracts provided by FireHub Core, it supplies the mechanisms required to register, construct, resolve, and manage application services. - -The purpose of this repository is to centralize dependency management while preserving separation of concerns between service definitions, runtime implementation, and application execution. +## FireHub Icon Development Branch -The Container architecture is designed to support: +⚠️ **This is the `develop` branch** -- Service registration and dependency resolution -- Interface-to-implementation bindings -- Instance, factory, and class-based service definitions -- Singleton and transient service lifetimes -- Dependency graph resolution and circular dependency detection -- Immutable service registrations after container construction -- Future support for autowiring and execution-scoped services +- Unstable +- APIs may change without notice +- Not intended for production use -FireHub Container follows the architectural principles of The FireHub Project by separating responsibilities between contracts, dependency management, and application lifecycle orchestration. +## FireHub Icon Branch Purpose -The Container does not control application execution or kernel lifecycle transitions. Instead, it provides dependency management services that can be integrated into applications through the appropriate bootstrap and composition mechanisms. +The `develop` branch is the **primary integration branch** for all ongoing development. -## FireHub Icon Security +It serves as the staging area where: +- Feature branches are merged +- Bug fixes are integrated +- Experimental work is stabilized +- Code is prepared for upcoming releases -For security reporting and guidelines, please refer to our [SECURITY.md](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file. +All **release branches** are created **from `develop`**. -## FireHub Icon Community +## Stability Guarantee -![GitHub watchers](https://img.shields.io/github/watchers/The-FireHub-Project/Container?style=social) -![GitHub Repo stars](https://img.shields.io/github/stars/The-FireHub-Project/Container?style=social) -![GitHub forks](https://img.shields.io/github/forks/The-FireHub-Project/Container?style=social) +❌ No backward compatibility guarantee +❌ APIs may change without notice +❌ Behavior may be incomplete or inconsistent +❌ Breaking changes are expected -- Be sure to review the [Code of Conduct](https://github.com/The-FireHub-Project/.github/blob/master/.github/CODE_OF_CONDUCT.md). -- Read a [Contributing](https://github.com/The-FireHub-Project/.github/blob/master/.github/CONTRIBUTING.md) file to see how you can contribute to this project. -- Read our [Security policy](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file to see how you can report a vulnerability. -- Review the project [Governance model](https://github.com/The-FireHub-Project/.github/blob/master/.github/GOVERNANCE.md), including roles, decision-making, RFC process, and releases. +This branch is intended **only for contributors and advanced testers**. -## FireHub Icon Support - -

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

+## FireHub Icon Composer Usage (Not Recommended) -For help with FireHub products, please refer to the following resources: +For internal testing only: -- **General questions & discussions**: [GitHub Discussions](https://github.com/orgs/The-FireHub-Project/discussions) -- **Bug reports & issues**: [Support](https://github.com/The-FireHub-Project/Container/issues) -- **Documentation & guides**: [FireHub Docs](https://the-firehub-project.github.io) -- **Commercial support** (Pro/Enterprise): contact your account manager +```json +{ + "require": { + "the-firehub-project/container": "dev-develop" + } +} +``` +⚠️ Never use dev-develop in production. ## FireHub Icon Authors and Contributors @@ -152,4 +134,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). +For more details, read the full license [here](./LICENSE). \ No newline at end of file From 671ea546f1caac8f933eda324b01da4c6ce83b4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 14:56:52 +0200 Subject: [PATCH 02/26] Introduced foundational service definitions for the FireHub dependency injection container. Added interfaces (`Definition` and `Service`) and concrete implementations (`Instance`, `Factory`, `Cls`, and `Alias`) to define service lifetimes, resolutions, and aliases. Enhanced modularity and extensibility for container services. --- src/Definition.php | 22 ++++++++++++++ src/Definition/Alias.php | 42 ++++++++++++++++++++++++++ src/Definition/Cls.php | 59 +++++++++++++++++++++++++++++++++++++ src/Definition/Factory.php | 59 +++++++++++++++++++++++++++++++++++++ src/Definition/Instance.php | 51 ++++++++++++++++++++++++++++++++ src/Definition/Service.php | 42 ++++++++++++++++++++++++++ 6 files changed, 275 insertions(+) create mode 100644 src/Definition.php create mode 100644 src/Definition/Alias.php create mode 100644 src/Definition/Cls.php create mode 100644 src/Definition/Factory.php create mode 100644 src/Definition/Instance.php create mode 100644 src/Definition/Service.php diff --git a/src/Definition.php b/src/Definition.php new file mode 100644 index 0000000..27cedb2 --- /dev/null +++ b/src/Definition.php @@ -0,0 +1,22 @@ + + * @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 Container + */ + +namespace FireHub\Container; + +/** + * ### Defines a service definition + * + * Describes the lifetime of a registered service without prescribing how its instance is created or resolved. + * @since 1.0.0 + */ +interface Definition {} \ No newline at end of file diff --git a/src/Definition/Alias.php b/src/Definition/Alias.php new file mode 100644 index 0000000..687b0fa --- /dev/null +++ b/src/Definition/Alias.php @@ -0,0 +1,42 @@ + + * @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 Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Container\Definition; + +/** + * ### Defines a service alias + * + * Redirects service resolution to another registered service identifier. + * + * Aliases do not own service instances or maintain independent lifetimes. + * @since 1.0.0 + */ +final readonly class Alias implements Definition { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param class-string $target

+ * Target service identifier. + *

+ * + * @return void + */ + public function __construct ( + public string $target + ) {} + +} \ No newline at end of file diff --git a/src/Definition/Cls.php b/src/Definition/Cls.php new file mode 100644 index 0000000..aae8c07 --- /dev/null +++ b/src/Definition/Cls.php @@ -0,0 +1,59 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Core\Meta\Enum\Container\Lifetime; + +/** + * ### Defines a class-based service + * + * Describes a service whose instance is constructed from a concrete class. + * + * The container may resolve constructor dependencies automatically. + * @since 1.0.0 + * + * @template TService of object + */ +final readonly class Cls implements Service { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param class-string $class

+ * Concrete implementation class. + *

+ * @param \FireHub\Core\Meta\Enum\Container\Lifetime $lifetime

+ * Service lifetime. + *

+ * + * @return void + */ + public function __construct ( + public string $class, + private Lifetime $lifetime = Lifetime::TRANSIENT + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function lifetime ():Lifetime { + + return $this->lifetime; + + } + +} \ No newline at end of file diff --git a/src/Definition/Factory.php b/src/Definition/Factory.php new file mode 100644 index 0000000..feb090a --- /dev/null +++ b/src/Definition/Factory.php @@ -0,0 +1,59 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Core\Meta\Enum\Container\Lifetime; +use Closure; + +/** + * ### Defines a factory-based service + * + * Describes a service created by invoking a factory function. + * + * The factory receives the active dependency resolver, allowing dependencies to be resolved within the appropriate + * execution scope. + * @since 1.0.0 + */ +final readonly class Factory implements Service { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param Closure(\FireHub\Core\Boundary\Resolver):object $factory

+ * Service factory. + *

+ * @param \FireHub\Core\Meta\Enum\Container\Lifetime $lifetime

+ * Service lifetime. + *

+ * + * @return void + */ + public function __construct ( + public Closure $factory, + private Lifetime $lifetime = Lifetime::TRANSIENT + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function lifetime ():Lifetime { + + return $this->lifetime; + + } + +} \ No newline at end of file diff --git a/src/Definition/Instance.php b/src/Definition/Instance.php new file mode 100644 index 0000000..ea48c31 --- /dev/null +++ b/src/Definition/Instance.php @@ -0,0 +1,51 @@ + + * @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 Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Core\Meta\Enum\Container\Lifetime; + +/** + * ### Defines an existing service instance + * + * Represents an already constructed object registered as a singleton service. + * @since 1.0.0 + */ +final readonly class Instance implements Service { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param object $instance

+ * Registered service instance. + *

+ * + * @return void + */ + public function __construct ( + public object $instance + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function lifetime ():Lifetime { + + return Lifetime::SINGLETON; + + } + +} \ No newline at end of file diff --git a/src/Definition/Service.php b/src/Definition/Service.php new file mode 100644 index 0000000..5c53b16 --- /dev/null +++ b/src/Definition/Service.php @@ -0,0 +1,42 @@ + + * @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.4 + * @package Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Core\Meta\Enum\Container\Lifetime; +use FireHub\Container\Definition; + +/** + * ### Defines a service definition with a managed lifetime + * + * Represents a registered service definition that specifies how long its resolved instance should be retained + * and reused by the dependency injection container. + * + * Service definitions provide lifetime metadata without prescribing how service instances are constructed, + * cached, or resolved. + * @since 1.0.0 + */ +interface Service extends Definition { + + /** + * ### Gets the service lifetime + * + * Returns the lifetime associated with this service definition, determining whether resolved instances are + * shared across the container, isolated within individual scopes, or created for each resolution request. + * @since 1.0.0 + * + * @return \FireHub\Core\Meta\Enum\Container\Lifetime Configured service lifetime. + */ + public function lifetime ():Lifetime; + +} \ No newline at end of file From 8ec1e8f5cb358be4b18e22fb6d3985f51437126f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 14 Oct 2026 15:16:07 +0200 Subject: [PATCH 03/26] Added core internal components for the FireHub dependency injection container. Implemented foundational classes (`Registry`, `InstanceStore`, and `ResolutionStack`) to handle service registration, instance management, and circular dependency detection. Introduced custom exceptions (`CircularDependencyException`, `DuplicateServiceException`, and `ServiceNotFoundException`) to enforce registry invariants and improve error handling. Designed the architecture to enhance resolution tracking and service definition management within the container. --- src/Exception/CircularDependencyException.php | 26 ++++ src/Exception/DuplicateServiceException.php | 29 +++++ src/Exception/ServiceNotFoundException.php | 28 ++++ src/Internal/InstanceStore.php | 97 ++++++++++++++ src/Internal/ResolutionStack.php | 78 +++++++++++ src/Registry.php | 122 ++++++++++++++++++ 6 files changed, 380 insertions(+) create mode 100644 src/Exception/CircularDependencyException.php create mode 100644 src/Exception/DuplicateServiceException.php create mode 100644 src/Exception/ServiceNotFoundException.php create mode 100644 src/Internal/InstanceStore.php create mode 100644 src/Internal/ResolutionStack.php create mode 100644 src/Registry.php diff --git a/src/Exception/CircularDependencyException.php b/src/Exception/CircularDependencyException.php new file mode 100644 index 0000000..183e044 --- /dev/null +++ b/src/Exception/CircularDependencyException.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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a circular dependency is detected + * @since 1.0.0 + */ +final class CircularDependencyException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'Circular dependency detected'; + +} \ No newline at end of file diff --git a/src/Exception/DuplicateServiceException.php b/src/Exception/DuplicateServiceException.php new file mode 100644 index 0000000..282a8eb --- /dev/null +++ b/src/Exception/DuplicateServiceException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a service identifier is already registered + * + * Indicates that a service registration would violate the registry invariant requiring each service identifier + * to have exactly one associated definition. + * @since 1.0.0 + */ +final class DuplicateServiceException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'A service with the specified identifier is already registered'; + +} \ No newline at end of file diff --git a/src/Exception/ServiceNotFoundException.php b/src/Exception/ServiceNotFoundException.php new file mode 100644 index 0000000..3f9d4c4 --- /dev/null +++ b/src/Exception/ServiceNotFoundException.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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a service definition cannot be found + * + * Indicates that the requested service identifier has no corresponding registration in the container registry. + * @since 1.0.0 + */ +final class ServiceNotFoundException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'No service definition found for the specified identifier'; + +} \ No newline at end of file diff --git a/src/Internal/InstanceStore.php b/src/Internal/InstanceStore.php new file mode 100644 index 0000000..95f1a8f --- /dev/null +++ b/src/Internal/InstanceStore.php @@ -0,0 +1,97 @@ + + * @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.0 + * @package Container + */ + +namespace FireHub\Container\Internal; + +/** + * ### Stores resolved service instances + * + * Maintains a collection of resolved service instances indexed by their registered service identifiers. + * @since 1.0.0 + * + * @internal + */ +final class InstanceStore { + + /** + * ### Service instances + * @since 1.0.0 + * + * @var array + */ + private array $instances = []; + + /** + * ### Checks whether an instance exists + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return bool True if the instance is stored. + */ + public function has (string $id):bool { + + return isset($this->instances[$id]); + + } + + /** + * ### Gets a stored instance + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return null|object Stored instance, or null if absent. + */ + public function get (string $id):?object { + + return $this->instances[$id] ?? null; + + } + + /** + * ### Stores a service instance + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * @param object $instance

+ * Service instance. + *

+ * + * @return void + */ + public function set (string $id, object $instance):void { + + $this->instances[$id] = $instance; + + } + + /** + * ### Releases stored instances + * @since 1.0.0 + * + * @return void + */ + public function clear ():void { + + $this->instances = []; + + } + +} \ No newline at end of file diff --git a/src/Internal/ResolutionStack.php b/src/Internal/ResolutionStack.php new file mode 100644 index 0000000..6c6aa4c --- /dev/null +++ b/src/Internal/ResolutionStack.php @@ -0,0 +1,78 @@ + + * @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.0 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\CircularDependencyException; +use FireHub\Runtime; + +/** + * ### Tracks active service resolutions + * + * Detects circular dependencies by tracking service identifiers currently being resolved. + * @since 1.0.0 + * + * @internal + */ +final class ResolutionStack { + + /** + * ### Service resolution stack + * @since 1.0.0 + * + * @var list + */ + private array $stack = []; + + /** + * ### Starts resolving a service + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Inspection::inArray() Checks if the service is already being resolved. + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @throws \FireHub\Container\Exception\CircularDependencyException If the service is already being resolved. + * + * @return void + */ + public function enter (string $id):void { + + if (Runtime\Arr\Inspection::inArray($this->stack, $id)) + throw new CircularDependencyException( + context: [ + 'stack' => $this->stack + ] + ); + + $this->stack[] = $id; + + } + + /** + * ### Finishes resolving the current service + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Mutation::pop() Removes the last element from the array. + * + * @return void + */ + public function leave ():void { + + Runtime\Arr\Mutation::pop($this->stack); + + } + +} \ No newline at end of file diff --git a/src/Registry.php b/src/Registry.php new file mode 100644 index 0000000..49f354e --- /dev/null +++ b/src/Registry.php @@ -0,0 +1,122 @@ + + * @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 Container + */ + +namespace FireHub\Container; + +use FireHub\Container\Exception\ { + DuplicateServiceException, ServiceNotFoundException +}; + +/** + * ### Manages registered service definitions + * + * Provides centralized storage and retrieval of dependency injection service definitions. + * + * Each service definition is associated with a unique class or interface identifier. Duplicate registrations + * are rejected to prevent accidental replacement of existing service definitions. + * + * The registry does not construct service instances, execute factories, or manage service lifetimes. + * @since 1.0.0 + */ +final class Registry { + + /** + * ### Registered service definitions + * @since 1.0.0 + * + * @var array + */ + private array $definitions = []; + + /** + * ### Registers a service definition + * + * Associates the specified service identifier with its definition. + * + * An identifier may only be registered once within the registry. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * @param \FireHub\Container\Definition $definition

+ * Service definition. + *

+ * + * @throws \FireHub\Container\Exception\DuplicateServiceException If the service identifier is already registered. + * + * @return void + */ + public function register (string $id, Definition $definition):void { + + if (isset($this->definitions[$id])) + throw new DuplicateServiceException; + + $this->definitions[$id] = $definition; + + } + + /** + * ### Gets a registered service definition + * + * Returns the definition associated with the specified service identifier without resolving the service. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * + * @return \FireHub\Container\Definition Registered service definition. + */ + public function get (string $id):Definition { + + return $this->definitions[$id] + ?? throw new ServiceNotFoundException; + + } + + /** + * ### Checks whether a service is registered + * + * Determines whether a definition exists for the specified service identifier. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return bool True if the service identifier is registered. + */ + public function has (string $id):bool { + + return isset($this->definitions[$id]); + + } + + /** + * ### Gets all registered service definitions + * + * Returns all registered service definitions indexed by their service identifiers. + * @since 1.0.0 + * + * @return array Registered service definitions. + */ + public function all ():array { + + return $this->definitions; + + } + +} \ No newline at end of file From 94d1e65a7909fdd91303b722f0cd52e0780ce8df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 09:22:16 +0200 Subject: [PATCH 04/26] Added core internal components for the FireHub dependency injection container. Implemented foundational classes (`Registry`, `InstanceStore`, and `ResolutionStack`) to handle service registration, instance management, and circular dependency detection. Introduced custom exceptions (`CircularDependencyException`, `DuplicateServiceException`, and `ServiceNotFoundException`) to enforce registry invariants and improve error handling. Designed the architecture to enhance resolution tracking and service definition management within the container. --- src/Container.php | 465 ++++++++++++++++++ src/Exception/ContainerFrozenException.php | 26 + src/Exception/ScopeClosedException.php | 26 + src/Exception/ScopeRequiredException.php | 29 ++ .../ServiceInstantiationException.php | 29 ++ .../ServiceTypeMismatchException.php | 29 ++ .../UnsupportedDefinitionException.php | 29 ++ src/Internal/ContextualResolver.php | 72 +++ src/Internal/ResolutionContext.php | 64 +++ src/Registry.php | 2 + src/Resolver.php | 391 +++++++++++++++ src/Sccpe.php | 192 ++++++++ 12 files changed, 1354 insertions(+) create mode 100644 src/Container.php create mode 100644 src/Exception/ContainerFrozenException.php create mode 100644 src/Exception/ScopeClosedException.php create mode 100644 src/Exception/ScopeRequiredException.php create mode 100644 src/Exception/ServiceInstantiationException.php create mode 100644 src/Exception/ServiceTypeMismatchException.php create mode 100644 src/Exception/UnsupportedDefinitionException.php create mode 100644 src/Internal/ContextualResolver.php create mode 100644 src/Internal/ResolutionContext.php create mode 100644 src/Resolver.php create mode 100644 src/Sccpe.php diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..e07d542 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,465 @@ + + * @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 Container + */ + +namespace FireHub\Container; + +use FireHub\Core\Boundary\Resolver as ResolverBoundary; +use FireHub\Core\Meta\Enum\Container\Lifetime; +use FireHub\Container\Definition\ { + Alias, Cls, Factory, Instance +}; +use FireHub\Container\Internal\InstanceStore; +use FireHub\Container\Exception\ContainerFrozenException; +use Closure; + +/** + * ### Dependency injection container + * + * Provides the central entry point for registering and resolving services within the FireHub dependency injection + * system. + * + * The container coordinates service definitions through a registry and delegates service resolution to a dedicated + * resolver. + * + * Service definitions may be registered during the configuration phase. Once the container is frozen, further + * registrations are prohibited. + * + * The container supports singleton, scoped, and transient service lifetimes. Scoped services require an active + * resolution scope. + * @since 1.0.0 + */ +final class Container implements ResolverBoundary { + + /** + * ### Service registry + * @since 1.0.0 + */ + private readonly Registry $registry; + + /** + * ### Service resolver + * @since 1.0.0 + */ + private readonly Resolver $resolver; + + /** + * ### Container freeze state + * @var bool + */ + private bool $frozen = false; + + /** + * ### Constructor + * @since 1.0.0 + * + * @return void + */ + public function __construct () { + + $this->registry = new Registry; + + $this->resolver = new Resolver( + $this->registry, + new InstanceStore + ); + + } + + /** + * ### Creates a dependency injection scope + * + * Creates an independent resolution scope associated with this container. + * + * Each scope maintains its own cache of scoped service instances while sharing singleton instances and service + * definitions with the parent container. + * + * Scopes must be explicitly closed when they are no longer needed. + * @since 1.0.0 + * + * @return \FireHub\Container\Scope Created resolution scope. + */ + public function createScope ():Scope { + + return new Scope($this->resolver); + + } + + /** + * ### Registers a service definition + * + * Associates a service identifier with its definition. + * + * Registration is only permitted while the container is not frozen. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::assertMutable() To check if the container is frozen. + * @uses \FireHub\Container\Registry::register() To register a service definition. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param \FireHub\Container\Definition $definition

+ * Service definition. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function register (string $id, Definition $definition):self { + + $this->assertMutable(); + + $this->registry->register($id, $definition); + + return $this; + + } + + /** + * ### Registers a class-based service + * + * Associates a service identifier with a concrete implementation class. + * + * The service is instantiated lazily according to the specified lifetime. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param class-string $class

+ * Concrete implementation class. + *

+ * @param \FireHub\Core\Meta\Enum\Container\Lifetime $lifetime [optional]

+ * Service lifetime. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function bind (string $id, string $class, Lifetime $lifetime = Lifetime::TRANSIENT):self { + + return $this->register( + $id, + new Cls($class, $lifetime) + ); + + } + + /** + * ### Registers a singleton service + * + * Registers a class-based service instantiated once per container. + * + * When no implementation is provided, the identifier itself is used as the implementation class. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::bind() To register a class-based service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param null|class-string $class [optional]

+ * Implementation class. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function singleton (string $id, ?string $class = null):self { + + return $this->bind( + $id, + $class ?? $id, + Lifetime::SINGLETON + ); + + } + + /** + * ### Registers a scoped service + * + * Registers a class-based service instantiated once per resolution scope. + * + * Each independent scope receives its own service instance. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::bind() To register a class-based service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param null|class-string $class [optional]

+ * Implementation class. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function scoped (string $id, ?string $class = null):self { + + return $this->bind( + $id, + $class ?? $id, + Lifetime::SCOPED + ); + + } + + /** + * ### Registers a transient service + * + * Registers a class-based service instantiated on each resolution. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::bind() To register a class-based service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param null|class-string $class [optional]

+ * Implementation class. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function transient (string $id, ?string $class = null):self { + + return $this->bind( + $id, + $class ?? $id + ); + + } + + /** + * ### Registers a factory-based service + * + * Registers a factory responsible for creating the requested service. + * + * The factory receives a contextual resolver to resolve dependencies within the current resolution context. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param Closure(\FireHub\Core\Boundary\Resolver):TService $factory

+ * Service factory. + *

+ * @param \FireHub\Core\Meta\Enum\Container\Lifetime $lifetime [optional]

+ * Service lifetime. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function factory (string $id, Closure $factory, Lifetime $lifetime = Lifetime::TRANSIENT):self { + + return $this->register( + $id, + new Factory($factory, $lifetime) + ); + + } + + /** + * ### Registers an existing service instance + * + * Registers an already constructed object as a singleton service. + * + * The provided instance is returned directly during resolution. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param TService $instance

+ * Existing service instance. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function instance (string $id, object $instance):self { + + return $this->register( + $id, + new Instance($instance) + ); + + } + + /** + * ### Registers a service alias + * + * Associates an additional identifier with another registered service. + * + * Aliases preserve the lifetime and identity of the target service. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @template TService of object + * + * @param class-string $id

+ * Alias identifier. + *

+ * @param class-string $target

+ * Target service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function alias (string $id, string $target):self { + + return $this->register( + $id, + new Alias($target) + ); + + } + + /** + * ### Resolves a registered service + * + * Resolves the requested service using the configured service definitions. + * + * Singleton services are shared across the container. Transient services are constructed on each resolution. + * + * Scoped services cannot be resolved without an active scope. + * @since 1.0.0 + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * + * @return TService Resolved service. + */ + public function get (string $id):object { + + return $this->resolver->get($id); + + } + + /** + * ### Checks whether a service is registered + * + * Determines whether the registry contains a definition for the specified service identifier. + * + * This operation does not instantiate the service. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return bool True if registered, false otherwise. + */ + public function has (string $id):bool { + + return $this->registry->has($id); + + } + + /** + * ### Freezes the container + * + * Prevents any further service registrations. + * + * Existing service definitions remain available for resolution. + * + * Calling this method multiple times has no additional effect. + * @since 1.0.0 + * + * @return void + */ + public function freeze ():void { + + $this->frozen = true; + + } + + /** + * ### Checks whether the container is frozen + * @since 1.0.0 + * + * @return bool True if frozen, false otherwise. + */ + public function isFrozen ():bool { + + return $this->frozen; + + } + + /** + * ### Ensures the container accepts registrations + * + * Prevents modification of service definitions after the container has been frozen. + * @since 1.0.0 + * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * + * @return void + */ + private function assertMutable ():void { + + if ($this->frozen) + throw new ContainerFrozenException; + + } + +} \ No newline at end of file diff --git a/src/Exception/ContainerFrozenException.php b/src/Exception/ContainerFrozenException.php new file mode 100644 index 0000000..703317e --- /dev/null +++ b/src/Exception/ContainerFrozenException.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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a container is attempted to be modified after it has been frozen + * @since 1.0.0 + */ +final class ContainerFrozenException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'Cannot register services in a frozen container'; + +} \ No newline at end of file diff --git a/src/Exception/ScopeClosedException.php b/src/Exception/ScopeClosedException.php new file mode 100644 index 0000000..cd47719 --- /dev/null +++ b/src/Exception/ScopeClosedException.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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Thrown when a scoped service is registered after the scope has been closed + * @since 1.0.0 + */ +final class ScopeClosedException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'Cannot register services in a closed scope'; + +} \ No newline at end of file diff --git a/src/Exception/ScopeRequiredException.php b/src/Exception/ScopeRequiredException.php new file mode 100644 index 0000000..083c50e --- /dev/null +++ b/src/Exception/ScopeRequiredException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a scoped service is resolved without an active scope + * + * Indicates that a service configured with scoped lifetime was requested from a resolution context that does + * not provide scoped instance storage. + * @since 1.0.0 + */ +final class ScopeRequiredException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'Scoped service requires an active scope'; + +} \ No newline at end of file diff --git a/src/Exception/ServiceInstantiationException.php b/src/Exception/ServiceInstantiationException.php new file mode 100644 index 0000000..bd23fe3 --- /dev/null +++ b/src/Exception/ServiceInstantiationException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Thrown when a service cannot be instantiated + * + * Indicates that the dependency resolver failed to construct a service instance from its registered class + * definition. + * @since 1.0.0 + */ +final class ServiceInstantiationException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'Failed to instantiate service'; + +} \ No newline at end of file diff --git a/src/Exception/ServiceTypeMismatchException.php b/src/Exception/ServiceTypeMismatchException.php new file mode 100644 index 0000000..1dbc31b --- /dev/null +++ b/src/Exception/ServiceTypeMismatchException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a resolved service does not match its registered identifier + * + * Indicates that a service definition produced an object that is not compatible with the class or interface + * associated with its registration. + * @since 1.0.0 + */ +final class ServiceTypeMismatchException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'Resolved service is not compatible with its registered identifier'; + +} \ No newline at end of file diff --git a/src/Exception/UnsupportedDefinitionException.php b/src/Exception/UnsupportedDefinitionException.php new file mode 100644 index 0000000..c40dbfa --- /dev/null +++ b/src/Exception/UnsupportedDefinitionException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when an unsupported service definition is encountered + * + * Indicates that the container cannot process a registered service definition because its type is not supported + * by the dependency resolver. + * @since 1.0.0 + */ +final class UnsupportedDefinitionException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'Unsupported service definition'; + +} \ No newline at end of file diff --git a/src/Internal/ContextualResolver.php b/src/Internal/ContextualResolver.php new file mode 100644 index 0000000..1319f35 --- /dev/null +++ b/src/Internal/ContextualResolver.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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Core\Boundary\Resolver; +use Closure; + +/** + * ### Resolves dependencies within an existing resolution context + * + * Preserves the active resolution context across nested factory resolutions. + * @since 1.0.0 + * + * @internal + */ +final readonly class ContextualResolver implements Resolver { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param Closure(class-string):object $resolve

+ * Service resolver. + *

+ * @param Closure(class-string):bool $has

+ * Service existence checker. + *

+ * + * @return void + */ + public function __construct ( + private Closure $resolve, + private Closure $has + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + * + * @template TService of object + */ + public function get (string $id):object { + + /** @var TService */ + return ($this->resolve)($id); // @phpstan-ignore return.type + + } + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function has (string $id):bool { + + return ($this->has)($id); + + } + +} \ No newline at end of file diff --git a/src/Internal/ResolutionContext.php b/src/Internal/ResolutionContext.php new file mode 100644 index 0000000..d863993 --- /dev/null +++ b/src/Internal/ResolutionContext.php @@ -0,0 +1,64 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +/** + * ### Represents an active dependency resolution context + * + * Maintains the state associated with a single dependency resolution chain. + * + * The context tracks circular dependencies and provides access to the active scoped instance storage. + * + * A new context is created for each independent resolution request and shared by all nested resolutions + * belonging to that request. + * @since 1.0.0 + * + * @internal + */ +final readonly class ResolutionContext { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ResolutionStack $stack

+ * Active dependency resolution stack. + *

+ * @param null|\FireHub\Container\Internal\InstanceStore $scoped

+ * Active scoped instance storage, or null for root resolution. + *

+ */ + public function __construct ( + public ResolutionStack $stack = new ResolutionStack, + public ?InstanceStore $scoped = null + ) {} + + /** + * ### Creates a root resolution context + * + * Returns a context that does not provide access to scoped instances while preserving the current + * dependency resolution stack. + * @since 1.0.0 + * + * @return self Root resolution context. + */ + public function root ():self { + + return new self( + stack: $this->stack + ); + + } + +} \ No newline at end of file diff --git a/src/Registry.php b/src/Registry.php index 49f354e..3a154a9 100644 --- a/src/Registry.php +++ b/src/Registry.php @@ -27,6 +27,8 @@ * * The registry does not construct service instances, execute factories, or manage service lifetimes. * @since 1.0.0 + * + * @internal */ final class Registry { diff --git a/src/Resolver.php b/src/Resolver.php new file mode 100644 index 0000000..b553e1c --- /dev/null +++ b/src/Resolver.php @@ -0,0 +1,391 @@ + + * @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 Container + */ + +namespace FireHub\Container; + +use FireHub\Core\Boundary\Resolver as ResolverBoundary; +use FireHub\Core\Meta\Enum\Container\Lifetime; +use FireHub\Container\Definition\ { + Alias, Cls, Factory, Instance, Service +}; +use FireHub\Container\Internal\ { + ContextualResolver, InstanceStore, ResolutionContext +}; +use FireHub\Container\Exception\ { + ScopeRequiredException, ServiceInstantiationException, ServiceTypeMismatchException, UnsupportedDefinitionException +}; +use ReflectionClass; + +/** + * ### Resolves registered dependency injection services + * + * Creates and retrieves service instances from registered definitions while enforcing their configured lifetimes. + * + * Supports singleton, scoped, and transient services, including class-based construction, factory invocation, + * existing instances, and service aliases. + * + * Singleton instances are shared across all resolution contexts, scoped instances are isolated within their + * associated scope, and transient instances are created for each resolution request. + * + * The resolver detects circular dependencies and validates resolved objects against their service identifiers. + * @since 1.0.0 + * + * @internal + */ +final readonly class Resolver implements ResolverBoundary { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Registry $registry

+ * Registry containing service definitions. + *

+ * @param \FireHub\Container\Internal\InstanceStore $singletons

+ * Shared singleton instance storage. + *

+ * + * @return void + */ + public function __construct ( + private Registry $registry, + private InstanceStore $singletons + ) {} + + /** + * ### Gets a registered service + * + * Resolves the specified service identifier using its registered definition and configured lifetime. + * + * Aliases are followed until their underlying service definition is reached. + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * + * @return TService Resolved service instance. + */ + public function get (string $id):object { + + return $this->resolve( + $id, + new ResolutionContext() + ); + + } + + /** + * ### Resolves a service within a scope + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param \FireHub\Container\Internal\InstanceStore $scoped

+ * Scoped instance storage. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * + * @return TService Resolved service. + */ + public function getScoped (string $id, InstanceStore $scoped):object { + + return $this->resolve( + $id, + new ResolutionContext(scoped: $scoped) + ); + + } + + /** + * ### Checks whether a service is registered + * + * Checks whether a service identifier exists without constructing or resolving its instance. + * @since 1.0.0 + * + * @uses \FireHub\Container\Registry::has() To check for service existence. + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return bool True if the service is registered. + */ + public function has (string $id):bool { + + return $this->registry->has($id); + + } + + /** + * ### Resolves a service within the specified context + * @since 1.0.0 + * + * @uses \FireHub\Container\Registry::get() To retrieve the service definition. + * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. + * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. + * @uses \FireHub\Container\Resolver::resolveService() To resolve a service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * + * @return TService Resolved service. + */ + private function resolve (string $id, ResolutionContext $context):object { + + $definition = $this->registry->get($id); + + if ($definition instanceof Alias) { + + $context->stack->enter($id); + + try { + + $instance = $this->resolve($definition->target, $context); + + if (!$instance instanceof $id) + throw new ServiceTypeMismatchException( + context: [ + 'instance' => $instance, + ] + ); + + return $instance; + + } finally { + + $context->stack->leave(); + + } + + } + + if (!$definition instanceof Service) + throw new UnsupportedDefinitionException; + + return $this->resolveService($id, $definition, $context); + + } + + /** + * ### Resolves a managed service + * @since 1.0.0 + * + * @uses \FireHub\Container\Definition\Service::lifetime() To determine the service lifetime. + * @uses \FireHub\Container\Internal\ResolutionContext::root() To get the root resolution context. + * @uses \FireHub\Container\Internal\InstanceStore::set() To store the resolved instance. + * @uses \FireHub\Container\Internal\InstanceStore::get() To retrieve a previously stored instance. + * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. + * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. + * @uses \FireHub\Container\Resolver::create() To construct a service instance. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * @param Service $definition

+ * Service definition. + *

+ * @param ResolutionContext $context

+ * Active resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the resolved service does not match the identifier. + * + * @return TService Resolved service. + */ + private function resolveService (string $id, Service $definition, ResolutionContext $context):object { + + $lifetime = $definition->lifetime(); + + if ($lifetime === Lifetime::SINGLETON) + $context = $context->root(); + + $store = match ($lifetime) { + Lifetime::SINGLETON => $this->singletons, + Lifetime::SCOPED => $context->scoped + ?? throw new ScopeRequiredException, + Lifetime::TRANSIENT => null + + }; + + if ($store !== null) { + + $instance = $store->get($id); + + if ($instance !== null) + /** @var TService $instance */ + return $instance; + + } + + $context->stack->enter($id); + + try { + + $instance = $this->create($definition, $context); + + if (!$instance instanceof $id) + throw new ServiceTypeMismatchException; + + $store?->set($id, $instance); + + return $instance; + + } finally { + + $context->stack->leave(); + + } + + } + + /** + * ### Creates a context-bound dependency resolver + * + * Creates a resolver that preserves the current resolution context across nested factory calls. + * + * This ensures that circular dependency detection and lifetime restrictions remain effective throughout + * the entire dependency resolution chain. + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * @uses \FireHub\Container\Resolver::has() To check for service existence. + * + * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active dependency resolution context. + *

+ * + * @return \FireHub\Container\Internal\ContextualResolver Context-bound resolver. + */ + private function contextual (ResolutionContext $context):ContextualResolver { + + return new ContextualResolver( + fn (string $id):object => $this->resolve($id, $context), + fn (string $id):bool => $this->has($id) + ); + + } + + /** + * ### Creates a service instance + * + * Constructs an object using the supplied service definition. + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::contextual() To create a contextual resolver. + * + * @param Service $definition

+ * Service definition. + *

+ * + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the class is not instantiable. + * + * @return object Created service instance. + */ + private function create (Service $definition, ResolutionContext $context):object { + + return match (true) { + + $definition instanceof Instance => $definition->instance, + + $definition instanceof Factory => ($definition->factory)( + $this->contextual($context) + ), + + $definition instanceof Cls => $this->construct($definition->class), + + default => throw new UnsupportedDefinitionException + + }; + + } + + /** + * ### Constructs a class-based service + * + * Creates an instance of the specified concrete class. + * + * Constructor dependency injection is not yet supported. + * @since 1.0.0 + * + * @param class-string $class

+ * Concrete implementation class. + *

+ * + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the class is not instantiable. + * + * @return object Created service instance. + */ + private function construct (string $class):object { + + try { + + $reflection = new ReflectionClass($class); + + if (!$reflection->isInstantiable()) + throw new ServiceInstantiationException( + context: [ + 'class' => $class + ] + ); + + return $reflection->newInstance(); + + } catch (ServiceInstantiationException $exception) { + + throw $exception; + + } catch (\Throwable $exception) { + + throw new ServiceInstantiationException( + previous: $exception + ); + + } + + } + +} \ No newline at end of file diff --git a/src/Sccpe.php b/src/Sccpe.php new file mode 100644 index 0000000..25b4ad0 --- /dev/null +++ b/src/Sccpe.php @@ -0,0 +1,192 @@ + + * @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 Container + */ + +namespace FireHub\Container; + +use FireHub\Core\Boundary\Resolver as ResolverBoundary; +use FireHub\Container\Internal\InstanceStore; +use FireHub\Container\Exception\ScopeClosedException; + +/** + * ### Dependency injection resolution scope + * + * Represents an isolated lifetime boundary for scoped service instances. + * + * Each scope maintains its own instance store while sharing the parent container's service resolver and singleton + * instances. + * + * Scoped services are created once per scope and reused throughout its lifetime. Singleton services remain shared + * across all scopes belonging to the same container, while transient services are created on each resolution. + * + * A scope can be explicitly closed to release its cached service instances. Once closed, the scope cannot be used + * for further service resolution. + * + * Scopes do not support nested scope creation. + * @since 1.0.0 + * + * @internal + */ +final class Scope implements ResolverBoundary { + + /** + * ### Scoped instance store + * @since 1.0.0 + */ + private readonly InstanceStore $instances; + + /** + * ### Scope closure state + * @since 1.0.0 + */ + private bool $closed = false; + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Resolver $resolver

+ * Parent container's service resolver. + *

+ * + * @return void + */ + public function __construct ( + private readonly Resolver $resolver + ) { + + $this->instances = new InstanceStore; + + } + + /** + * ### Resolves a registered service + * + * Resolves the requested service within the current scope. + * + * Scoped services are cached within this scope, singleton services are shared with the parent container, and + * transient services are created on each resolution. + * + * Resolution is prohibited after the scope has been closed. + * @since 1.0.0 + * + * @uses \FireHub\Container\Scope::assertOpen() To check if the scope is open. + * @uses \FireHub\Container\Resolver::getScoped() To resolve a scoped service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope has been closed. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * + * @return TService Resolved service. + */ + public function get (string $id):object { + + $this->assertOpen(); + + return $this->resolver->getScoped($id, $this->instances); + + } + + /** + * ### Checks whether a service is registered + * + * Determines whether the parent container contains a definition for the specified service identifier. + * + * The operation does not instantiate the service. + * + * Registration checks are prohibited after the scope has been closed. + * @since 1.0.0 + * + * @uses \FireHub\Container\Scope::assertOpen() To check if the scope is open. + * @uses \FireHub\Container\Resolver::has() To check if a service is registered. + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope has been closed. + * + * @return bool True if the service is registered, false otherwise. + */ + public function has (string $id):bool { + + $this->assertOpen(); + + return $this->resolver->has($id); + + } + + /** + * ### Closes the scope + * + * Releases all cached scoped service instances and prevents further service resolution through this scope. + * + * Closing an already closed scope has no additional effect. + * + * Closing a scope does not affect singleton instances maintained by the parent container or instances stored in + * other scopes. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\InstanceStore::clear() To clear the instance store. + * + * @return void + */ + public function close ():void { + + if ($this->closed) + return; + + $this->closed = true; + + $this->instances->clear(); + + } + + /** + * ### Checks whether the scope is closed + * @since 1.0.0 + * + * @return bool True if the scope is closed, false otherwise. + */ + public function isClosed ():bool { + + return $this->closed; + + } + + /** + * ### Ensures the scope is open + * + * Prevents service resolution and registration checks after the + * scope has been closed. + * @since 1.0.0 + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * + * @return void + */ + private function assertOpen ():void { + + if ($this->closed) + throw new ScopeClosedException; + + } + +} \ No newline at end of file From 77d355d43e0dee6d9a43f31e85c6085073829f55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 09:49:00 +0200 Subject: [PATCH 05/26] Introduced `ScopeState` to manage dependency injection scope lifecycle. Enhanced scope mechanisms with precise state tracking, including resolution entry/exit, open/close assertions, and execution guarding. Refactored `Scope` class to improve lifecycle management and enforce strict control over state transitions. Added `run` method for executing callbacks within a scope and handling automatic cleanup. Introduced `ScopeActiveException` for concurrent execution errors. Updated `Resolver` and internal components (`ResolutionContext`, `InstanceStore`) to support new lifecycle operations. Improved exception messages for better diagnostics. --- src/Container.php | 3 +- src/Exception/ScopeActiveException.php | 26 ++++ src/Exception/ScopeClosedException.php | 7 +- src/Internal/ResolutionContext.php | 11 +- src/Internal/ScopeState.php | 167 +++++++++++++++++++++++++ src/Resolver.php | 58 +++++++-- src/{Sccpe.php => Scope.php} | 106 +++++++++------- 7 files changed, 321 insertions(+), 57 deletions(-) create mode 100644 src/Exception/ScopeActiveException.php create mode 100644 src/Internal/ScopeState.php rename src/{Sccpe.php => Scope.php} (67%) diff --git a/src/Container.php b/src/Container.php index e07d542..8c7899b 100644 --- a/src/Container.php +++ b/src/Container.php @@ -383,7 +383,8 @@ public function alias (string $id, string $target):self { *

* * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. - * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the resolved service is not compatible + * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. * diff --git a/src/Exception/ScopeActiveException.php b/src/Exception/ScopeActiveException.php new file mode 100644 index 0000000..71e7467 --- /dev/null +++ b/src/Exception/ScopeActiveException.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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Thrown when closing a scope during active service resolution + * @since 1.0.0 + */ +final class ScopeActiveException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'Cannot close a dependency injection scope while service resolution is active.'; + +} \ No newline at end of file diff --git a/src/Exception/ScopeClosedException.php b/src/Exception/ScopeClosedException.php index cd47719..95482c7 100644 --- a/src/Exception/ScopeClosedException.php +++ b/src/Exception/ScopeClosedException.php @@ -16,11 +16,14 @@ use FireHub\Core\Exception\RuntimeException; /** - * ### Thrown when a scoped service is registered after the scope has been closed + * ### Thrown when accessing a closed dependency injection scope + * + * Indicates that an operation was attempted on a scope that has already been closed and is no longer available for + * service resolution. * @since 1.0.0 */ final class ScopeClosedException extends RuntimeException { - protected const string DEFAULT_MESSAGE = 'Cannot register services in a closed scope'; + protected const string DEFAULT_MESSAGE = 'Cannot access a closed dependency injection scope.'; } \ No newline at end of file diff --git a/src/Internal/ResolutionContext.php b/src/Internal/ResolutionContext.php index d863993..e11d30f 100644 --- a/src/Internal/ResolutionContext.php +++ b/src/Internal/ResolutionContext.php @@ -38,10 +38,16 @@ * @param null|\FireHub\Container\Internal\InstanceStore $scoped

* Active scoped instance storage, or null for root resolution. *

+ * @param null|\FireHub\Container\Internal\ScopeState $scope

+ * Associated scope lifecycle state. + *

+ * + * @return void */ public function __construct ( public ResolutionStack $stack = new ResolutionStack, - public ?InstanceStore $scoped = null + public ?InstanceStore $scoped = null, + public ?ScopeState $scope = null ) {} /** @@ -56,7 +62,8 @@ public function __construct ( public function root ():self { return new self( - stack: $this->stack + stack: $this->stack, + scope: $this->scope ); } diff --git a/src/Internal/ScopeState.php b/src/Internal/ScopeState.php new file mode 100644 index 0000000..04380a5 --- /dev/null +++ b/src/Internal/ScopeState.php @@ -0,0 +1,167 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\ { + ScopeActiveException, ScopeClosedException +}; + +/** + * ### Manages dependency injection scope lifecycle + * + * Tracks whether a scope is closed and prevents its closure while service resolution is active. + * @since 1.0.0 + * + * @internal + */ +final class ScopeState { + + /** + * ### Scope closure state + * @since 1.0.0 + */ + private bool $closed = false; + + /** + * ### Active resolution count + * @since 1.0.0 + */ + private int $active = 0; + + /** + * ### Scope execution state + * @since 1.0.0 + */ + private bool $running = false; + + /** + * ### Begins a resolution operation + * + * Prevents resolution through a closed scope and increments + * the number of active resolution operations. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ScopeState::assertOpen() To check if the scope is open. + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * + * @return void + */ + public function enter ():void { + + $this->assertOpen(); + + $this->active++; + + } + + /** + * ### Completes a resolution operation + * @since 1.0.0 + * + * @return void + */ + public function leave ():void { + + if ($this->active > 0) + $this->active--; + + } + + /** + * ### Ensures the scope is open + * @since 1.0.0 + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * + * @return void + */ + public function assertOpen ():void { + + if ($this->closed) + throw new ScopeClosedException; + + } + + /** + * ### Closes the scope + * + * Prevents closure while service resolution is active. + * @since 1.0.0 + * + * @throws \FireHub\Container\Exception\ScopeActiveException If service resolution is active. + * + * @return void + */ + public function close ():void { + + if ($this->closed) + return; + + if ($this->active > 0) + throw new ScopeActiveException; + + $this->closed = true; + + } + + /** + * ### Checks whether the scope is closed + * @since 1.0.0 + * + * @return bool True if closed, false otherwise. + */ + public function isClosed ():bool { + + return $this->closed; + + } + + /** + * ### Begins scope execution + * + * Prevents concurrent or nested execution of the same scope. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ScopeState::assertOpen() To check if the scope is open. + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @throws \FireHub\Container\Exception\ScopeActiveException If scope execution is already in progress. + * + * @return void + */ + public function beginRun ():void { + + $this->assertOpen(); + + if ($this->running) + throw new ScopeActiveException; + + $this->running = true; + + } + + /** + * ### Ends scope execution + * @since 1.0.0 + * + * @return void + */ + public function endRun ():void { + + $this->running = false; + + } + +} \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index b553e1c..1e83c78 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,7 +19,7 @@ Alias, Cls, Factory, Instance, Service }; use FireHub\Container\Internal\ { - ContextualResolver, InstanceStore, ResolutionContext + ContextualResolver, InstanceStore, ResolutionContext, ScopeState }; use FireHub\Container\Exception\ { ScopeRequiredException, ServiceInstantiationException, ServiceTypeMismatchException, UnsupportedDefinitionException @@ -99,6 +99,8 @@ public function get (string $id):object { * @since 1.0.0 * * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * @uses \FireHub\Container\Internal\ScopeState::enter() To mark the scope as being currently being resolved. + * @uses \FireHub\Container\Internal\ScopeState::leave() To remove the scope from the resolution stack. * * @template TService of object * @@ -108,20 +110,37 @@ public function get (string $id):object { * @param \FireHub\Container\Internal\InstanceStore $scoped

* Scoped instance storage. *

+ * @param \FireHub\Container\Internal\ScopeState $scope

+ * Scope lifecycle state. + *

* * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. * * @return TService Resolved service. */ - public function getScoped (string $id, InstanceStore $scoped):object { + public function getScoped (string $id, InstanceStore $scoped, ScopeState $scope):object { - return $this->resolve( - $id, - new ResolutionContext(scoped: $scoped) - ); + $scope->enter(); + + try { + + return $this->resolve( + $id, + new ResolutionContext( + scoped: $scoped, + scope: $scope + ) + ); + + } finally { + + $scope->leave(); + + } } @@ -292,6 +311,9 @@ private function resolveService (string $id, Service $definition, ResolutionCont * * @uses \FireHub\Container\Resolver::resolve() To resolve a service. * @uses \FireHub\Container\Resolver::has() To check for service existence. + * @uses \FireHub\Container\Internal\ScopeState::assertOpen() To check if the current scope is open. + * @uses \FireHub\Container\Internal\ScopeState::enter() To mark the current scope as being currently being resolved. + * @uses \FireHub\Container\Internal\ScopeState::leave() To remove the current scope from the resolution stack. * * @param \FireHub\Container\Internal\ResolutionContext $context

* Active dependency resolution context. @@ -302,8 +324,28 @@ private function resolveService (string $id, Service $definition, ResolutionCont private function contextual (ResolutionContext $context):ContextualResolver { return new ContextualResolver( - fn (string $id):object => $this->resolve($id, $context), - fn (string $id):bool => $this->has($id) + function (string $id) use ($context):object { + + $context->scope?->enter(); + + try { + + return $this->resolve($id, $context); + + } finally { + + $context->scope?->leave(); + + } + + }, + function (string $id) use ($context):bool { + + $context->scope?->assertOpen(); + + return $this->has($id); + + } ); } diff --git a/src/Sccpe.php b/src/Scope.php similarity index 67% rename from src/Sccpe.php rename to src/Scope.php index 25b4ad0..89ad6ed 100644 --- a/src/Sccpe.php +++ b/src/Scope.php @@ -14,8 +14,10 @@ namespace FireHub\Container; use FireHub\Core\Boundary\Resolver as ResolverBoundary; -use FireHub\Container\Internal\InstanceStore; -use FireHub\Container\Exception\ScopeClosedException; +use FireHub\Container\Internal\ { + InstanceStore, ScopeState +}; +use Closure; /** * ### Dependency injection resolution scope @@ -33,22 +35,20 @@ * * Scopes do not support nested scope creation. * @since 1.0.0 - * - * @internal */ -final class Scope implements ResolverBoundary { +final readonly class Scope implements ResolverBoundary { /** * ### Scoped instance store * @since 1.0.0 */ - private readonly InstanceStore $instances; + private InstanceStore $instances; /** - * ### Scope closure state + * ### Scope lifecycle state * @since 1.0.0 */ - private bool $closed = false; + private ScopeState $state; /** * ### Constructor @@ -61,11 +61,13 @@ final class Scope implements ResolverBoundary { * @return void */ public function __construct ( - private readonly Resolver $resolver + private Resolver $resolver ) { $this->instances = new InstanceStore; + $this->state = new ScopeState; + } /** @@ -79,7 +81,6 @@ public function __construct ( * Resolution is prohibited after the scope has been closed. * @since 1.0.0 * - * @uses \FireHub\Container\Scope::assertOpen() To check if the scope is open. * @uses \FireHub\Container\Resolver::getScoped() To resolve a scoped service. * * @template TService of object @@ -90,17 +91,21 @@ public function __construct ( * * @throws \FireHub\Container\Exception\ScopeClosedException If the scope has been closed. * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. - * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the resolved service is not compatible + * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. * * @return TService Resolved service. */ public function get (string $id):object { - $this->assertOpen(); - - return $this->resolver->getScoped($id, $this->instances); + return $this->resolver->getScoped( + $id, + $this->instances, + $this->state + ); } @@ -114,7 +119,7 @@ public function get (string $id):object { * Registration checks are prohibited after the scope has been closed. * @since 1.0.0 * - * @uses \FireHub\Container\Scope::assertOpen() To check if the scope is open. + * @uses \FireHub\Container\Internal\ScopeState::assertOpen() To check if the scope is open. * @uses \FireHub\Container\Resolver::has() To check if a service is registered. * * @param class-string $id

@@ -127,65 +132,78 @@ public function get (string $id):object { */ public function has (string $id):bool { - $this->assertOpen(); + $this->state->assertOpen(); return $this->resolver->has($id); } /** - * ### Closes the scope + * ### Executes a callback within the scope * - * Releases all cached scoped service instances and prevents further service resolution through this scope. + * Executes the provided callback using this scope as its dependency resolver. * - * Closing an already closed scope has no additional effect. + * The scope is automatically closed after callback execution, including when the callback throws an exception. * - * Closing a scope does not affect singleton instances maintained by the parent container or instances stored in - * other scopes. + * Nested execution of the same scope is not permitted. * @since 1.0.0 * - * @uses \FireHub\Container\Internal\InstanceStore::clear() To clear the instance store. + * @uses \FireHub\Container\Internal\ScopeState::beginRun() To begin scope execution. + * @uses \FireHub\Container\Internal\ScopeState::endRun() To end scope execution. + * @uses \FireHub\Container\Scope::close() To close the scope. * - * @return void + * @template TResult + * + * @param Closure(\FireHub\Core\Boundary\Resolver):TResult $callback

+ * Callback to execute within the scope. + *

+ * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @throws \FireHub\Container\Exception\ScopeActiveException If the scope is already executing. + * + * @return TResult Callback result. */ - public function close ():void { + public function run (Closure $callback):mixed { - if ($this->closed) - return; + $this->state->beginRun(); - $this->closed = true; + try { - $this->instances->clear(); + return $callback($this); - } + } finally { - /** - * ### Checks whether the scope is closed - * @since 1.0.0 - * - * @return bool True if the scope is closed, false otherwise. - */ - public function isClosed ():bool { + $this->state->endRun(); + + $this->close(); - return $this->closed; + } } /** - * ### Ensures the scope is open + * ### Closes the scope + * + * Releases all cached scoped service instances and prevents further service resolution through this scope. + * + * Closing an already closed scope has no additional effect. * - * Prevents service resolution and registration checks after the - * scope has been closed. + * Closing a scope does not affect singleton instances maintained by the parent container or instances stored in + * other scopes. * @since 1.0.0 * - * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @uses \FireHub\Container\Internal\ScopeState::close() To close the scope. + * @uses \FireHub\Container\Internal\InstanceStore::clear() To clear the instance store. + * + * @throws \FireHub\Container\Exception\ScopeActiveException If service resolution is active. * * @return void */ - private function assertOpen ():void { + public function close ():void { - if ($this->closed) - throw new ScopeClosedException; + $this->state->close(); + + $this->instances->clear(); } From b73c5058c680ea3d9e018620b5122a47d94e7bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 09:58:51 +0200 Subject: [PATCH 06/26] Refactored `Scope` lifecycle to enhance state management and improve error handling. - Added `isClosed` method to determine the state of a scope. - Improved exception messages in `ScopeActiveException` for clarity. - Updated `ScopeState` to incorporate a `running` check for better execution guarding. - Fixed incorrect exception descriptions in `Resolver` docblocks to ensure proper documentation of errors. --- src/Exception/ScopeActiveException.php | 7 +++++-- src/Internal/ScopeState.php | 2 +- src/Resolver.php | 8 +++++--- src/Scope.php | 15 ++++++++++++++- 4 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/Exception/ScopeActiveException.php b/src/Exception/ScopeActiveException.php index 71e7467..9ed842c 100644 --- a/src/Exception/ScopeActiveException.php +++ b/src/Exception/ScopeActiveException.php @@ -16,11 +16,14 @@ use FireHub\Core\Exception\RuntimeException; /** - * ### Thrown when closing a scope during active service resolution + * ### Thrown when a scope operation conflicts with active execution + * + * Indicates that the scope cannot be closed or executed because another + * resolution or execution operation is currently active. * @since 1.0.0 */ final class ScopeActiveException extends RuntimeException { - protected const string DEFAULT_MESSAGE = 'Cannot close a dependency injection scope while service resolution is active.'; + protected const string DEFAULT_MESSAGE = 'The dependency injection scope is currently active.'; } \ No newline at end of file diff --git a/src/Internal/ScopeState.php b/src/Internal/ScopeState.php index 04380a5..621ef95 100644 --- a/src/Internal/ScopeState.php +++ b/src/Internal/ScopeState.php @@ -109,7 +109,7 @@ public function close ():void { if ($this->closed) return; - if ($this->active > 0) + if ($this->running || $this->active > 0) throw new ScopeActiveException; $this->closed = true; diff --git a/src/Resolver.php b/src/Resolver.php index 1e83c78..b91e7f7 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -79,7 +79,8 @@ public function __construct ( *

* * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. - * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the resolved service is not compatible + * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. * @@ -115,7 +116,7 @@ public function get (string $id):object { *

* * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. - * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the resolved service is not compatible with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. @@ -183,7 +184,8 @@ public function has (string $id):bool { *

* * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. - * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the resolved service is not compatible + * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. * diff --git a/src/Scope.php b/src/Scope.php index 89ad6ed..fa429ae 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -95,7 +95,6 @@ public function __construct ( * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. - * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. * * @return TService Resolved service. */ @@ -181,6 +180,20 @@ public function run (Closure $callback):mixed { } + /** + * ### Checks whether the scope is closed + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ScopeState::isClosed() To check the scope lifecycle state. + * + * @return bool True if the scope is closed, false otherwise. + */ + public function isClosed ():bool { + + return $this->state->isClosed(); + + } + /** * ### Closes the scope * From 7b73d2a285077ea5798b5975a0becf6ab0b80977 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 10:17:37 +0200 Subject: [PATCH 07/26] Implemented constructor autowiring for the `Resolver` service logic. - Added `Autowirer` class to automatically resolve and construct registered services using constructor dependency injection. - Integrated `Autowirer` into `Resolver` to support autowiring of class-based service definitions (`Cls`). - Replaced manual instantiation in `Resolver::construct` with `Resolver::autowire` to maintain current resolution context. - Enhanced exception handling, including improvements to docblocks for better error documentation in `Resolver`. --- src/Internal/Autowirer.php | 151 +++++++++++++++++++++++++++++++++++++ src/Resolver.php | 67 ++++++++-------- 2 files changed, 183 insertions(+), 35 deletions(-) create mode 100644 src/Internal/Autowirer.php diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php new file mode 100644 index 0000000..38b2edc --- /dev/null +++ b/src/Internal/Autowirer.php @@ -0,0 +1,151 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\ServiceInstantiationException; +use Closure, ReflectionClass, ReflectionException, ReflectionNamedType, ReflectionParameter; + +/** + * ### Automatically constructs registered services + * + * Resolves constructor dependencies using registered service definitions. + * + * Only class and interface dependencies are supported. Scalar, union, and intersection types require explicit + * factory definitions. + * @since 1.0.0 + * + * @internal + */ +final readonly class Autowirer { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param Closure(class-string):object $resolve

+ * Context-aware service resolver. + *

+ * @param Closure(class-string):bool $has

+ * Service registration checker. + *

+ */ + public function __construct ( + private Closure $resolve, + private Closure $has + ) {} + + /** + * ### Constructs a service with resolved dependencies + * + * Inspects the constructor and resolves registered object dependencies. + * + * Optional parameters with unresolved dependencies use their default values when available. + * @since 1.0.0 + * + * @param class-string $class

+ * Concrete service class. + *

+ * + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot + * be resolved. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. + * @throws ReflectionException If the class cannot be reflected. + * + * @return object Constructed service instance. + */ + public function construct (string $class):object { + + try { + + $reflection = new ReflectionClass($class); + + } catch (ReflectionException $exception) { // @phpstan-ignore catch.neverThrown + + throw new ServiceInstantiationException( + previous: $exception + ); + + } + + if (!$reflection->isInstantiable()) + throw new ServiceInstantiationException( + context: ['class' => $class] + ); + + $constructor = $reflection->getConstructor(); + + if ($constructor === null) + return $reflection->newInstance(); + + $arguments = []; + + foreach ($constructor->getParameters() as $parameter) + $arguments[] = $this->resolveParameter($parameter); + + return $reflection->newInstanceArgs($arguments); + + } + + /** + * ### Resolves a constructor parameter + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot + * be resolved. + * + * @return mixed Resolved argument. + */ + private function resolveParameter (ReflectionParameter $parameter):mixed { + + if ($parameter->isVariadic()) + throw new ServiceInstantiationException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName() + ] + ); + + $type = $parameter->getType(); + + if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { + + /** @var class-string $id */ + $id = match ($type->getName()) { + 'self' => $parameter->getDeclaringClass()?->getName(), + 'parent' => $parameter->getDeclaringClass()?->getParentClass()?->getName(), // @phpstan-ignore method.nonObject + default => $type->getName() + }; + + if ($id !== null && ($this->has)($id)) + return ($this->resolve)($id); + + } + + if ($parameter->isDefaultValueAvailable()) + return $parameter->getDefaultValue(); + + throw new ServiceInstantiationException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName() + ] + ); + + } + +} \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index b91e7f7..bde4c97 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,12 +19,12 @@ Alias, Cls, Factory, Instance, Service }; use FireHub\Container\Internal\ { - ContextualResolver, InstanceStore, ResolutionContext, ScopeState + Autowirer, ContextualResolver, InstanceStore, ResolutionContext, ScopeState }; use FireHub\Container\Exception\ { - ScopeRequiredException, ServiceInstantiationException, ServiceTypeMismatchException, UnsupportedDefinitionException + ScopeRequiredException, ServiceTypeMismatchException, UnsupportedDefinitionException }; -use ReflectionClass; +use ReflectionException; /** * ### Resolves registered dependency injection services @@ -358,6 +358,7 @@ function (string $id) use ($context):bool { * Constructs an object using the supplied service definition. * @since 1.0.0 * + * @uses \FireHub\Container\Resolver::autowire() To construct an instance using autowiring. * @uses \FireHub\Container\Resolver::contextual() To create a contextual resolver. * * @param Service $definition

@@ -365,7 +366,10 @@ function (string $id) use ($context):bool { *

* * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. - * @throws \FireHub\Container\Exception\ServiceInstantiationException If the class is not instantiable. + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot + * be resolved. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. + * @throws ReflectionException If the class cannot be reflected. * * @return object Created service instance. */ @@ -379,7 +383,10 @@ private function create (Service $definition, ResolutionContext $context):object $this->contextual($context) ), - $definition instanceof Cls => $this->construct($definition->class), + $definition instanceof Cls => $this->autowire( + $definition->class, + $context + ), default => throw new UnsupportedDefinitionException @@ -388,47 +395,37 @@ private function create (Service $definition, ResolutionContext $context):object } /** - * ### Constructs a class-based service - * - * Creates an instance of the specified concrete class. + * ### Constructs a service using constructor autowiring * - * Constructor dependency injection is not yet supported. + * Automatically resolves registered constructor dependencies while preserving the current resolution context. * @since 1.0.0 * + * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * @uses \FireHub\Container\Resolver::has() To check for service existence. + * @uses \FireHub\Container\Internal\Autowirer::construct() To construct an instance using autowiring. + * * @param class-string $class

* Concrete implementation class. *

+ * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active resolution context. + *

* - * @throws \FireHub\Container\Exception\ServiceInstantiationException If the class is not instantiable. + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot + * be resolved. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. + * @throws ReflectionException If the class cannot be reflected. * - * @return object Created service instance. + * @return object Constructed service instance. */ - private function construct (string $class):object { - - try { - - $reflection = new ReflectionClass($class); - - if (!$reflection->isInstantiable()) - throw new ServiceInstantiationException( - context: [ - 'class' => $class - ] - ); - - return $reflection->newInstance(); - - } catch (ServiceInstantiationException $exception) { + private function autowire (string $class, ResolutionContext $context):object { - throw $exception; - - } catch (\Throwable $exception) { - - throw new ServiceInstantiationException( - previous: $exception - ); + $autowirer = new Autowirer( + fn (string $id):object => $this->resolve($id, $context), + fn (string $id):bool => $this->has($id) + ); - } + return $autowirer->construct($class); } From 3b444af15a817ee026c324d1a4a40837eb5a3375 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 10:41:45 +0200 Subject: [PATCH 08/26] Added implicit service resolution to `Resolver` with constructor-based autowiring. - Introduced `resolveImplicit` method in `Resolver` to handle unregistered concrete class instantiation using implicit construction logic. - Enhanced `Autowirer` module with `canResolve` and `classId` helper methods to streamline dependency resolution and class type identification. - Updated `resolve` logic to route unregistered service requests through `resolveImplicit`, supporting transient lifetimes. - Improved exception handling and updated docblocks for `Resolver` and `Autowirer` to document implicit resolution workflows. - Integrated `FireHub\Runtime\ObjectModel\Inspection` for class validation in service resolution mechanics. --- src/Internal/Autowirer.php | 75 +++++++++++++++++++++++++++++---- src/Resolver.php | 86 +++++++++++++++++++++++++++++++++++++- 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index 38b2edc..c0d44fa 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -14,6 +14,7 @@ namespace FireHub\Container\Internal; use FireHub\Container\Exception\ServiceInstantiationException; +use FireHub\Runtime; use Closure, ReflectionClass, ReflectionException, ReflectionNamedType, ReflectionParameter; /** @@ -97,10 +98,75 @@ public function construct (string $class):object { } + /** + * ### Checks whether a dependency can be resolved + * + * Determines whether the dependency has an explicit registration or represents an implicitly constructible + * concrete class. + * + * This check does not instantiate the dependency. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. + * + * @param class-string $id

+ * Dependency identifier. + *

+ * + * @return bool True if resolution can be attempted. + */ + private function canResolve (string $id):bool { + + if (($this->has)($id)) + return true; + + if (!Runtime\ObjectModel\Inspection::isClass($id)) + return false; + + return new ReflectionClass($id)->isInstantiable(); + + } + + /** + * ### Resolves a parameter class identifier + * + * Resolves the declared class or interface type of the constructor parameter. + * + * Relative class types are resolved against the class declaring the constructor. Built-in types are not + * considered service identifiers. + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionNamedType $type

+ * Declared parameter type. + *

+ * + * @return null|class-string Resolved service identifier, or null for built-in types. + */ + private function classId (ReflectionParameter $parameter, ReflectionNamedType $type):?string { + + if ($type->isBuiltin()) + return null; + + /** @var null|class-string */ + return match ($type->getName()) { + 'self' => $parameter->getDeclaringClass()?->getName(), + 'parent' => $parameter->getDeclaringClass()?->getParentClass()?->getName(), // @phpstan-ignore method.nonObject + default => $type->getName() + }; + + } + /** * ### Resolves a constructor parameter * @since 1.0.0 * + * @uses \FireHub\Container\Internal\Autowirer::canResolve() Checks if a dependency can be resolved. + * @uses \FireHub\Container\Internal\Autowirer::resolve() Resolves a dependency. + * @uses \FireHub\Container\Internal\Autowirer::classId() Resolves a parameter class identifier. + * * @param ReflectionParameter $parameter

* Constructor parameter. *

@@ -124,14 +190,9 @@ private function resolveParameter (ReflectionParameter $parameter):mixed { if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { - /** @var class-string $id */ - $id = match ($type->getName()) { - 'self' => $parameter->getDeclaringClass()?->getName(), - 'parent' => $parameter->getDeclaringClass()?->getParentClass()?->getName(), // @phpstan-ignore method.nonObject - default => $type->getName() - }; + $id = $this->classId($parameter, $type); - if ($id !== null && ($this->has)($id)) + if ($id !== null && $this->canResolve($id)) return ($this->resolve)($id); } diff --git a/src/Resolver.php b/src/Resolver.php index bde4c97..04f36f2 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -22,9 +22,10 @@ Autowirer, ContextualResolver, InstanceStore, ResolutionContext, ScopeState }; use FireHub\Container\Exception\ { - ScopeRequiredException, ServiceTypeMismatchException, UnsupportedDefinitionException + ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException }; -use ReflectionException; +use FireHub\Runtime; +use ReflectionClass, ReflectionException; /** * ### Resolves registered dependency injection services @@ -169,10 +170,12 @@ public function has (string $id):bool { * ### Resolves a service within the specified context * @since 1.0.0 * + * @uses \FireHub\Container\Registry::has() To check for service existence. * @uses \FireHub\Container\Registry::get() To retrieve the service definition. * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. * @uses \FireHub\Container\Resolver::resolveService() To resolve a service. + * @uses \FireHub\Container\Resolver::resolveImplicit() To resolve an implicitly constructed service. * * @template TService of object * @@ -188,11 +191,16 @@ public function has (string $id):bool { * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the identifier is not an instantiable class. + * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. * * @return TService Resolved service. */ private function resolve (string $id, ResolutionContext $context):object { + if (!$this->registry->has($id)) + return $this->resolveImplicit($id, $context); + $definition = $this->registry->get($id); if ($definition instanceof Alias) { @@ -302,6 +310,80 @@ private function resolveService (string $id, Service $definition, ResolutionCont } + /** + * ### Resolves an implicitly constructed service + * + * Automatically constructs an unregistered concrete class using constructor dependency injection. + * + * Implicit services are never cached or registered and always use transient lifetime semantics. + * + * Abstract classes and interfaces cannot be implicitly constructed. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. + * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. + * @uses \FireHub\Container\Resolver::autowire() To construct an instance using autowiring. + * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. + * + * @template TService of object + * + * @param class-string $id

+ * Unregistered concrete class identifier. + *

+ * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the identifier is not an instantiable class. + * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the constructed instance does not match + * the identifier. + * + * @return TService Constructed service. + */ + private function resolveImplicit (string $id, ResolutionContext $context):object { + + if (!Runtime\ObjectModel\Inspection::isClass($id)) + throw new ServiceNotFoundException; + + try { + + $reflection = new ReflectionClass($id); + + } catch (ReflectionException $exception) { // @phpstan-ignore catch.neverThrown + + throw new ServiceNotFoundException( + previous: $exception + ); + + } + + if (!$reflection->isInstantiable()) + throw new ServiceNotFoundException; + + $context->stack->enter($id); + + try { + + $instance = $this->autowire($id, $context); + + if (!$instance instanceof $id) + throw new ServiceTypeMismatchException( + context: [ + 'instance' => $instance + ] + ); + + return $instance; + + } finally { + + $context->stack->leave(); + + } + + } + /** * ### Creates a context-bound dependency resolver * From 692cef122dfda7f7465911e9a6205c1f33a0ee91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 13:31:13 +0200 Subject: [PATCH 09/26] Enhanced autowiring with union and intersection type dependency resolution. - Added support for resolving union (`ReflectionUnionType`) and intersection (`ReflectionIntersectionType`) type dependencies in the `Autowirer`. - Introduced `unionId` and `intersectionId` methods to handle complex type-based resolution workflows. - Added `AmbiguousDependencyException` to handle cases with multiple eligible service candidates. - Updated `Registry` with an `implementations` method to provide known service implementation types for intersection resolution. - Improved exception handling and updated docblocks to clarify dependency resolution rules. - Extended `resolveParameter` in `Autowirer` to support variadic parameters, complex types, and stricter resolution logic. --- .../AmbiguousDependencyException.php | 29 +++ src/Internal/Autowirer.php | 207 ++++++++++++++++-- src/Registry.php | 31 +++ src/Resolver.php | 9 +- 4 files changed, 250 insertions(+), 26 deletions(-) create mode 100644 src/Exception/AmbiguousDependencyException.php diff --git a/src/Exception/AmbiguousDependencyException.php b/src/Exception/AmbiguousDependencyException.php new file mode 100644 index 0000000..24a9093 --- /dev/null +++ b/src/Exception/AmbiguousDependencyException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\RuntimeException; + +/** + * ### Thrown when multiple services match a dependency + * + * Indicates that automatic dependency resolution cannot select a unique + * candidate from multiple eligible service identifiers. + * @since 1.0.0 + */ +final class AmbiguousDependencyException extends RuntimeException { + + protected const string DEFAULT_MESSAGE = 'Multiple services match the requested dependency.'; + +} \ No newline at end of file diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index c0d44fa..ece998f 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -13,9 +13,12 @@ namespace FireHub\Container\Internal; -use FireHub\Container\Exception\ServiceInstantiationException; +use FireHub\Container\Exception\ { + AmbiguousDependencyException, ServiceInstantiationException +}; use FireHub\Runtime; -use Closure, ReflectionClass, ReflectionException, ReflectionNamedType, ReflectionParameter; +use Closure, ReflectionClass, ReflectionException, ReflectionNamedType, ReflectionParameter, + ReflectionIntersectionType, ReflectionUnionType; /** * ### Automatically constructs registered services @@ -40,18 +43,24 @@ * @param Closure(class-string):bool $has

* Service registration checker. *

+ * @param Closure():(iterable) $candidates

+ * Service candidates provider. + *

*/ public function __construct ( private Closure $resolve, - private Closure $has + private Closure $has, + private Closure $candidates ) {} /** * ### Constructs a service with resolved dependencies * - * Inspects the constructor and resolves registered object dependencies. + * Inspects the constructor and resolves registered or implicitly constructible object dependencies. * - * Optional parameters with unresolved dependencies use their default values when available. + * Parameters with default values use their defaults when no eligible dependency is available. + * Nullable parameters without default values remain required. + * Variadic parameters are left empty unless explicitly supplied through a future binding mechanism. * @since 1.0.0 * * @param class-string $class

@@ -61,7 +70,7 @@ public function __construct ( * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot * be resolved. * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. - * @throws ReflectionException If the class cannot be reflected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return object Constructed service instance. */ @@ -91,9 +100,15 @@ public function construct (string $class):object { $arguments = []; - foreach ($constructor->getParameters() as $parameter) + foreach ($constructor->getParameters() as $parameter) { + + if ($parameter->isVariadic()) + continue; + $arguments[] = $this->resolveParameter($parameter); + } + return $reflection->newInstanceArgs($arguments); } @@ -159,6 +174,149 @@ private function classId (ReflectionParameter $parameter, ReflectionNamedType $t } + /** + * ### Resolves a union dependency identifier + * + * Selects a unique dependency from a union of class or interface types. + * + * Explicitly registered services take precedence over implicitly constructible concrete classes. + * + * Multiple candidates at the same priority level are considered ambiguous. + * + * This method does not instantiate any dependencies. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the union contains multiple candidates. + * @uses \FireHub\Runtime\Arr\Access::keys() To get the union member identifiers. + * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first union member identifier. + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionUnionType $type

+ * Declared union type. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * + * @return null|class-string Selected service identifier, or null if no candidate is available. + */ + private function unionId (ReflectionParameter $parameter, ReflectionUnionType $type):?string { + + $registered = []; + $implicit = []; + + foreach ($type->getTypes() as $member) { + + if (!$member instanceof ReflectionNamedType) + continue; + + $id = $this->classId($parameter, $member); + + if ($id === null) + continue; + + if (($this->has)($id)) { + + $registered[$id] = true; + + continue; + + } + + if ($this->canResolve($id)) + $implicit[$id] = true; + + } + + $candidates = $registered !== [] ? $registered : $implicit; + + if (Runtime\Arr\Inspection::count($candidates) > 1) + throw new AmbiguousDependencyException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName(), + 'type' => (string)$type, + 'candidates' => Runtime\Arr\Access::keys($candidates) + ] + ); + + return Runtime\Arr\Access::firstKey($candidates); + + } + + /** + * ### Resolves an intersection dependency identifier + * + * Selects a registered service whose implementation satisfies every declared type in the intersection. + * + * Resolution succeeds only when exactly one eligible candidate exists. + * + * This method does not instantiate any dependencies. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\Autowirer::classId() Resolves a parameter class identifier. + * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the intersection contains multiple candidates. + * @uses \FireHub\Runtime\Arr\Access::keys() To get the intersection member identifiers. + * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first intersection member identifier. + * @uses \FireHub\Runtime\ObjectModel\Relation::instanceOf() To check if a service implements a contract. + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionIntersectionType $type

+ * Declared intersection type. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates satisfy the intersection. + * + * @return null|string Selected service identifier, or null if none matches. + */ + private function intersectionId (ReflectionParameter $parameter, ReflectionIntersectionType $type):?string { + + $matches = []; + + foreach (($this->candidates)() as $id => $implementation) { + + $matchesAll = true; + + foreach ($type->getTypes() as $member) { + + $contract = $this->classId($parameter, $member); + + if ($contract === null + || !Runtime\ObjectModel\Relation::instanceOf( + $implementation, + $contract + )) { + + $matchesAll = false; + + break; + + } + + } + + if ($matchesAll) + $matches[$id] = true; + + } + + if (Runtime\Arr\Inspection::count($matches) > 1) + throw new AmbiguousDependencyException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName(), + 'type' => (string)$type, + 'candidates' => Runtime\Arr\Access::keys($matches) + ] + ); + + return Runtime\Arr\Access::firstKey($matches); + + } + /** * ### Resolves a constructor parameter * @since 1.0.0 @@ -166,6 +324,7 @@ private function classId (ReflectionParameter $parameter, ReflectionNamedType $t * @uses \FireHub\Container\Internal\Autowirer::canResolve() Checks if a dependency can be resolved. * @uses \FireHub\Container\Internal\Autowirer::resolve() Resolves a dependency. * @uses \FireHub\Container\Internal\Autowirer::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\Autowirer::unionId() Resolves a union dependency identifier. * * @param ReflectionParameter $parameter

* Constructor parameter. @@ -173,29 +332,26 @@ private function classId (ReflectionParameter $parameter, ReflectionNamedType $t * * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot * be resolved. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return mixed Resolved argument. */ private function resolveParameter (ReflectionParameter $parameter):mixed { - if ($parameter->isVariadic()) - throw new ServiceInstantiationException( - context: [ - 'class' => $parameter->getDeclaringClass()?->getName(), - 'parameter' => $parameter->getName() - ] - ); - $type = $parameter->getType(); - if ($type instanceof ReflectionNamedType && !$type->isBuiltin()) { - - $id = $this->classId($parameter, $type); - - if ($id !== null && $this->canResolve($id)) - return ($this->resolve)($id); + $id = match (true) { + $type instanceof ReflectionNamedType + => $this->classId($parameter, $type), + $type instanceof ReflectionUnionType + => $this->unionId($parameter, $type), + $type instanceof ReflectionIntersectionType + => $this->intersectionId($parameter, $type), + default => null + }; - } + if ($id !== null && $this->canResolve($id)) + return ($this->resolve)($id); if ($parameter->isDefaultValueAvailable()) return $parameter->getDefaultValue(); @@ -203,7 +359,12 @@ private function resolveParameter (ReflectionParameter $parameter):mixed { throw new ServiceInstantiationException( context: [ 'class' => $parameter->getDeclaringClass()?->getName(), - 'parameter' => $parameter->getName() + 'parameter' => $parameter->getName(), + 'position' => $parameter->getPosition(), + 'type' => $type?->__toString(), + 'nullable' => $type?->allowsNull(), + 'optional' => $parameter->isOptional(), + 'has_default' => $parameter->isDefaultValueAvailable() ] ); diff --git a/src/Registry.php b/src/Registry.php index 3a154a9..a32ed01 100644 --- a/src/Registry.php +++ b/src/Registry.php @@ -13,6 +13,9 @@ namespace FireHub\Container; +use FireHub\Container\Definition\ { + Cls, Instance +}; use FireHub\Container\Exception\ { DuplicateServiceException, ServiceNotFoundException }; @@ -121,4 +124,32 @@ public function all ():array { } + /** + * ### Gets registered service implementation types + * + * Returns service identifiers mapped to their statically known implementation classes. + * + * Factory definitions without declared implementation types are excluded. + * @since 1.0.0 + * + * @return array Registered implementation types. + */ + public function implementations ():array { + + $implementations = []; + + foreach ($this->definitions as $id => $definition) { + + if ($definition instanceof Cls) + $implementations[$id] = $definition->class; + + elseif ($definition instanceof Instance) + $implementations[$id] = $definition->instance::class; + + } + + return $implementations; + + } + } \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index 04f36f2..d902eb4 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -338,6 +338,7 @@ private function resolveService (string $id, Service $definition, ResolutionCont * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the constructed instance does not match * the identifier. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return TService Constructed service. */ @@ -451,7 +452,7 @@ function (string $id) use ($context):bool { * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot * be resolved. * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. - * @throws ReflectionException If the class cannot be reflected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return object Created service instance. */ @@ -485,6 +486,7 @@ private function create (Service $definition, ResolutionContext $context):object * @uses \FireHub\Container\Resolver::resolve() To resolve a service. * @uses \FireHub\Container\Resolver::has() To check for service existence. * @uses \FireHub\Container\Internal\Autowirer::construct() To construct an instance using autowiring. + * @uses \FireHub\Container\Registry::implementations() To get all registered concrete implementations. * * @param class-string $class

* Concrete implementation class. @@ -496,7 +498,7 @@ private function create (Service $definition, ResolutionContext $context):object * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot * be resolved. * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. - * @throws ReflectionException If the class cannot be reflected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return object Constructed service instance. */ @@ -504,7 +506,8 @@ private function autowire (string $class, ResolutionContext $context):object { $autowirer = new Autowirer( fn (string $id):object => $this->resolve($id, $context), - fn (string $id):bool => $this->has($id) + fn (string $id):bool => $this->has($id), + fn ():iterable => $this->registry->implementations() ); return $autowirer->construct($class); From 32c623ad82cd3d8ce866d48a30165afe8ec01efa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 13:37:45 +0200 Subject: [PATCH 10/26] Refined type annotations for improved autowiring and docblock clarity. - Updated `Autowirer` to use stricter type annotations for `Closure` parameters and return types, ensuring accurate dependency resolution for candidates and identifiers. - Added a condition to skip non-`ReflectionNamedType` instances while processing intersection type dependencies in `intersectionId`. - Updated the `Registry::implementations` return type to provide precise mapping of class-string keys to class-string values. - Improved docblocks across updated methods for better maintainability and understanding of type-based services. --- src/Internal/Autowirer.php | 7 +++++-- src/Registry.php | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index ece998f..55f134b 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -43,7 +43,7 @@ * @param Closure(class-string):bool $has

* Service registration checker. *

- * @param Closure():(iterable) $candidates

+ * @param Closure():(iterable) $candidates

* Service candidates provider. *

*/ @@ -270,7 +270,7 @@ private function unionId (ReflectionParameter $parameter, ReflectionUnionType $t * * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates satisfy the intersection. * - * @return null|string Selected service identifier, or null if none matches. + * @return null|class-string Selected service identifier, or null if none matches. */ private function intersectionId (ReflectionParameter $parameter, ReflectionIntersectionType $type):?string { @@ -282,6 +282,9 @@ private function intersectionId (ReflectionParameter $parameter, ReflectionInter foreach ($type->getTypes() as $member) { + if (!$member instanceof ReflectionNamedType) + continue; + $contract = $this->classId($parameter, $member); if ($contract === null diff --git a/src/Registry.php b/src/Registry.php index a32ed01..784bbf5 100644 --- a/src/Registry.php +++ b/src/Registry.php @@ -132,7 +132,7 @@ public function all ():array { * Factory definitions without declared implementation types are excluded. * @since 1.0.0 * - * @return array Registered implementation types. + * @return array Registered implementation types. */ public function implementations ():array { From 17d7024a93a59fea4b529de8c19e4720c0f574c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 14:26:31 +0200 Subject: [PATCH 11/26] Refactored `Autowirer` and introduced modular dependency resolution components. - Extracted dependency candidate evaluation into standalone `CandidateSelector` for improved union and intersection type handling. - Added `TypeMatcher` class to encapsulate type inspection and matching logic, replacing inline methods for `classId` and `isConstructible`. - Updated `Autowirer` to integrate `CandidateSelector` and `TypeMatcher`, reducing complexity and centralizing resolution workflows. - Deprecated internal methods (`canResolve`, `classId`, `unionId`, `intersectionId`) in favor of modular components. - Adjusted `Resolver`'s autowiring to utilize the modular `CandidateSelector` and `TypeMatcher` for enhanced constructor-based dependency injection. - Improved alignment with PSR standards and updated minimum PHP version constraint to `>=8.2`. --- src/Internal/Autowirer.php | 251 +++-------------------------- src/Internal/CandidateSelector.php | 239 +++++++++++++++++++++++++++ src/Internal/TypeMatcher.php | 117 ++++++++++++++ src/Resolver.php | 18 ++- 4 files changed, 395 insertions(+), 230 deletions(-) create mode 100644 src/Internal/CandidateSelector.php create mode 100644 src/Internal/TypeMatcher.php diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index 55f134b..8dd0858 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -7,16 +7,13 @@ * @copyright 2026-present The FireHub Project - All rights reserved * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 * - * @php-version >=8.5 + * @php-version >=8.2 * @package Container */ namespace FireHub\Container\Internal; -use FireHub\Container\Exception\ { - AmbiguousDependencyException, ServiceInstantiationException -}; -use FireHub\Runtime; +use FireHub\Container\Exception\ServiceInstantiationException; use Closure, ReflectionClass, ReflectionException, ReflectionNamedType, ReflectionParameter, ReflectionIntersectionType, ReflectionUnionType; @@ -41,16 +38,20 @@ * Context-aware service resolver. *

* @param Closure(class-string):bool $has

- * Service registration checker. + * Explicit service registration checker. *

- * @param Closure():(iterable) $candidates

- * Service candidates provider. + * @param \FireHub\Container\Internal\TypeMatcher $matcher

+ * Dependency type matcher. + *

+ * @param \FireHub\Container\Internal\CandidateSelector $selector

+ * Dependency candidate selector. *

*/ public function __construct ( private Closure $resolve, private Closure $has, - private Closure $candidates + private TypeMatcher $matcher, + private CandidateSelector $selector ) {} /** @@ -113,221 +114,16 @@ public function construct (string $class):object { } - /** - * ### Checks whether a dependency can be resolved - * - * Determines whether the dependency has an explicit registration or represents an implicitly constructible - * concrete class. - * - * This check does not instantiate the dependency. - * @since 1.0.0 - * - * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. - * - * @param class-string $id

- * Dependency identifier. - *

- * - * @return bool True if resolution can be attempted. - */ - private function canResolve (string $id):bool { - - if (($this->has)($id)) - return true; - - if (!Runtime\ObjectModel\Inspection::isClass($id)) - return false; - - return new ReflectionClass($id)->isInstantiable(); - - } - - /** - * ### Resolves a parameter class identifier - * - * Resolves the declared class or interface type of the constructor parameter. - * - * Relative class types are resolved against the class declaring the constructor. Built-in types are not - * considered service identifiers. - * @since 1.0.0 - * - * @param ReflectionParameter $parameter

- * Constructor parameter. - *

- * @param ReflectionNamedType $type

- * Declared parameter type. - *

- * - * @return null|class-string Resolved service identifier, or null for built-in types. - */ - private function classId (ReflectionParameter $parameter, ReflectionNamedType $type):?string { - - if ($type->isBuiltin()) - return null; - - /** @var null|class-string */ - return match ($type->getName()) { - 'self' => $parameter->getDeclaringClass()?->getName(), - 'parent' => $parameter->getDeclaringClass()?->getParentClass()?->getName(), // @phpstan-ignore method.nonObject - default => $type->getName() - }; - - } - - /** - * ### Resolves a union dependency identifier - * - * Selects a unique dependency from a union of class or interface types. - * - * Explicitly registered services take precedence over implicitly constructible concrete classes. - * - * Multiple candidates at the same priority level are considered ambiguous. - * - * This method does not instantiate any dependencies. - * @since 1.0.0 - * - * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the union contains multiple candidates. - * @uses \FireHub\Runtime\Arr\Access::keys() To get the union member identifiers. - * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first union member identifier. - * - * @param ReflectionParameter $parameter

- * Constructor parameter. - *

- * @param ReflectionUnionType $type

- * Declared union type. - *

- * - * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. - * - * @return null|class-string Selected service identifier, or null if no candidate is available. - */ - private function unionId (ReflectionParameter $parameter, ReflectionUnionType $type):?string { - - $registered = []; - $implicit = []; - - foreach ($type->getTypes() as $member) { - - if (!$member instanceof ReflectionNamedType) - continue; - - $id = $this->classId($parameter, $member); - - if ($id === null) - continue; - - if (($this->has)($id)) { - - $registered[$id] = true; - - continue; - - } - - if ($this->canResolve($id)) - $implicit[$id] = true; - - } - - $candidates = $registered !== [] ? $registered : $implicit; - - if (Runtime\Arr\Inspection::count($candidates) > 1) - throw new AmbiguousDependencyException( - context: [ - 'class' => $parameter->getDeclaringClass()?->getName(), - 'parameter' => $parameter->getName(), - 'type' => (string)$type, - 'candidates' => Runtime\Arr\Access::keys($candidates) - ] - ); - - return Runtime\Arr\Access::firstKey($candidates); - - } - - /** - * ### Resolves an intersection dependency identifier - * - * Selects a registered service whose implementation satisfies every declared type in the intersection. - * - * Resolution succeeds only when exactly one eligible candidate exists. - * - * This method does not instantiate any dependencies. - * @since 1.0.0 - * - * @uses \FireHub\Container\Internal\Autowirer::classId() Resolves a parameter class identifier. - * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the intersection contains multiple candidates. - * @uses \FireHub\Runtime\Arr\Access::keys() To get the intersection member identifiers. - * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first intersection member identifier. - * @uses \FireHub\Runtime\ObjectModel\Relation::instanceOf() To check if a service implements a contract. - * - * @param ReflectionParameter $parameter

- * Constructor parameter. - *

- * @param ReflectionIntersectionType $type

- * Declared intersection type. - *

- * - * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates satisfy the intersection. - * - * @return null|class-string Selected service identifier, or null if none matches. - */ - private function intersectionId (ReflectionParameter $parameter, ReflectionIntersectionType $type):?string { - - $matches = []; - - foreach (($this->candidates)() as $id => $implementation) { - - $matchesAll = true; - - foreach ($type->getTypes() as $member) { - - if (!$member instanceof ReflectionNamedType) - continue; - - $contract = $this->classId($parameter, $member); - - if ($contract === null - || !Runtime\ObjectModel\Relation::instanceOf( - $implementation, - $contract - )) { - - $matchesAll = false; - - break; - - } - - } - - if ($matchesAll) - $matches[$id] = true; - - } - - if (Runtime\Arr\Inspection::count($matches) > 1) - throw new AmbiguousDependencyException( - context: [ - 'class' => $parameter->getDeclaringClass()?->getName(), - 'parameter' => $parameter->getName(), - 'type' => (string)$type, - 'candidates' => Runtime\Arr\Access::keys($matches) - ] - ); - - return Runtime\Arr\Access::firstKey($matches); - - } - /** * ### Resolves a constructor parameter * @since 1.0.0 * - * @uses \FireHub\Container\Internal\Autowirer::canResolve() Checks if a dependency can be resolved. * @uses \FireHub\Container\Internal\Autowirer::resolve() Resolves a dependency. - * @uses \FireHub\Container\Internal\Autowirer::classId() Resolves a parameter class identifier. - * @uses \FireHub\Container\Internal\Autowirer::unionId() Resolves a union dependency identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::unionId() Resolves a union dependency identifier. + * @uses \FireHub\Container\Internal\CandidateSelector::union() Resolves a union dependency. + * @uses \FireHub\Container\Internal\CandidateSelector::intersection() Resolves an intersection dependency. + * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() Checks if a class is implicitly constructible. * * @param ReflectionParameter $parameter

* Constructor parameter. @@ -344,16 +140,19 @@ private function resolveParameter (ReflectionParameter $parameter):mixed { $type = $parameter->getType(); $id = match (true) { - $type instanceof ReflectionNamedType - => $this->classId($parameter, $type), - $type instanceof ReflectionUnionType - => $this->unionId($parameter, $type), - $type instanceof ReflectionIntersectionType - => $this->intersectionId($parameter, $type), + $type instanceof ReflectionNamedType => + $this->matcher->classId($parameter, $type), + $type instanceof ReflectionUnionType => + $this->selector->union($parameter, $type), + $type instanceof ReflectionIntersectionType => + $this->selector->intersection($parameter, $type), default => null }; - if ($id !== null && $this->canResolve($id)) + if ($id !== null && ( + ($this->has)($id) + || $this->matcher->isConstructible($id) + )) return ($this->resolve)($id); if ($parameter->isDefaultValueAvailable()) diff --git a/src/Internal/CandidateSelector.php b/src/Internal/CandidateSelector.php new file mode 100644 index 0000000..a732b67 --- /dev/null +++ b/src/Internal/CandidateSelector.php @@ -0,0 +1,239 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\ { + AmbiguousDependencyException, ServiceInstantiationException +}; +use FireHub\Runtime; +use Closure, ReflectionIntersectionType, ReflectionNamedType, + ReflectionParameter, ReflectionUnionType; + +/** + * ### Selects dependency injection candidates + * + * Evaluates available service candidates for union and intersection constructor dependency types. + * + * Explicit registrations take precedence over implicitly constructible classes when resolving union dependencies. + * + * Intersection dependencies are matched against statically known registered implementation types. + * + * Candidate selection does not instantiate services. + * @since 1.0.0 + * + * @internal + */ +final readonly class CandidateSelector { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\TypeMatcher $matcher

+ * Dependency type matcher- + *

+ * @param Closure(class-string):bool $has

+ * Explicit service registration checker. + *

+ * @param Closure():(iterable) $candidates

+ * Registered service identifiers mapped to implementation classes. + *

+ */ + public function __construct ( + private TypeMatcher $matcher, + private Closure $has, + private Closure $candidates + ) {} + + /** + * ### Selects a union dependency + * + * Selects exactly one eligible service from a union type. + * + * Explicitly registered candidates take precedence over implicitly constructible candidates. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\CandidateSelector::ambiguous() To report multiple candidates. + * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() Checks if a class is implicitly constructible. + * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the union contains multiple candidates. + * @uses \FireHub\Runtime\Arr\Access::keys() To get the union member identifiers. + * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first union member identifier. + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionUnionType $type

+ * Union dependency type. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the union contains an unsupported + * intersection member. + * + * @return null|class-string Selected service identifier. + */ + public function union (ReflectionParameter $parameter, ReflectionUnionType $type):?string { + + /** @var array $registered */ + $registered = []; + + /** @var array $implicit */ + $implicit = []; + + foreach ($type->getTypes() as $member) { + + if (!$member instanceof ReflectionNamedType) + throw new ServiceInstantiationException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName(), + 'type' => (string)$type + ] + ); + + $id = $this->matcher->classId($parameter, $member); + + if ($id === null) + continue; + + if (($this->has)($id)) { + + $registered[$id] = true; + + continue; + + } + + if ($this->matcher->isConstructible($id)) + $implicit[$id] = true; + + } + + $selected = $registered !== [] ? $registered : $implicit; + + if (Runtime\Arr\Inspection::count($selected) > 1) + $this->ambiguous($parameter, $type, Runtime\Arr\Access::keys($selected)); + + return Runtime\Arr\Access::firstKey($selected); + + } + + /** + * ### Selects an intersection dependency + * + * Selects a registered service whose implementation satisfies every contract declared by the intersection type. + * + * Factories without statically known implementation types are excluded from candidate discovery. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\CandidateSelector::ambiguous() To report multiple candidates. + * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::matches() To check if a service implementation satisfies a + * contract. + * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the union contains multiple candidates. + * @uses \FireHub\Runtime\Arr\Access::keys() To get the union member identifiers. + * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first union member identifier. + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionIntersectionType $type

+ * Intersection dependency type. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates satisfy the dependency. + * + * @return null|class-string Selected service identifier. + */ + public function intersection (ReflectionParameter $parameter, ReflectionIntersectionType $type):?string { + + /** @var array $matches */ + $matches = []; + + foreach (($this->candidates)() as $id => $implementation) { + + $matchesAll = true; + + foreach ($type->getTypes() as $member) { + + if (!$member instanceof ReflectionNamedType) { + + $matchesAll = false; + + break; + + } + + $contract = $this->matcher->classId($parameter, $member); + + if ( + $contract === null + || !$this->matcher->matches($implementation, $contract) + ) { + + $matchesAll = false; + + break; + + } + + } + + if ($matchesAll) + $matches[$id] = true; + + } + + if (Runtime\Arr\Inspection::count($matches) > 1) + $this->ambiguous($parameter, $type, Runtime\Arr\Access::keys($matches)); + + return Runtime\Arr\Access::firstKey($matches); + + } + + /** + * ### Throws an ambiguous dependency exception + * + * Reports multiple eligible candidates for a constructor parameter. + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionUnionType|ReflectionIntersectionType $type

+ * Requested dependency type. + *

+ * @param list $candidates

+ * Matching service identifiers. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException Always. + * + * @return never + */ + private function ambiguous (ReflectionParameter $parameter, ReflectionUnionType|ReflectionIntersectionType $type, array $candidates):never { + + throw new AmbiguousDependencyException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName(), + 'type' => (string)$type, + 'candidates' => $candidates + ] + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/TypeMatcher.php b/src/Internal/TypeMatcher.php new file mode 100644 index 0000000..504d00f --- /dev/null +++ b/src/Internal/TypeMatcher.php @@ -0,0 +1,117 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Runtime; +use ReflectionClass, ReflectionNamedType, ReflectionParameter; + +/** + * ### Provides dependency type inspection and matching + * + * Resolves declared constructor parameter types into service identifiers and evaluates compatibility between + * concrete implementations and required dependency contracts. + * + * Type inspection does not instantiate services or modify registrations. + * @since 1.0.0 + * + * @internal + */ +final readonly class TypeMatcher { + + /** + * ### Resolves a parameter class identifier + * + * Converts a named constructor parameter type into a service identifier. + * + * Relative class types are resolved against the class declaring the constructor. Built-in types are excluded. + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionNamedType $type

+ * Declared parameter type. + *

+ * + * @return null|class-string Resolved service identifier, or null for built-in types. + */ + public function classId (ReflectionParameter $parameter, ReflectionNamedType $type):?string { + + if ($type->isBuiltin()) + return null; + + $declaringClass = $parameter->getDeclaringClass(); + + $parent = $declaringClass?->getParentClass(); + + /** @var null|class-string */ + return match ($type->getName()) { + 'self' => $declaringClass?->getName(), + 'parent' => $parent !== false ? $parent?->getName() : null, + default => $type->getName() + }; + + } + + /** + * ### Checks whether a class supports implicit construction + * + * Determines whether the identifier represents an instantiable concrete class. + * + * Constructor dependencies are not resolved during this check. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. + * + * @param class-string $class

+ * Class identifier. + *

+ * + * @return bool True if the class can be implicitly constructed. + */ + public function isConstructible (string $class):bool { + + if (!Runtime\ObjectModel\Inspection::isClass($class)) + return false; + + return new ReflectionClass($class)->isInstantiable(); + + } + + /** + * ### Checks whether an implementation satisfies a contract + * + * Determines whether a concrete implementation is compatible with the specified class or interface. + * + * The implementation is not instantiated. + * @since 1.0.0 + * + * @uses \FireHub\Runtime\ObjectModel\Relation::instanceOf() To check if the implementation satisfies the contract. + * + * @param class-string $implementation

+ * Concrete implementation class. + *

+ * @param class-string $contract

+ * Required dependency contract. + *

+ * + * @return bool True if the implementation satisfies the contract. + */ + public function matches (string $implementation, string $contract):bool { + + return Runtime\ObjectModel\Relation::instanceOf($implementation, $contract); + + } + +} \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index d902eb4..4cb87c4 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,7 +19,7 @@ Alias, Cls, Factory, Instance, Service }; use FireHub\Container\Internal\ { - Autowirer, ContextualResolver, InstanceStore, ResolutionContext, ScopeState + Autowirer, CandidateSelector, ContextualResolver, InstanceStore, ResolutionContext, ScopeState, TypeMatcher }; use FireHub\Container\Exception\ { ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException @@ -486,7 +486,6 @@ private function create (Service $definition, ResolutionContext $context):object * @uses \FireHub\Container\Resolver::resolve() To resolve a service. * @uses \FireHub\Container\Resolver::has() To check for service existence. * @uses \FireHub\Container\Internal\Autowirer::construct() To construct an instance using autowiring. - * @uses \FireHub\Container\Registry::implementations() To get all registered concrete implementations. * * @param class-string $class

* Concrete implementation class. @@ -504,10 +503,21 @@ private function create (Service $definition, ResolutionContext $context):object */ private function autowire (string $class, ResolutionContext $context):object { + $matcher = new TypeMatcher; + + $has = fn (string $id):bool => $this->has($id); + + $selector = new CandidateSelector( + $matcher, + $has, + fn ():iterable => $this->registry->implementations() + ); + $autowirer = new Autowirer( fn (string $id):object => $this->resolve($id, $context), - fn (string $id):bool => $this->has($id), - fn ():iterable => $this->registry->implementations() + $has, + $matcher, + $selector ); return $autowirer->construct($class); From b0c2a10d412125bf747744aa17429eb3a4d00f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 14:44:24 +0200 Subject: [PATCH 12/26] Enhanced exception handling and modularized resolver components. - Added new exceptions (`ScopeClosedException`, `CircularDependencyException`, and `AmbiguousDependencyException`) to improve error reporting during resolution workflows. - Introduced `TypeMatcher` and `CandidateSelector` as reusable class properties in `Resolver` for better dependency management and reduced redundancy. - Refactored `autowire` logic in `Resolver` to reuse shared `TypeMatcher` and `CandidateSelector` instances, consolidating resolution logic. - Updated docblocks to document new exceptions and provide clearer type annotations. --- src/Container.php | 3 +++ src/Resolver.php | 50 +++++++++++++++++++++++++++++++++-------------- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/Container.php b/src/Container.php index 8c7899b..145bf33 100644 --- a/src/Container.php +++ b/src/Container.php @@ -387,6 +387,9 @@ public function alias (string $id, string $target):self { * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return TService Resolved service. */ diff --git a/src/Resolver.php b/src/Resolver.php index 4cb87c4..3794679 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -45,10 +45,25 @@ */ final readonly class Resolver implements ResolverBoundary { + /** + * ### Dependency type matcher + * @since 1.0.0 + */ + private TypeMatcher $matcher; + + /** + * ### Dependency candidate selector + * @since 1.0.0 + */ + private CandidateSelector $selector; + /** * ### Constructor * @since 1.0.0 * + * @uses \FireHub\Container\Resolver::has() To check for service existence. + * @uses \FireHub\Container\Registry::implementations() To get all registered implementations. + * * @param \FireHub\Container\Registry $registry

* Registry containing service definitions. *

@@ -61,7 +76,17 @@ public function __construct ( private Registry $registry, private InstanceStore $singletons - ) {} + ) { + + $this->matcher = new TypeMatcher; + + $this->selector = new CandidateSelector( + $this->matcher, + fn (string $id):bool => $this->has($id), + fn ():iterable => $this->registry->implementations() + ); + + } /** * ### Gets a registered service @@ -84,6 +109,9 @@ public function __construct ( * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return TService Resolved service instance. */ @@ -121,6 +149,8 @@ public function get (string $id):object { * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return TService Resolved service. */ @@ -191,8 +221,8 @@ public function has (string $id):bool { * with its registered identifier. * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. * @throws \FireHub\Container\Exception\ScopeRequiredException If the service requires a scope but none is provided. - * @throws \FireHub\Container\Exception\ServiceNotFoundException If the identifier is not an instantiable class. * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return TService Resolved service. */ @@ -503,21 +533,11 @@ private function create (Service $definition, ResolutionContext $context):object */ private function autowire (string $class, ResolutionContext $context):object { - $matcher = new TypeMatcher; - - $has = fn (string $id):bool => $this->has($id); - - $selector = new CandidateSelector( - $matcher, - $has, - fn ():iterable => $this->registry->implementations() - ); - $autowirer = new Autowirer( fn (string $id):object => $this->resolve($id, $context), - $has, - $matcher, - $selector + fn (string $id):bool => $this->has($id), + $this->matcher, + $this->selector ); return $autowirer->construct($class); From 5743a9ffe3909c5597443358961da780039ff08a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 15:03:34 +0200 Subject: [PATCH 13/26] Added `ConstructorMetadata` and `ConstructorMetadataCache` to centralize and optimize constructor metadata handling. - Introduced `ConstructorMetadata` class to encapsulate immutable metadata for constructors, including instantiability, constructor existence, and parameter details. - Added `ConstructorMetadataCache` to cache and reuse reflection metadata for faster dependency resolution and reduced runtime overhead. - Integrated `ConstructorMetadataCache` into `Autowirer`, `Resolver`, and `TypeMatcher` to streamline constructor metadata retrieval and refactor reflection logic. - Replaced direct `ReflectionClass` and constructor inspection with cached metadata lookups. - Updated `Autowirer::construct` and other resolution logic to use `ConstructorMetadata` for evaluating instantiability and parameter data. - Adjusted constructor dependencies and updated docblocks for improved clarity and alignment with the FireHub architecture. --- src/Internal/Autowirer.php | 41 ++++++------- src/Internal/ConstructorMetadata.php | 57 ++++++++++++++++++ src/Internal/ConstructorMetadataCache.php | 72 +++++++++++++++++++++++ src/Internal/TypeMatcher.php | 19 +++++- src/Resolver.php | 34 +++++------ 5 files changed, 181 insertions(+), 42 deletions(-) create mode 100644 src/Internal/ConstructorMetadata.php create mode 100644 src/Internal/ConstructorMetadataCache.php diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index 8dd0858..3d7ac18 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -7,7 +7,7 @@ * @copyright 2026-present The FireHub Project - All rights reserved * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 * - * @php-version >=8.2 + * @php-version >=8.5 * @package Container */ @@ -46,12 +46,18 @@ * @param \FireHub\Container\Internal\CandidateSelector $selector

* Dependency candidate selector. *

+ * @param \FireHub\Container\Internal\ConstructorMetadataCache $metadata

+ * Shared constructor metadata cache. + *

+ * + * @return void */ public function __construct ( private Closure $resolve, private Closure $has, private TypeMatcher $matcher, - private CandidateSelector $selector + private CandidateSelector $selector, + private ConstructorMetadataCache $metadata ) {} /** @@ -64,6 +70,9 @@ public function __construct ( * Variadic parameters are left empty unless explicitly supplied through a future binding mechanism. * @since 1.0.0 * + * @uses \FireHub\Container\Internal\Autowirer::resolveParameter() To resolve a constructor parameter. + * @uses \FireHub\Container\Internal\ConstructorMetadataCache::get() To get constructor metadata. + * * @param class-string $class

* Concrete service class. *

@@ -77,31 +86,17 @@ public function __construct ( */ public function construct (string $class):object { - try { - - $reflection = new ReflectionClass($class); - - } catch (ReflectionException $exception) { // @phpstan-ignore catch.neverThrown - - throw new ServiceInstantiationException( - previous: $exception - ); - - } - - if (!$reflection->isInstantiable()) - throw new ServiceInstantiationException( - context: ['class' => $class] - ); + $metadata = $this->metadata->get($class); - $constructor = $reflection->getConstructor(); + if (!$metadata->instantiable) + throw new ServiceInstantiationException; - if ($constructor === null) - return $reflection->newInstance(); + if (!$metadata->hasConstructor) + return $metadata->reflection->newInstance(); $arguments = []; - foreach ($constructor->getParameters() as $parameter) { + foreach ($metadata->parameters as $parameter) { if ($parameter->isVariadic()) continue; @@ -110,7 +105,7 @@ public function construct (string $class):object { } - return $reflection->newInstanceArgs($arguments); + return $metadata->reflection->newInstanceArgs($arguments); } diff --git a/src/Internal/ConstructorMetadata.php b/src/Internal/ConstructorMetadata.php new file mode 100644 index 0000000..bee5caf --- /dev/null +++ b/src/Internal/ConstructorMetadata.php @@ -0,0 +1,57 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionClass, ReflectionParameter; + +/** + * ### Represents cached constructor metadata + * + * Provides immutable reflection metadata for a service implementation. + * + * Stores the reflected class, its insatiability, and constructor parameters without resolving dependencies or + * creating service instances. + * @since 1.0.0 + * + * @internal + */ +final readonly class ConstructorMetadata { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param ReflectionClass $reflection

+ * Reflected service implementation. + *

+ * @param bool $instantiable

+ * Whether the implementation can be instantiated. + *

+ * @param bool $hasConstructor

+ * Whether the implementation declares or inherits a constructor. + *

+ * @param list $parameters

+ * Constructor parameters. + *

+ * + * @return void + */ + public function __construct ( + public ReflectionClass $reflection, + public bool $instantiable, + public bool $hasConstructor, + public array $parameters + ) {} + +} \ No newline at end of file diff --git a/src/Internal/ConstructorMetadataCache.php b/src/Internal/ConstructorMetadataCache.php new file mode 100644 index 0000000..87de238 --- /dev/null +++ b/src/Internal/ConstructorMetadataCache.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 Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionClass; + +/** + * ### Caches constructor reflection metadata + * + * Stores constructor metadata for service implementation classes to avoid repeated reflection inspection. + * + * Metadata is cached per resolver instance and reused across dependency resolution operations. + * + * The cache does not store service instances, resolved dependencies, or registration-dependent candidate selections. + * @since 1.0.0 + * + * @internal + */ +final class ConstructorMetadataCache { + + /** + * ### Cached constructor metadata + * @since 1.0.0 + * + * @var array + */ + private array $metadata = []; + + /** + * ### Gets constructor metadata + * + * Returns previously inspected constructor metadata or creates and caches it on first access. + * + * Reflection inspection is performed only once for each implementation class within the cache lifetime. + * @since 1.0.0 + * + * @param class-string $class

+ * Service implementation class. + *

+ * + * @return \FireHub\Container\Internal\ConstructorMetadata Cached constructor metadata. + */ + public function get (string $class):ConstructorMetadata { + + if (isset($this->metadata[$class])) + return $this->metadata[$class]; + + $reflection = new ReflectionClass($class); + + $constructor = $reflection->getConstructor(); + + return $this->metadata[$class] = new ConstructorMetadata( + $reflection, + $reflection->isInstantiable(), + $constructor !== null, + $constructor?->getParameters() ?? [] + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/TypeMatcher.php b/src/Internal/TypeMatcher.php index 504d00f..21529d6 100644 --- a/src/Internal/TypeMatcher.php +++ b/src/Internal/TypeMatcher.php @@ -14,7 +14,7 @@ namespace FireHub\Container\Internal; use FireHub\Runtime; -use ReflectionClass, ReflectionNamedType, ReflectionParameter; +use ReflectionNamedType, ReflectionParameter; /** * ### Provides dependency type inspection and matching @@ -29,6 +29,20 @@ */ final readonly class TypeMatcher { + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ConstructorMetadataCache $metadata

+ * Shared constructor metadata cache. + *

+ * + * @return void + */ + public function __construct ( + private ConstructorMetadataCache $metadata + ) {} + /** * ### Resolves a parameter class identifier * @@ -72,6 +86,7 @@ public function classId (ReflectionParameter $parameter, ReflectionNamedType $ty * Constructor dependencies are not resolved during this check. * @since 1.0.0 * + * @uses \FireHub\Container\Internal\ConstructorMetadataCache::get() To retrieve constructor metadata. * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. * * @param class-string $class

@@ -85,7 +100,7 @@ public function isConstructible (string $class):bool { if (!Runtime\ObjectModel\Inspection::isClass($class)) return false; - return new ReflectionClass($class)->isInstantiable(); + return $this->metadata->get($class)->instantiable; } diff --git a/src/Resolver.php b/src/Resolver.php index 3794679..358b9f7 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,13 +19,13 @@ Alias, Cls, Factory, Instance, Service }; use FireHub\Container\Internal\ { - Autowirer, CandidateSelector, ContextualResolver, InstanceStore, ResolutionContext, ScopeState, TypeMatcher + Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualResolver, InstanceStore, ResolutionContext, + ScopeState, TypeMatcher }; use FireHub\Container\Exception\ { ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException }; use FireHub\Runtime; -use ReflectionClass, ReflectionException; /** * ### Resolves registered dependency injection services @@ -57,6 +57,12 @@ */ private CandidateSelector $selector; + /** + * ### Constructor metadata cache + * @since 1.0.0 + */ + private ConstructorMetadataCache $metadata; + /** * ### Constructor * @since 1.0.0 @@ -78,7 +84,11 @@ public function __construct ( private InstanceStore $singletons ) { - $this->matcher = new TypeMatcher; + $this->metadata = new ConstructorMetadataCache; + + $this->matcher = new TypeMatcher( + $this->metadata + ); $this->selector = new CandidateSelector( $this->matcher, @@ -353,6 +363,7 @@ private function resolveService (string $id, Service $definition, ResolutionCont * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. * @uses \FireHub\Container\Resolver::autowire() To construct an instance using autowiring. + * @uses \FireHub\Container\Internal\ConstructorMetadataCache::get() To retrieve constructor metadata. * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. * * @template TService of object @@ -377,19 +388,7 @@ private function resolveImplicit (string $id, ResolutionContext $context):object if (!Runtime\ObjectModel\Inspection::isClass($id)) throw new ServiceNotFoundException; - try { - - $reflection = new ReflectionClass($id); - - } catch (ReflectionException $exception) { // @phpstan-ignore catch.neverThrown - - throw new ServiceNotFoundException( - previous: $exception - ); - - } - - if (!$reflection->isInstantiable()) + if (!$this->metadata->get($id)->instantiable) throw new ServiceNotFoundException; $context->stack->enter($id); @@ -537,7 +536,8 @@ private function autowire (string $class, ResolutionContext $context):object { fn (string $id):object => $this->resolve($id, $context), fn (string $id):bool => $this->has($id), $this->matcher, - $this->selector + $this->selector, + $this->metadata ); return $autowirer->construct($class); From 1b48f6709850319a9ad16d2dd6824aaf0ccc8037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 15:21:48 +0200 Subject: [PATCH 14/26] Added `DependencyPlan`, `ParameterPlan`, and `DependencyPlanCache` to optimize and centralize dependency resolution workflows. - Introduced `DependencyPlan` and `ParameterPlan` classes to encapsulate precompiled dependency and parameter construction plans. - Added `DependencyPlanCache` to store and reuse static parameter analysis for faster service instantiation and reduced runtime overhead. - Refactored `Autowirer` to use `DependencyPlanCache` and precompiled plans, streamlining the resolution process. - Adjusted `Resolver` and associated components to integrate `DependencyPlanCache`, replacing dynamic constructor metadata lookups with cached plans. - Enhanced `resolveParameter` logic in `Autowirer` to execute precompiled parameter plans, improving modularity and reuse. - Updated docblocks for new classes and methods, aligning with FireHub architecture standards. - Improved service instantiation performance by reducing redundant constructor reflection and reusing cached data. --- src/Internal/Autowirer.php | 91 ++++++++++----------- src/Internal/DependencyPlan.php | 46 +++++++++++ src/Internal/DependencyPlanCache.php | 118 +++++++++++++++++++++++++++ src/Internal/ParameterPlan.php | 60 ++++++++++++++ src/Resolver.php | 17 +++- 5 files changed, 282 insertions(+), 50 deletions(-) create mode 100644 src/Internal/DependencyPlan.php create mode 100644 src/Internal/DependencyPlanCache.php create mode 100644 src/Internal/ParameterPlan.php diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index 3d7ac18..66ef640 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -14,16 +14,15 @@ namespace FireHub\Container\Internal; use FireHub\Container\Exception\ServiceInstantiationException; -use Closure, ReflectionClass, ReflectionException, ReflectionNamedType, ReflectionParameter, - ReflectionIntersectionType, ReflectionUnionType; +use Closure, ReflectionIntersectionType, ReflectionUnionType; /** - * ### Automatically constructs registered services + * ### Constructs services using compiled dependency plans * - * Resolves constructor dependencies using registered service definitions. + * Executes cached dependency construction plans to instantiate services and resolve their constructor dependencies. * - * Only class and interface dependencies are supported. Scalar, union, and intersection types require explicit - * factory definitions. + * Static parameter analysis is performed by the dependency plan cache, while service availability and candidate + * selection are evaluated dynamically within the active resolution context. * @since 1.0.0 * * @internal @@ -46,8 +45,8 @@ * @param \FireHub\Container\Internal\CandidateSelector $selector

* Dependency candidate selector. *

- * @param \FireHub\Container\Internal\ConstructorMetadataCache $metadata

- * Shared constructor metadata cache. + * @param \FireHub\Container\Internal\DependencyPlanCache $plans

+ * Shared dependency plan cache. *

* * @return void @@ -57,20 +56,16 @@ public function __construct ( private Closure $has, private TypeMatcher $matcher, private CandidateSelector $selector, - private ConstructorMetadataCache $metadata + private DependencyPlanCache $plans ) {} /** - * ### Constructs a service with resolved dependencies + * ### Constructs a service using its compiled dependency plan * - * Inspects the constructor and resolves registered or implicitly constructible object dependencies. - * - * Parameters with default values use their defaults when no eligible dependency is available. - * Nullable parameters without default values remain required. - * Variadic parameters are left empty unless explicitly supplied through a future binding mechanism. + * Resolves constructor dependencies dynamically while reusing previously compiled parameter information. * @since 1.0.0 * - * @uses \FireHub\Container\Internal\Autowirer::resolveParameter() To resolve a constructor parameter. + * @uses \FireHub\Container\Internal\DependencyPlanCache::get() To retrieve the compiled construction plan. * @uses \FireHub\Container\Internal\ConstructorMetadataCache::get() To get constructor metadata. * * @param class-string $class

@@ -86,7 +81,9 @@ public function __construct ( */ public function construct (string $class):object { - $metadata = $this->metadata->get($class); + $plan = $this->plans->get($class); + + $metadata = $plan->metadata; if (!$metadata->instantiable) throw new ServiceInstantiationException; @@ -96,32 +93,26 @@ public function construct (string $class):object { $arguments = []; - foreach ($metadata->parameters as $parameter) { - - if ($parameter->isVariadic()) - continue; - + foreach ($plan->parameters as $parameter) $arguments[] = $this->resolveParameter($parameter); - } - return $metadata->reflection->newInstanceArgs($arguments); } /** - * ### Resolves a constructor parameter + * ### Resolves a compiled constructor parameter + * + * Executes the precompiled parameter plan while evaluating dependency availability and composite candidate + * selection against the current service registrations. * @since 1.0.0 * - * @uses \FireHub\Container\Internal\Autowirer::resolve() Resolves a dependency. - * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. - * @uses \FireHub\Container\Internal\TypeMatcher::unionId() Resolves a union dependency identifier. - * @uses \FireHub\Container\Internal\CandidateSelector::union() Resolves a union dependency. - * @uses \FireHub\Container\Internal\CandidateSelector::intersection() Resolves an intersection dependency. - * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() Checks if a class is implicitly constructible. + * @uses \FireHub\Container\Internal\CandidateSelector::union() To select a union dependency. + * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select an intersection dependency. + * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check implicit constructibility. * - * @param ReflectionParameter $parameter

- * Constructor parameter. + * @param \FireHub\Container\Internal\ParameterPlan $plan

+ * Compiled constructor parameter plan. *

* * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot @@ -130,19 +121,21 @@ public function construct (string $class):object { * * @return mixed Resolved argument. */ - private function resolveParameter (ReflectionParameter $parameter):mixed { + private function resolveParameter (ParameterPlan $plan):mixed { - $type = $parameter->getType(); + $id = $plan->id; + + if ($plan->composite instanceof ReflectionUnionType) + $id = $this->selector->union( + $plan->parameter, + $plan->composite + ); - $id = match (true) { - $type instanceof ReflectionNamedType => - $this->matcher->classId($parameter, $type), - $type instanceof ReflectionUnionType => - $this->selector->union($parameter, $type), - $type instanceof ReflectionIntersectionType => - $this->selector->intersection($parameter, $type), - default => null - }; + elseif ($plan->composite instanceof ReflectionIntersectionType) + $id = $this->selector->intersection( + $plan->parameter, + $plan->composite + ); if ($id !== null && ( ($this->has)($id) @@ -150,8 +143,12 @@ private function resolveParameter (ReflectionParameter $parameter):mixed { )) return ($this->resolve)($id); - if ($parameter->isDefaultValueAvailable()) - return $parameter->getDefaultValue(); + if ($plan->hasDefault) + return $plan->default; + + $parameter = $plan->parameter; + + $type = $parameter->getType(); throw new ServiceInstantiationException( context: [ @@ -161,7 +158,7 @@ private function resolveParameter (ReflectionParameter $parameter):mixed { 'type' => $type?->__toString(), 'nullable' => $type?->allowsNull(), 'optional' => $parameter->isOptional(), - 'has_default' => $parameter->isDefaultValueAvailable() + 'has_default' => $plan->hasDefault ] ); diff --git a/src/Internal/DependencyPlan.php b/src/Internal/DependencyPlan.php new file mode 100644 index 0000000..23bb26a --- /dev/null +++ b/src/Internal/DependencyPlan.php @@ -0,0 +1,46 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +/** + * ### Represents a compiled dependency construction plan + * + * Defines the statically analyzed constructor parameters required to instantiate a service. + * + * The plan contains no resolved service instances and is independent of the active resolution context. + * @since 1.0.0 + * + * @internal + */ +final readonly class DependencyPlan { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ConstructorMetadata $metadata

+ * Cached constructor reflection metadata. + *

+ * @param list<\FireHub\Container\Internal\ParameterPlan> $parameters

+ * Compiled constructor parameter plans. + *

+ * + * @return void + */ + public function __construct ( + public ConstructorMetadata $metadata, + public array $parameters + ) {} + +} \ No newline at end of file diff --git a/src/Internal/DependencyPlanCache.php b/src/Internal/DependencyPlanCache.php new file mode 100644 index 0000000..3617655 --- /dev/null +++ b/src/Internal/DependencyPlanCache.php @@ -0,0 +1,118 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionNamedType, ReflectionUnionType, ReflectionIntersectionType; + +/** + * ### Compiles and caches dependency construction plans + * + * Analyzes constructor parameters once per implementation class and stores the resulting dependency plans. + * + * Compiled plans contain only static constructor information. Registered service availability, union and + * intersection candidate selection, and dependency instantiation remain dynamic. + * @since 1.0.0 + * + * @internal + */ +final class DependencyPlanCache { + + /** + * ### Cached dependency plans + * @since 1.0.0 + * + * @var array + */ + private array $plans = []; + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ConstructorMetadataCache $metadata

+ * Shared constructor metadata cache. + *

+ * @param \FireHub\Container\Internal\TypeMatcher $matcher

+ * Dependency type matcher. + *

+ * + * @return void + */ + public function __construct( + private readonly ConstructorMetadataCache $metadata, + private readonly TypeMatcher $matcher + ) {} + + /** + * ### Gets a compiled dependency plan + * + * Returns the cached dependency plan or compiles one from constructor reflection metadata. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\TypeMatcher::classId() To resolve named dependency identifiers. + * @uses \FireHub\Container\Internal\ConstructorMetadataCache::get() To retrieve constructor metadata. + * + * @param class-string $class

+ * Service implementation class. + *

+ * + * @return \FireHub\Container\Internal\DependencyPlan Compiled dependency plan. + */ + public function get (string $class):DependencyPlan { + + if (isset($this->plans[$class])) + return $this->plans[$class]; + + $metadata = $this->metadata->get($class); + + $parameters = []; + + foreach ($metadata->parameters as $parameter) { + + if ($parameter->isVariadic()) + continue; + + $type = $parameter->getType(); + + $id = $type instanceof ReflectionNamedType + ? $this->matcher->classId($parameter, $type) + : null; + + $composite = $type instanceof ReflectionUnionType + || $type instanceof ReflectionIntersectionType + ? $type + : null; + + $hasDefault = $parameter->isDefaultValueAvailable(); + + $parameters[] = new ParameterPlan( + $parameter, + $id, + $composite, + $hasDefault, + $hasDefault + ? $parameter->getDefaultValue() + : null + ); + + } + + return $this->plans[$class] = new DependencyPlan( + $metadata, + $parameters + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/ParameterPlan.php b/src/Internal/ParameterPlan.php new file mode 100644 index 0000000..66e463a --- /dev/null +++ b/src/Internal/ParameterPlan.php @@ -0,0 +1,60 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionParameter, ReflectionUnionType, ReflectionIntersectionType; + +/** + * ### Represents a compiled constructor parameter plan + * + * Stores the statically determined information required to resolve a constructor parameter. + * + * Dependency availability and candidate selection remain dynamic and are evaluated during plan execution. + * @since 1.0.0 + * + * @internal + */ +final readonly class ParameterPlan { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Original constructor parameter. + *

+ * @param null|class-string $id

+ * Statically resolved named dependency identifier. + *

+ * @param null|ReflectionUnionType|ReflectionIntersectionType $composite

+ * Composite dependency type requiring dynamic candidate selection. + *

+ * @param bool $hasDefault

+ * Whether the parameter provides a default value. + *

+ * @param mixed $default

+ * Default parameter value. + *

+ * + * @return void + */ + public function __construct ( + public ReflectionParameter $parameter, + public ?string $id, + public ReflectionUnionType|ReflectionIntersectionType|null $composite, + public bool $hasDefault, + public mixed $default + ) {} + +} \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index 358b9f7..c005fa8 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,8 +19,8 @@ Alias, Cls, Factory, Instance, Service }; use FireHub\Container\Internal\ { - Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualResolver, InstanceStore, ResolutionContext, - ScopeState, TypeMatcher + Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualResolver, DependencyPlanCache, InstanceStore, + ResolutionContext, ScopeState, TypeMatcher }; use FireHub\Container\Exception\ { ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException @@ -63,6 +63,12 @@ */ private ConstructorMetadataCache $metadata; + /** + * ### Compiled dependency plan cache + * @since 1.0.0 + */ + private DependencyPlanCache $plans; + /** * ### Constructor * @since 1.0.0 @@ -96,6 +102,11 @@ public function __construct ( fn ():iterable => $this->registry->implementations() ); + $this->plans = new DependencyPlanCache( + $this->metadata, + $this->matcher + ); + } /** @@ -537,7 +548,7 @@ private function autowire (string $class, ResolutionContext $context):object { fn (string $id):bool => $this->has($id), $this->matcher, $this->selector, - $this->metadata + $this->plans ); return $autowirer->construct($class); From 164a7356d455c3258f6a093fd58a1542c5fc63ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 15:30:31 +0200 Subject: [PATCH 15/26] Refactored `Autowirer` and `Resolver` to reuse a shared `Autowirer` instance for enhanced dependency resolution. - Updated `Autowirer` to accept `ResolutionContext` in its methods, enabling context-aware resolution. - Refactored `Resolver` to introduce a reusable `Autowirer` instance, consolidating dependency resolution workflows. - Simplified `autowire` implementation in `Resolver` by delegating directly to the shared `Autowirer` instance. - Updated method signatures in `Autowirer` (`construct`, `resolveParameter`) to support contextual resolution. - Integrated `Autowirer` adjustments with `ResolutionContext` to improve modularity, reusability, and maintainability. - Enhanced docblocks to reflect new parameter signatures and provide better clarity on context-aware resolution mechanics. --- src/Internal/Autowirer.php | 16 +++++++++++----- src/Resolver.php | 28 +++++++++++++++++----------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index 66ef640..fa80ad3 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -33,7 +33,7 @@ * ### Constructor * @since 1.0.0 * - * @param Closure(class-string):object $resolve

+ * @param Closure(class-string, \FireHub\Container\Internal\ResolutionContext):object $resolve

* Context-aware service resolver. *

* @param Closure(class-string):bool $has

@@ -71,6 +71,9 @@ public function __construct ( * @param class-string $class

* Concrete service class. *

+ * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active dependency resolution context. + *

* * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot * be resolved. @@ -79,7 +82,7 @@ public function __construct ( * * @return object Constructed service instance. */ - public function construct (string $class):object { + public function construct (string $class, ResolutionContext $context):object { $plan = $this->plans->get($class); @@ -94,7 +97,7 @@ public function construct (string $class):object { $arguments = []; foreach ($plan->parameters as $parameter) - $arguments[] = $this->resolveParameter($parameter); + $arguments[] = $this->resolveParameter($parameter, $context); return $metadata->reflection->newInstanceArgs($arguments); @@ -114,6 +117,9 @@ public function construct (string $class):object { * @param \FireHub\Container\Internal\ParameterPlan $plan

* Compiled constructor parameter plan. *

+ * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active resolution context. + *

* * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot * be resolved. @@ -121,7 +127,7 @@ public function construct (string $class):object { * * @return mixed Resolved argument. */ - private function resolveParameter (ParameterPlan $plan):mixed { + private function resolveParameter (ParameterPlan $plan, ResolutionContext $context):mixed { $id = $plan->id; @@ -141,7 +147,7 @@ private function resolveParameter (ParameterPlan $plan):mixed { ($this->has)($id) || $this->matcher->isConstructible($id) )) - return ($this->resolve)($id); + return ($this->resolve)($id, $context); if ($plan->hasDefault) return $plan->default; diff --git a/src/Resolver.php b/src/Resolver.php index c005fa8..a591d81 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -69,11 +69,18 @@ */ private DependencyPlanCache $plans; + /** + * ### Reusable dependency autowirer + * @since 1.0.0 + */ + private Autowirer $autowirer; + /** * ### Constructor * @since 1.0.0 * * @uses \FireHub\Container\Resolver::has() To check for service existence. + * @uses \FireHub\Container\Resolver::get() To retrieve a service definition. * @uses \FireHub\Container\Registry::implementations() To get all registered implementations. * * @param \FireHub\Container\Registry $registry

@@ -107,6 +114,15 @@ public function __construct ( $this->matcher ); + $this->autowirer = new Autowirer( + fn (string $id, ResolutionContext $context):object => + $this->resolve($id, $context), + fn (string $id):bool => $this->has($id), + $this->matcher, + $this->selector, + $this->plans + ); + } /** @@ -523,8 +539,6 @@ private function create (Service $definition, ResolutionContext $context):object * Automatically resolves registered constructor dependencies while preserving the current resolution context. * @since 1.0.0 * - * @uses \FireHub\Container\Resolver::resolve() To resolve a service. - * @uses \FireHub\Container\Resolver::has() To check for service existence. * @uses \FireHub\Container\Internal\Autowirer::construct() To construct an instance using autowiring. * * @param class-string $class

@@ -543,15 +557,7 @@ private function create (Service $definition, ResolutionContext $context):object */ private function autowire (string $class, ResolutionContext $context):object { - $autowirer = new Autowirer( - fn (string $id):object => $this->resolve($id, $context), - fn (string $id):bool => $this->has($id), - $this->matcher, - $this->selector, - $this->plans - ); - - return $autowirer->construct($class); + return $this->autowirer->construct($class, $context); } From 537ad5d729ba9fae1a4b1645867a32eb5e265a7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 15:37:09 +0200 Subject: [PATCH 16/26] Added `find` method to `Registry` for non-throwing service definition lookup and updated `Resolver` to integrate improved resolution logic. - Introduced `Registry::find` to allow retrieving service definitions without exceptions for missing identifiers. - Refactored `Resolver::resolve` to use `Registry::find`, replacing the previous `has` and `get` calls for streamlined service resolution. - Enhanced singleton resolution in `Resolver::get` to check and return existing instances before resolving services. - Updated `Resolver::resolve` to handle null definitions by delegating to `resolveImplicit`, ensuring robust handling of unregistered services. - Adjusted docblocks to reflect the addition of `find` and updated service resolution workflow in `Registry` and `Resolver`. --- src/Registry.php | 19 +++++++++++++++++++ src/Resolver.php | 23 ++++++++++++----------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/Registry.php b/src/Registry.php index 784bbf5..35cba35 100644 --- a/src/Registry.php +++ b/src/Registry.php @@ -110,6 +110,25 @@ public function has (string $id):bool { } + /** + * ### Finds a registered service definition + * + * Returns the service definition associated with the specified identifier without throwing an exception when + * the identifier is not registered. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return null|\FireHub\Container\Definition Registered service definition, or null if absent. + */ + public function find (string $id):?Definition { + + return $this->definitions[$id] ?? null; + + } + /** * ### Gets all registered service definitions * diff --git a/src/Resolver.php b/src/Resolver.php index a591d81..bb078ca 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -134,6 +134,7 @@ public function __construct ( * @since 1.0.0 * * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * @uses \FireHub\Container\Internal\InstanceStore::get() To retrieve a previously resolved instance. * * @template TService of object * @@ -154,9 +155,15 @@ public function __construct ( */ public function get (string $id):object { + $instance = $this->singletons->get($id); + + if ($instance !== null) + /** @var TService $instance */ + return $instance; + return $this->resolve( $id, - new ResolutionContext() + new ResolutionContext ); } @@ -237,8 +244,7 @@ public function has (string $id):bool { * ### Resolves a service within the specified context * @since 1.0.0 * - * @uses \FireHub\Container\Registry::has() To check for service existence. - * @uses \FireHub\Container\Registry::get() To retrieve the service definition. + * @uses \FireHub\Container\Registry::find() To retrieve a service definition. * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. * @uses \FireHub\Container\Resolver::resolveService() To resolve a service. @@ -265,10 +271,10 @@ public function has (string $id):bool { */ private function resolve (string $id, ResolutionContext $context):object { - if (!$this->registry->has($id)) - return $this->resolveImplicit($id, $context); + $definition = $this->registry->find($id); - $definition = $this->registry->get($id); + if ($definition === null) + return $this->resolveImplicit($id, $context); if ($definition instanceof Alias) { @@ -515,20 +521,15 @@ function (string $id) use ($context):bool { private function create (Service $definition, ResolutionContext $context):object { return match (true) { - $definition instanceof Instance => $definition->instance, - $definition instanceof Factory => ($definition->factory)( $this->contextual($context) ), - $definition instanceof Cls => $this->autowire( $definition->class, $context ), - default => throw new UnsupportedDefinitionException - }; } From 6ccf726d10e85b1733442176e862d7d9e4592f8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 15:47:08 +0200 Subject: [PATCH 17/26] Refactored `ResolutionStack` to improve circular dependency detection and optimize state management. - Introduced `active` property in `ResolutionStack` to track active service identifiers, replacing the previous implementation relying on array inspections. - Updated `enter` method to use `active` for circular dependency detection, improving performance and reducing reliance on helper utilities. - Added logic to manage `active` state in `leave` method, ensuring accurate cleanup of resolved service identifiers. - Enhanced docblocks to describe the purpose of `active` and its role in resolution workflows. - Simplified `ResolutionContext::root` logic to handle null-scoped cases gracefully. --- src/Internal/ResolutionContext.php | 3 +++ src/Internal/ResolutionStack.php | 19 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Internal/ResolutionContext.php b/src/Internal/ResolutionContext.php index e11d30f..d71208b 100644 --- a/src/Internal/ResolutionContext.php +++ b/src/Internal/ResolutionContext.php @@ -61,6 +61,9 @@ public function __construct ( */ public function root ():self { + if ($this->scoped === null) + return $this; + return new self( stack: $this->stack, scope: $this->scope diff --git a/src/Internal/ResolutionStack.php b/src/Internal/ResolutionStack.php index 6c6aa4c..bac440f 100644 --- a/src/Internal/ResolutionStack.php +++ b/src/Internal/ResolutionStack.php @@ -35,10 +35,16 @@ final class ResolutionStack { private array $stack = []; /** - * ### Starts resolving a service + * ### Active service identifiers * @since 1.0.0 * - * @uses \FireHub\Runtime\Arr\Inspection::inArray() Checks if the service is already being resolved. + * @var array + */ + private array $active = []; + + /** + * ### Starts resolving a service + * @since 1.0.0 * * @param class-string $id

* Service identifier. @@ -50,7 +56,7 @@ final class ResolutionStack { */ public function enter (string $id):void { - if (Runtime\Arr\Inspection::inArray($this->stack, $id)) + if (isset($this->active[$id])) throw new CircularDependencyException( context: [ 'stack' => $this->stack @@ -59,6 +65,8 @@ public function enter (string $id):void { $this->stack[] = $id; + $this->active[$id] = true; + } /** @@ -71,7 +79,10 @@ public function enter (string $id):void { */ public function leave ():void { - Runtime\Arr\Mutation::pop($this->stack); + $id = Runtime\Arr\Mutation::pop($this->stack); + + if ($id !== null) + unset($this->active[$id]); } From 7ac1646ba87250af590fead4facc12bbdcf0d280 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Thu, 15 Oct 2026 15:59:38 +0200 Subject: [PATCH 18/26] Enhanced autowiring with modularized type matching and service validation. - Added `matchesType` method to `CandidateSelector` for recursive evaluation of union, intersection, and named dependency types. - Updated `Autowirer` to validate service implementations using `CandidateSelector::matchesType` during instantiation, throwing a detailed `ServiceInstantiationException` for mismatches. - Refactored union dependency resolution to streamline candidate selection with stricter type checks, prioritizing registered implementations. - Improved exception handling and updated docblocks to describe the role of `matchesType` in dependency resolution workflows. - Enhanced service instantiation safety by enforcing type conformity for dynamically resolved dependencies. --- src/Internal/Autowirer.php | 27 ++++++- src/Internal/CandidateSelector.php | 120 ++++++++++++++++++++--------- 2 files changed, 110 insertions(+), 37 deletions(-) diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index fa80ad3..0917a17 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -113,6 +113,7 @@ public function construct (string $class, ResolutionContext $context):object { * @uses \FireHub\Container\Internal\CandidateSelector::union() To select a union dependency. * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select an intersection dependency. * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check implicit constructibility. + * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation * * @param \FireHub\Container\Internal\ParameterPlan $plan

* Compiled constructor parameter plan. @@ -146,8 +147,30 @@ private function resolveParameter (ParameterPlan $plan, ResolutionContext $conte if ($id !== null && ( ($this->has)($id) || $this->matcher->isConstructible($id) - )) - return ($this->resolve)($id, $context); + )) { + + $instance = ($this->resolve)($id, $context); + + if ( + $plan->composite !== null + && !$this->selector->matchesType( + $instance::class, + $plan->parameter, + $plan->composite + ) + ) + throw new ServiceInstantiationException( + context: [ + 'class' => $plan->parameter->getDeclaringClass()?->getName(), + 'parameter' => $plan->parameter->getName(), + 'type' => (string)$plan->composite, + 'instance' => $instance::class + ] + ); + + return $instance; + + } if ($plan->hasDefault) return $plan->default; diff --git a/src/Internal/CandidateSelector.php b/src/Internal/CandidateSelector.php index a732b67..a700f11 100644 --- a/src/Internal/CandidateSelector.php +++ b/src/Internal/CandidateSelector.php @@ -18,7 +18,7 @@ }; use FireHub\Runtime; use Closure, ReflectionIntersectionType, ReflectionNamedType, - ReflectionParameter, ReflectionUnionType; + ReflectionParameter, ReflectionType, ReflectionUnionType; /** * ### Selects dependency injection candidates @@ -57,13 +57,15 @@ public function __construct ( ) {} /** - * ### Selects a union dependency + * ### Selects a union or DNF dependency * - * Selects exactly one eligible service from a union type. + * Selects a unique eligible service from the declared union alternatives. * - * Explicitly registered candidates take precedence over implicitly constructible candidates. + * Registered candidates take precedence over implicitly constructible alternatives. * @since 1.0.0 * + * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation + * satisfies a dependency type. * @uses \FireHub\Container\Internal\CandidateSelector::ambiguous() To report multiple candidates. * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() Checks if a class is implicitly constructible. @@ -92,16 +94,17 @@ public function union (ReflectionParameter $parameter, ReflectionUnionType $type /** @var array $implicit */ $implicit = []; + foreach (($this->candidates)() as $id => $implementation) { + + if ($this->matchesType($implementation, $parameter, $type)) + $registered[$id] = true; + + } + foreach ($type->getTypes() as $member) { if (!$member instanceof ReflectionNamedType) - throw new ServiceInstantiationException( - context: [ - 'class' => $parameter->getDeclaringClass()?->getName(), - 'parameter' => $parameter->getName(), - 'type' => (string)$type - ] - ); + continue; $id = $this->matcher->classId($parameter, $member); @@ -121,10 +124,16 @@ public function union (ReflectionParameter $parameter, ReflectionUnionType $type } - $selected = $registered !== [] ? $registered : $implicit; + $selected = $registered !== [] + ? $registered + : $implicit; if (Runtime\Arr\Inspection::count($selected) > 1) - $this->ambiguous($parameter, $type, Runtime\Arr\Access::keys($selected)); + $this->ambiguous( + $parameter, + $type, + Runtime\Arr\Access::keys($selected) + ); return Runtime\Arr\Access::firstKey($selected); @@ -139,9 +148,8 @@ public function union (ReflectionParameter $parameter, ReflectionUnionType $type * @since 1.0.0 * * @uses \FireHub\Container\Internal\CandidateSelector::ambiguous() To report multiple candidates. - * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. - * @uses \FireHub\Container\Internal\TypeMatcher::matches() To check if a service implementation satisfies a - * contract. + * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation + * satisfies a dependency type. * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the union contains multiple candidates. * @uses \FireHub\Runtime\Arr\Access::keys() To get the union member identifiers. * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first union member identifier. @@ -164,42 +172,84 @@ public function intersection (ReflectionParameter $parameter, ReflectionIntersec foreach (($this->candidates)() as $id => $implementation) { - $matchesAll = true; + if ($this->matchesType($implementation, $parameter, $type)) + $matches[$id] = true; - foreach ($type->getTypes() as $member) { + } - if (!$member instanceof ReflectionNamedType) { + if (Runtime\Arr\Inspection::count($matches) > 1) + $this->ambiguous( + $parameter, + $type, + Runtime\Arr\Access::keys($matches) + ); - $matchesAll = false; + return Runtime\Arr\Access::firstKey($matches); - break; + } - } + /** + * ### Checks whether an implementation satisfies a dependency type + * + * Evaluates named, intersection, and union dependency types recursively. + * + * Intersection types require all members to match, while union types require at least one matching member. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::matches() To check if a service implementation satisfies a + * contract. + * + * @param class-string $implementation

+ * Concrete implementation class. + *

+ * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionType $type

+ * Dependency type. + *

+ * + * @return bool True if the implementation satisfies the dependency type. + */ + public function matchesType (string $implementation, ReflectionParameter $parameter, ReflectionType $type):bool { + + if ($type instanceof ReflectionNamedType) { - $contract = $this->matcher->classId($parameter, $member); + $contract = $this->matcher->classId($parameter, $type); - if ( - $contract === null - || !$this->matcher->matches($implementation, $contract) - ) { + return $contract !== null + && $this->matcher->matches($implementation, $contract); - $matchesAll = false; + } + + if ($type instanceof ReflectionIntersectionType) { - break; + foreach ($type->getTypes() as $member) { - } + if (!$this->matchesType($implementation, $parameter, $member)) + return false; } - if ($matchesAll) - $matches[$id] = true; + return true; } - if (Runtime\Arr\Inspection::count($matches) > 1) - $this->ambiguous($parameter, $type, Runtime\Arr\Access::keys($matches)); + if ($type instanceof ReflectionUnionType) { - return Runtime\Arr\Access::firstKey($matches); + foreach ($type->getTypes() as $member) { + + if ($this->matchesType($implementation, $parameter, $member)) + return true; + + } + + return false; + + } + + return false; } From a01cbeedb7e1937c958eb1d4a8b8acc8289c114d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 16 Oct 2026 08:27:41 +0200 Subject: [PATCH 19/26] Added `ParameterBindingRegistry` and integrated explicit constructor parameter binding into `Container`. - Introduced `ParameterBindingRegistry` to manage explicit constructor parameter bindings, supporting parameter-level configuration for implementation classes. - Updated `Container` to include a `bindParameters` method, enabling external registration of explicit bindings while maintaining compatibility with dynamically constructed and registered services. - Refactored `Resolver` and `Autowirer` to integrate the shared `ParameterBindingRegistry`, improving modularity in dependency resolution workflows. - Enhanced `Autowirer::resolveParameter` to prioritize explicit parameter bindings over auto-wiring or constructor default values. - Updated constructor dependencies for `Resolver`, `Autowirer`, and associated components to support the new registry. - Improved docblocks to document new methods and parameter binding concepts in alignment with FireHub conventions. --- src/Container.php | 49 ++++++++++- src/Internal/Autowirer.php | 29 +++++- src/Internal/ParameterBindingRegistry.php | 102 ++++++++++++++++++++++ src/Resolver.php | 43 ++++----- 4 files changed, 189 insertions(+), 34 deletions(-) create mode 100644 src/Internal/ParameterBindingRegistry.php diff --git a/src/Container.php b/src/Container.php index 145bf33..1fa06af 100644 --- a/src/Container.php +++ b/src/Container.php @@ -18,7 +18,9 @@ use FireHub\Container\Definition\ { Alias, Cls, Factory, Instance }; -use FireHub\Container\Internal\InstanceStore; +use FireHub\Container\Internal\ { + InstanceStore, ParameterBindingRegistry +}; use FireHub\Container\Exception\ContainerFrozenException; use Closure; @@ -52,6 +54,12 @@ final class Container implements ResolverBoundary { */ private readonly Resolver $resolver; + /** + * ### Constructor parameter binding registry + * @since 1.0.0 + */ + private readonly ParameterBindingRegistry $parameters; + /** * ### Container freeze state * @var bool @@ -67,10 +75,12 @@ final class Container implements ResolverBoundary { public function __construct () { $this->registry = new Registry; + $this->parameters = new ParameterBindingRegistry; $this->resolver = new Resolver( $this->registry, - new InstanceStore + new InstanceStore, + $this->parameters ); } @@ -366,6 +376,41 @@ public function alias (string $id, string $target):self { } + /** + * ### Registers explicit constructor parameter bindings + * + * Associates an implementation class with explicitly configured constructor argument values. + * + * Explicit bindings take precedence over automatic dependency resolution and constructor default values. + * + * Bindings are associated with the implementation class rather than a service identifier, allowing them to work + * with both registered and implicitly constructed services. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::assertMutable() To ensure the container accepts configuration changes. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::register() To register parameter bindings. + * + * @param class-string $class

+ * Implementation class. + *

+ * @param array $parameters

+ * Constructor parameter names mapped to explicit values. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * + * @return $this Container instance. + */ + public function bindParameters (string $class, array $parameters):self { + + $this->assertMutable(); + + $this->parameters->register($class, $parameters); + + return $this; + + } + /** * ### Resolves a registered service * diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index 0917a17..a065982 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -48,6 +48,9 @@ * @param \FireHub\Container\Internal\DependencyPlanCache $plans

* Shared dependency plan cache. *

+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $bindings

+ * Shared parameter binding registry. + *

* * @return void */ @@ -56,7 +59,8 @@ public function __construct ( private Closure $has, private TypeMatcher $matcher, private CandidateSelector $selector, - private DependencyPlanCache $plans + private DependencyPlanCache $plans, + private ParameterBindingRegistry $bindings ) {} /** @@ -97,7 +101,11 @@ public function construct (string $class, ResolutionContext $context):object { $arguments = []; foreach ($plan->parameters as $parameter) - $arguments[] = $this->resolveParameter($parameter, $context); + $arguments[] = $this->resolveParameter( + $class, + $parameter, + $context + ); return $metadata->reflection->newInstanceArgs($arguments); @@ -113,8 +121,14 @@ public function construct (string $class, ResolutionContext $context):object { * @uses \FireHub\Container\Internal\CandidateSelector::union() To select a union dependency. * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select an intersection dependency. * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check implicit constructibility. - * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation + * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check if a parameter is bound. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::get() To get a bound parameter value. + * @uses \FireHub\Container\Resolver::resolve() To resolve a service. * + * @param class-string $class

+ * Concrete service class. + *

* @param \FireHub\Container\Internal\ParameterPlan $plan

* Compiled constructor parameter plan. *

@@ -128,7 +142,14 @@ public function construct (string $class, ResolutionContext $context):object { * * @return mixed Resolved argument. */ - private function resolveParameter (ParameterPlan $plan, ResolutionContext $context):mixed { + private function resolveParameter (string $class, ParameterPlan $plan, ResolutionContext $context):mixed { + + $parameter = $plan->parameter; + + $name = $parameter->getName(); + + if ($this->bindings->has($class, $name)) + return $this->bindings->get($class, $name); $id = $plan->id; diff --git a/src/Internal/ParameterBindingRegistry.php b/src/Internal/ParameterBindingRegistry.php new file mode 100644 index 0000000..f3aabd7 --- /dev/null +++ b/src/Internal/ParameterBindingRegistry.php @@ -0,0 +1,102 @@ + + * @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.4 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Runtime; + +/** + * ### Stores explicit constructor parameter bindings + * + * Maintains parameter values associated with individual implementation classes. + * + * Explicit bindings take precedence over automatic dependency resolution and constructor defaults. + * @since 1.0.0 + * + * @internal + */ +final class ParameterBindingRegistry { + + /** + * ### Parameter bindings + * @since 1.0.0 + * + * @var array> + */ + private array $bindings = []; + + /** + * ### Registers constructor parameter bindings + * + * Associates parameter names with explicitly configured values for an implementation class. + * + * Existing bindings for the same class are replaced. + * @since 1.0.0 + * + * @param class-string $class

+ * Implementation class. + *

+ * @param array $parameters

+ * Parameter names mapped to explicit values. + *

+ * + * @return void + */ + public function register (string $class, array $parameters):void { + + $this->bindings[$class] = $parameters; + + } + + /** + * ### Checks whether a parameter has an explicit binding + * @since 1.0.0 + * + * @uses \FireHub\Runtime\Arr\Access::keyExists() To check for a parameter name. + * + * @param class-string $class

+ * Implementation class. + *

+ * @param non-empty-string $parameter

+ * Constructor parameter name. + *

+ * + * @return bool True if a binding exists. + */ + public function has (string $class, string $parameter):bool { + + return isset($this->bindings[$class]) + && Runtime\Arr\Access::keyExists($this->bindings[$class], $parameter); + + } + + /** + * ### Gets an explicitly bound parameter value + * @since 1.0.0 + * + * @param class-string $class

+ * Implementation class. + *

+ * @param non-empty-string $parameter

+ * Constructor parameter name. + *

+ * + * @return mixed Explicit parameter value. + */ + public function get (string $class, string $parameter):mixed { + + return $this->bindings[$class][$parameter]; // @phpstan-ignore-line + + } + +} \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index bb078ca..014c7c1 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -20,7 +20,7 @@ }; use FireHub\Container\Internal\ { Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualResolver, DependencyPlanCache, InstanceStore, - ResolutionContext, ScopeState, TypeMatcher + ParameterBindingRegistry, ResolutionContext, ScopeState, TypeMatcher }; use FireHub\Container\Exception\ { ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException @@ -45,30 +45,12 @@ */ final readonly class Resolver implements ResolverBoundary { - /** - * ### Dependency type matcher - * @since 1.0.0 - */ - private TypeMatcher $matcher; - - /** - * ### Dependency candidate selector - * @since 1.0.0 - */ - private CandidateSelector $selector; - /** * ### Constructor metadata cache * @since 1.0.0 */ private ConstructorMetadataCache $metadata; - /** - * ### Compiled dependency plan cache - * @since 1.0.0 - */ - private DependencyPlanCache $plans; - /** * ### Reusable dependency autowirer * @since 1.0.0 @@ -89,38 +71,43 @@ * @param \FireHub\Container\Internal\InstanceStore $singletons

* Shared singleton instance storage. *

+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $parameters

+ * Parameter binding registry. + *

* * @return void */ public function __construct ( private Registry $registry, - private InstanceStore $singletons + private InstanceStore $singletons, + ParameterBindingRegistry $parameters ) { $this->metadata = new ConstructorMetadataCache; - $this->matcher = new TypeMatcher( + $matcher = new TypeMatcher( $this->metadata ); - $this->selector = new CandidateSelector( - $this->matcher, + $selector = new CandidateSelector( + $matcher, fn (string $id):bool => $this->has($id), fn ():iterable => $this->registry->implementations() ); - $this->plans = new DependencyPlanCache( + $plans = new DependencyPlanCache( $this->metadata, - $this->matcher + $matcher ); $this->autowirer = new Autowirer( fn (string $id, ResolutionContext $context):object => $this->resolve($id, $context), fn (string $id):bool => $this->has($id), - $this->matcher, - $this->selector, - $this->plans + $matcher, + $selector, + $plans, + $parameters ); } From f30c6a8e474b775892cb2600dce9f3348ce7d294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Fri, 16 Oct 2026 12:07:20 +0200 Subject: [PATCH 20/26] Fix: Reorder dependencies in composer.json files for consistent formatting Reordered "the-firehub-project/runtime" and other dependencies in multiple composer.json files across Kernel, Container, and Advanced-DataStructures-Capability directories to ensure consistent dependency order. No functional changes introduced. --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 20e7ad2..cb7ba03 100644 --- a/composer.json +++ b/composer.json @@ -8,8 +8,8 @@ "require": { "php": "^8.5", - "the-firehub-project/runtime": ">=0.1.0 <1.0.0", - "the-firehub-project/core-standard": "dev-develop" + "the-firehub-project/core-standard": "dev-develop", + "the-firehub-project/runtime": ">=0.1.0 <1.0.0" }, "require-dev": { "phpstan/phpstan": "^2.0", From d3962e53804ea2f058b48f99450ba99c06428fc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Mon, 19 Oct 2026 14:27:41 +0200 Subject: [PATCH 21/26] Refactored `Scope` and `Container` to align boundaries and enhance documentation. - Updated `Scope` to implement `ScopeBoundary` instead of `ResolverBoundary`, reflecting corrected boundary alignment. - Adjusted `Container` to implement `ContainerBoundary` and updated relevant imports for clarity. - Enhanced PHPDoc blocks across `Container` to include detailed usage examples, improving developer understanding and usability. - Added inline code examples to multiple service registration methods (`bind`, `singleton`, `scoped`, etc.) to demonstrate usage patterns in alignment with `FireHub` conventions. - Improved readability and consistency of code documentation without introducing any functional changes. --- src/Container.php | 214 +++++++++++++++++++++++++++++++++++++++++++--- src/Scope.php | 4 +- 2 files changed, 206 insertions(+), 12 deletions(-) diff --git a/src/Container.php b/src/Container.php index 1fa06af..c2edc62 100644 --- a/src/Container.php +++ b/src/Container.php @@ -13,7 +13,9 @@ namespace FireHub\Container; -use FireHub\Core\Boundary\Resolver as ResolverBoundary; +use FireHub\Core\Boundary\ { + Container\Scope as ScopeBoundary, Container as ContainerBoundary +}; use FireHub\Core\Meta\Enum\Container\Lifetime; use FireHub\Container\Definition\ { Alias, Cls, Factory, Instance @@ -40,7 +42,7 @@ * resolution scope. * @since 1.0.0 */ -final class Container implements ResolverBoundary { +final class Container implements ContainerBoundary { /** * ### Service registry @@ -86,19 +88,19 @@ public function __construct () { } /** - * ### Creates a dependency injection scope + * {@inheritDoc} * - * Creates an independent resolution scope associated with this container. + * + * use FireHub\Container\Container; * - * Each scope maintains its own cache of scoped service instances while sharing singleton instances and service - * definitions with the parent container. + * $container = new Container; * - * Scopes must be explicitly closed when they are no longer needed. - * @since 1.0.0 + * $scope = $container->createScope(); + * * - * @return \FireHub\Container\Scope Created resolution scope. + * @since 1.0.0 */ - public function createScope ():Scope { + public function createScope ():ScopeBoundary { return new Scope($this->resolver); @@ -110,6 +112,15 @@ public function createScope ():Scope { * Associates a service identifier with its definition. * * Registration is only permitted while the container is not frozen. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->register(Test::class, new Cls(Test::class)); + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::assertMutable() To check if the container is frozen. @@ -145,6 +156,15 @@ public function register (string $id, Definition $definition):self { * Associates a service identifier with a concrete implementation class. * * The service is instantiated lazily according to the specified lifetime. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->bind(Test::class, Test::class); + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::register() To register a service definition. @@ -181,6 +201,20 @@ public function bind (string $id, string $class, Lifetime $lifetime = Lifetime:: * Registers a class-based service instantiated once per container. * * When no implementation is provided, the identifier itself is used as the implementation class. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $first = $container->get(Test::class); + * $second = $container->get(Test::class); + * + * // $first === $second + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::bind() To register a class-based service. @@ -215,6 +249,26 @@ public function singleton (string $id, ?string $class = null):self { * Registers a class-based service instantiated once per resolution scope. * * Each independent scope receives its own service instance. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->scoped(Test::class); + * + * $firstScope = $container->createScope(); + * $secondScope = $container->createScope(); + * + * $first = $firstScope->get(Test::class); + * $second = $secondScope->get(Test::class); + * + * // $first !== $second + * + * $firstScope->close(); + * $secondScope->close(); + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::bind() To register a class-based service. @@ -247,6 +301,19 @@ public function scoped (string $id, ?string $class = null):self { * ### Registers a transient service * * Registers a class-based service instantiated on each resolution. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->transient(Test::class); + * + * $first = $container->get(Test::class); + * $second = $container->get(Test::class); + * + * // $first !== $second + * * @since 1.0.0 * * @uses \FireHub\Container\Container::bind() To register a class-based service. @@ -280,6 +347,35 @@ public function transient (string $id, ?string $class = null):self { * Registers a factory responsible for creating the requested service. * * The factory receives a contextual resolver to resolve dependencies within the current resolution context. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->factory( + * Test::class, + * static fn ($resolver):Test => new Test + * ); + * + * $service = $container->get(Test::class); + * + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Logger::class); + * + * $container->factory( + * Test::class, + * static fn ($resolver):Test => new Test( + * $resolver->get(Logger::class) + * ) + * ); + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::register() To register a service definition. @@ -316,6 +412,21 @@ public function factory (string $id, Closure $factory, Lifetime $lifetime = Life * Registers an already constructed object as a singleton service. * * The provided instance is returned directly during resolution. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $instance = new Test; + * + * $container->instance(Test::class, $instance); + * + * $service = $container->get(Test::class); + * + * // $service === $instance + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::register() To register a service definition. @@ -349,6 +460,18 @@ public function instance (string $id, object $instance):self { * Associates an additional identifier with another registered service. * * Aliases preserve the lifetime and identity of the target service. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * $container->alias(TestInterface::class, Test::class); + * + * $service = $container->get(TestInterface::class); + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::register() To register a service definition. @@ -385,6 +508,23 @@ public function alias (string $id, string $target):self { * * Bindings are associated with the implementation class rather than a service identifier, allowing them to work * with both registered and implicitly constructed services. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->bindParameters( + * Test::class, + * [ + * 'name' => 'FireHub', + * 'enabled' => true + * ] + * ); + * + * $service = $container->get(Test::class); + * + * * @since 1.0.0 * * @uses \FireHub\Container\Container::assertMutable() To ensure the container accepts configuration changes. @@ -419,6 +559,17 @@ public function bindParameters (string $class, array $parameters):self { * Singleton services are shared across the container. Transient services are constructed on each resolution. * * Scoped services cannot be resolved without an active scope. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $service = $container->get(Test::class); + * + * * @since 1.0.0 * * @template TService of object @@ -450,6 +601,19 @@ public function get (string $id):object { * Determines whether the registry contains a definition for the specified service identifier. * * This operation does not instantiate the service. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $registered = $container->has(Test::class); + * + * // true + * + * * @since 1.0.0 * * @param class-string $id

@@ -472,6 +636,23 @@ public function has (string $id):bool { * Existing service definitions remain available for resolution. * * Calling this method multiple times has no additional effect. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $container->freeze(); + * + * // Existing services remain available. + * $service = $container->get(Test::class); + * + * // Further registrations are prohibited. + * // $container->singleton(AnotherTest::class); + * + * * @since 1.0.0 * * @return void @@ -484,6 +665,19 @@ public function freeze ():void { /** * ### Checks whether the container is frozen + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->isFrozen(); // false + * + * $container->freeze(); + * + * $container->isFrozen(); // true + * + * * @since 1.0.0 * * @return bool True if frozen, false otherwise. diff --git a/src/Scope.php b/src/Scope.php index fa429ae..81a6fda 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -13,7 +13,7 @@ namespace FireHub\Container; -use FireHub\Core\Boundary\Resolver as ResolverBoundary; +use FireHub\Core\Boundary\Container\Scope as ScopeBoundary; use FireHub\Container\Internal\ { InstanceStore, ScopeState }; @@ -36,7 +36,7 @@ * Scopes do not support nested scope creation. * @since 1.0.0 */ -final readonly class Scope implements ResolverBoundary { +final readonly class Scope implements ScopeBoundary { /** * ### Scoped instance store From 48deda4f321b3095b8f60a314b22b8882325c1b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Tue, 20 Oct 2026 13:31:40 +0200 Subject: [PATCH 22/26] Added `Registrar` implementation to `Container` and refactored PHPDoc for improved consistency. - Updated `Container` to implement the `Registrar` interface, aligning it with extended boundaries for service registration. - Simplified PHPDoc blocks for `bind`, `factory`, `instance`, and `alias` methods by replacing detailed annotations with `{@inheritDoc}` for consistency and maintainability. - Removed redundant parameter templates and return annotations, leveraging inherited documentation to reduce duplication. - Refined imports by adding `Registrar` reference and reordering for clarity. --- src/Container.php | 78 ++++------------------------------------------- 1 file changed, 6 insertions(+), 72 deletions(-) diff --git a/src/Container.php b/src/Container.php index c2edc62..30ad25d 100644 --- a/src/Container.php +++ b/src/Container.php @@ -14,7 +14,7 @@ namespace FireHub\Container; use FireHub\Core\Boundary\ { - Container\Scope as ScopeBoundary, Container as ContainerBoundary + Container\Registrar, Container\Scope as ScopeBoundary, Container as ContainerBoundary }; use FireHub\Core\Meta\Enum\Container\Lifetime; use FireHub\Container\Definition\ { @@ -42,7 +42,7 @@ * resolution scope. * @since 1.0.0 */ -final class Container implements ContainerBoundary { +final class Container implements ContainerBoundary, Registrar { /** * ### Service registry @@ -151,11 +151,7 @@ public function register (string $id, Definition $definition):self { } /** - * ### Registers a class-based service - * - * Associates a service identifier with a concrete implementation class. - * - * The service is instantiated lazily according to the specified lifetime. + * {@inheritDoc} * * * use FireHub\Container\Container; @@ -169,22 +165,8 @@ public function register (string $id, Definition $definition):self { * * @uses \FireHub\Container\Container::register() To register a service definition. * - * @template TService of object - * - * @param class-string $id

- * Service identifier. - *

- * @param class-string $class

- * Concrete implementation class. - *

- * @param \FireHub\Core\Meta\Enum\Container\Lifetime $lifetime [optional]

- * Service lifetime. - *

- * * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. - * - * @return $this Container instance. */ public function bind (string $id, string $class, Lifetime $lifetime = Lifetime::TRANSIENT):self { @@ -342,11 +324,7 @@ public function transient (string $id, ?string $class = null):self { } /** - * ### Registers a factory-based service - * - * Registers a factory responsible for creating the requested service. - * - * The factory receives a contextual resolver to resolve dependencies within the current resolution context. + * {@inheritDoc} * * * use FireHub\Container\Container; @@ -380,22 +358,8 @@ public function transient (string $id, ?string $class = null):self { * * @uses \FireHub\Container\Container::register() To register a service definition. * - * @template TService of object - * - * @param class-string $id

- * Service identifier. - *

- * @param Closure(\FireHub\Core\Boundary\Resolver):TService $factory

- * Service factory. - *

- * @param \FireHub\Core\Meta\Enum\Container\Lifetime $lifetime [optional]

- * Service lifetime. - *

- * * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. - * - * @return $this Container instance. */ public function factory (string $id, Closure $factory, Lifetime $lifetime = Lifetime::TRANSIENT):self { @@ -407,11 +371,7 @@ public function factory (string $id, Closure $factory, Lifetime $lifetime = Life } /** - * ### Registers an existing service instance - * - * Registers an already constructed object as a singleton service. - * - * The provided instance is returned directly during resolution. + * {@inheritDoc} * * * use FireHub\Container\Container; @@ -431,19 +391,8 @@ public function factory (string $id, Closure $factory, Lifetime $lifetime = Life * * @uses \FireHub\Container\Container::register() To register a service definition. * - * @template TService of object - * - * @param class-string $id

- * Service identifier. - *

- * @param TService $instance

- * Existing service instance. - *

- * * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. - * - * @return $this Container instance. */ public function instance (string $id, object $instance):self { @@ -455,11 +404,7 @@ public function instance (string $id, object $instance):self { } /** - * ### Registers a service alias - * - * Associates an additional identifier with another registered service. - * - * Aliases preserve the lifetime and identity of the target service. + * {@inheritDoc} * * * use FireHub\Container\Container; @@ -476,19 +421,8 @@ public function instance (string $id, object $instance):self { * * @uses \FireHub\Container\Container::register() To register a service definition. * - * @template TService of object - * - * @param class-string $id

- * Alias identifier. - *

- * @param class-string $target

- * Target service identifier. - *

- * * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. - * - * @return $this Container instance. */ public function alias (string $id, string $target):self { From 4b52818802d1a480fd73f4640c361f28a5ae6603 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Tue, 20 Oct 2026 15:38:30 +0200 Subject: [PATCH 23/26] Enhance ScopeState and Scope classes to improve state management and exception handling --- src/Internal/ScopeState.php | 2 +- src/Scope.php | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Internal/ScopeState.php b/src/Internal/ScopeState.php index 621ef95..4f77c09 100644 --- a/src/Internal/ScopeState.php +++ b/src/Internal/ScopeState.php @@ -145,7 +145,7 @@ public function beginRun ():void { $this->assertOpen(); - if ($this->running) + if ($this->running || $this->active > 0) throw new ScopeActiveException; $this->running = true; diff --git a/src/Scope.php b/src/Scope.php index 81a6fda..6ddf816 100644 --- a/src/Scope.php +++ b/src/Scope.php @@ -172,9 +172,15 @@ public function run (Closure $callback):mixed { } finally { - $this->state->endRun(); + try { - $this->close(); + $this->state->endRun(); + + } finally { + + $this->close(); + + } } From 94e4760943c6079ab9e63ccb6f0173e7d454dfff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 21 Oct 2026 09:56:07 +0200 Subject: [PATCH 24/26] Add contextual dependency binding support with registry and selector --- src/Container.php | 54 ++++- .../DuplicateContextualBindingException.php | 29 +++ src/Internal/Autowirer.php | 199 ++++++++++++++---- src/Internal/ContextualBindingRegistry.php | 113 ++++++++++ src/Internal/ContextualCandidateSelector.php | 155 ++++++++++++++ src/Resolver.php | 17 +- 6 files changed, 517 insertions(+), 50 deletions(-) create mode 100644 src/Exception/DuplicateContextualBindingException.php create mode 100644 src/Internal/ContextualBindingRegistry.php create mode 100644 src/Internal/ContextualCandidateSelector.php diff --git a/src/Container.php b/src/Container.php index 30ad25d..0f227c5 100644 --- a/src/Container.php +++ b/src/Container.php @@ -21,7 +21,7 @@ Alias, Cls, Factory, Instance }; use FireHub\Container\Internal\ { - InstanceStore, ParameterBindingRegistry + ContextualBindingRegistry, InstanceStore, ParameterBindingRegistry }; use FireHub\Container\Exception\ContainerFrozenException; use Closure; @@ -62,6 +62,12 @@ final class Container implements ContainerBoundary, Registrar { */ private readonly ParameterBindingRegistry $parameters; + /** + * ### Contextual dependency binding registry + * @since 1.0.0 + */ + private readonly ContextualBindingRegistry $contextual; + /** * ### Container freeze state * @var bool @@ -78,11 +84,13 @@ public function __construct () { $this->registry = new Registry; $this->parameters = new ParameterBindingRegistry; + $this->contextual = new ContextualBindingRegistry; $this->resolver = new Resolver( $this->registry, new InstanceStore, - $this->parameters + $this->parameters, + $this->contextual ); } @@ -150,6 +158,48 @@ public function register (string $id, Definition $definition):self { } + /** + * ### Registers a contextual dependency binding + * + * Associates a specific consumer class with an implementation to be used when resolving the specified dependency. + * + * Contextual bindings apply only to the exact consumer class and do not affect global service registrations. + * + * The target service retains its configured lifetime. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::assertMutable() + * @uses \FireHub\Container\Internal\ContextualBindingRegistry::register() + * + * @param class-string $consumer

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

+ * @param class-string $implementation

+ * Target service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateContextualBindingException If the binding already exists. + * + * @return $this Container instance. + */ + public function bindContextual (string $consumer, string $dependency, string $implementation):self { + + $this->assertMutable(); + + $this->contextual->register( + $consumer, + $dependency, + $implementation + ); + + return $this; + + } + /** * {@inheritDoc} * diff --git a/src/Exception/DuplicateContextualBindingException.php b/src/Exception/DuplicateContextualBindingException.php new file mode 100644 index 0000000..0309a6d --- /dev/null +++ b/src/Exception/DuplicateContextualBindingException.php @@ -0,0 +1,29 @@ + + * @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 Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a contextual dependency binding is already registered + * + * Indicates that a contextual dependency registration would violate the registry invariant requiring each + * consumer and dependency combination to have exactly one associated implementation. + * @since 1.0.0 + */ +final class DuplicateContextualBindingException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'A contextual binding for the specified consumer and dependency is already registered'; + +} \ No newline at end of file diff --git a/src/Internal/Autowirer.php b/src/Internal/Autowirer.php index a065982..14bf5d0 100644 --- a/src/Internal/Autowirer.php +++ b/src/Internal/Autowirer.php @@ -21,8 +21,10 @@ * * Executes cached dependency construction plans to instantiate services and resolve their constructor dependencies. * - * Static parameter analysis is performed by the dependency plan cache, while service availability and candidate - * selection are evaluated dynamically within the active resolution context. + * Static parameter analysis is performed by the dependency plan cache, while service availability, contextual + * bindings, and candidate selection are evaluated dynamically within the active resolution context. + * + * Explicit parameter bindings take precedence over contextual bindings and automatic dependency resolution. * @since 1.0.0 * * @internal @@ -51,6 +53,9 @@ * @param \FireHub\Container\Internal\ParameterBindingRegistry $bindings

* Shared parameter binding registry. *

+ * @param \FireHub\Container\Internal\ContextualCandidateSelector $contextual_selector

+ * Shared contextual dependency selector. + *

* * @return void */ @@ -60,7 +65,8 @@ public function __construct ( private TypeMatcher $matcher, private CandidateSelector $selector, private DependencyPlanCache $plans, - private ParameterBindingRegistry $bindings + private ParameterBindingRegistry $bindings, + private ContextualCandidateSelector $contextual_selector ) {} /** @@ -114,17 +120,22 @@ public function construct (string $class, ResolutionContext $context):object { /** * ### Resolves a compiled constructor parameter * - * Executes the precompiled parameter plan while evaluating dependency availability and composite candidate - * selection against the current service registrations. + * Resolves constructor arguments according to the following precedence: + * + * 1. Explicit parameter binding. + * 2. Contextual dependency binding. + * 3. Registered or implicitly constructible dependency. + * 4. Constructor default value. + * + * Contextual bindings apply only to named dependency types and the exact consumer implementation class. * @since 1.0.0 * - * @uses \FireHub\Container\Internal\CandidateSelector::union() To select a union dependency. - * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select an intersection dependency. - * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check implicit constructibility. - * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation. - * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check if a parameter is bound. - * @uses \FireHub\Container\Internal\ParameterBindingRegistry::get() To get a bound parameter value. - * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check explicit parameter bindings. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::get() To retrieve explicitly bound values. + * @uses \FireHub\Container\Internal\ContextualBindingRegistry::find() To find contextual dependency bindings. + * @uses \FireHub\Container\Internal\Autowirer::selectDependency() To select a dependency identifier. + * @uses \FireHub\Container\Internal\Autowirer::resolveDependency() To resolve selected dependencies. + * @uses \FireHub\Container\Internal\Autowirer::unresolvedParameter() To create resolution exceptions. * * @param class-string $class

* Concrete service class. @@ -136,71 +147,175 @@ public function construct (string $class, ResolutionContext $context):object { * Active resolution context. *

* - * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot - * be resolved. + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the dependency cannot be resolved. * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. * * @return mixed Resolved argument. */ private function resolveParameter (string $class, ParameterPlan $plan, ResolutionContext $context):mixed { - $parameter = $plan->parameter; - - $name = $parameter->getName(); + $name = $plan->parameter->getName(); if ($this->bindings->has($class, $name)) return $this->bindings->get($class, $name); - $id = $plan->id; + $id = $this->contextual_selector->select($class, $plan); - if ($plan->composite instanceof ReflectionUnionType) - $id = $this->selector->union( - $plan->parameter, - $plan->composite - ); + if ($id !== null) + return $this->resolveDependency($id, $plan, $context, true); - elseif ($plan->composite instanceof ReflectionIntersectionType) - $id = $this->selector->intersection( - $plan->parameter, - $plan->composite - ); + $id = $this->selectDependency($plan); if ($id !== null && ( ($this->has)($id) || $this->matcher->isConstructible($id) - )) { + ) + ) return $this->resolveDependency($id, $plan, $context); + + if ($plan->hasDefault) + return $plan->default; + + throw $this->unresolvedParameter($plan); - $instance = ($this->resolve)($id, $context); + } - if ( - $plan->composite !== null - && !$this->selector->matchesType( - $instance::class, + /** + * ### Selects a dependency identifier + * + * Determines the service identifier required by a constructor parameter. + * + * Named dependency types use their precompiled identifiers. Union and intersection dependency types are + * evaluated dynamically against the currently registered service implementations. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\CandidateSelector::union() To select a union dependency. + * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select an intersection dependency. + * + * @param \FireHub\Container\Internal\ParameterPlan $plan

+ * Compiled constructor parameter plan. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the union contains an unsupported + * intersection member. + * + * @return null|class-string Selected dependency identifier. + */ + private function selectDependency (ParameterPlan $plan):?string { + + return match (true) { + $plan->composite instanceof ReflectionUnionType => + $this->selector->union( $plan->parameter, $plan->composite - ) + ), + $plan->composite instanceof ReflectionIntersectionType => + $this->selector->intersection( + $plan->parameter, + $plan->composite + ), + default => $plan->id + }; + + } + + /** + * ### Resolves a selected dependency + * + * Resolves the selected service identifier within the active resolution context. + * + * For union and intersection dependency types, validates that the resolved instance satisfies the complete + * declared parameter type. + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::resolve() To resolve the selected dependency. + * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To validate composite dependency types. + * + * @param class-string $id

+ * Selected dependency identifier. + *

+ * @param \FireHub\Container\Internal\ParameterPlan $plan

+ * Compiled constructor parameter plan. + *

+ * @param \FireHub\Container\Internal\ResolutionContext $context

+ * Active resolution context. + *

+ * @param bool $contextual [optional]

+ * Whether the dependency was selected through contextual binding. + *

+ * + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the resolved dependency does not satisfy + * its declared composite type. + * + * @return object Resolved dependency. + */ + private function resolveDependency (string $id, ParameterPlan $plan, ResolutionContext $context, bool $contextual = false):object { + + $instance = ($this->resolve)($id, $context); + + if ( + $plan->composite !== null + && !$this->selector->matchesType( + $instance::class, + $plan->parameter, + $plan->composite ) + ) throw new ServiceInstantiationException( + context: [ + 'class' => $plan->parameter->getDeclaringClass()?->getName(), + 'parameter' => $plan->parameter->getName(), + 'type' => (string)$plan->composite, + 'instance' => $instance::class, + ] + ); + + if ( + $contextual + && $plan->composite === null + && $plan->id !== null + ) { + + $expected = $plan->id; + + if (!$instance instanceof $expected) throw new ServiceInstantiationException( context: [ 'class' => $plan->parameter->getDeclaringClass()?->getName(), 'parameter' => $plan->parameter->getName(), - 'type' => (string)$plan->composite, - 'instance' => $instance::class + 'type' => $expected, + 'implementation' => $id, + 'instance' => $instance::class, ] ); - return $instance; - } - if ($plan->hasDefault) - return $plan->default; + return $instance; + + } + + /** + * ### Creates an unresolved parameter exception + * + * Creates an exception containing diagnostic information about a constructor parameter that could not be + * resolved through explicit bindings, contextual bindings, registered services, implicit construction, or + * default values. + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ParameterPlan $plan

+ * Compiled constructor parameter plan. + *

+ * + * @return \FireHub\Container\Exception\ServiceInstantiationException Parameter resolution exception. + */ + private function unresolvedParameter (ParameterPlan $plan):ServiceInstantiationException { $parameter = $plan->parameter; $type = $parameter->getType(); - throw new ServiceInstantiationException( + return new ServiceInstantiationException( context: [ 'class' => $parameter->getDeclaringClass()?->getName(), 'parameter' => $parameter->getName(), diff --git a/src/Internal/ContextualBindingRegistry.php b/src/Internal/ContextualBindingRegistry.php new file mode 100644 index 0000000..03e69db --- /dev/null +++ b/src/Internal/ContextualBindingRegistry.php @@ -0,0 +1,113 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\DuplicateContextualBindingException; + +/** + * ### Stores contextual dependency bindings + * + * Associates consumer implementation classes with specific dependency contracts and their target service identifiers. + * + * Contextual bindings do not replace global definitions and are evaluated dynamically during constructor autowiring. + * @since 1.0.0 + * + * @internal + */ +final class ContextualBindingRegistry { + + /** + * ### Contextual dependency bindings + * @since 1.0.0 + * + * @var array> + */ + private array $bindings = []; + + /** + * ### Registers a contextual dependency binding + * + * Associates a consumer class and required dependency with a target service identifier. + * + * Duplicate registrations for the same consumer and dependency are rejected. + * @since 1.0.0 + * + * @param class-string $consumer

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

+ * @param class-string $implementation

+ * Target service identifier. + *

+ * + * @throws DuplicateContextualBindingException If the contextual binding already exists. + * + * @return void + */ + public function register (string $consumer, string $dependency, string $implementation):void { + + if (isset($this->bindings[$consumer][$dependency])) + throw new DuplicateContextualBindingException; + + $this->bindings[$consumer][$dependency] = $implementation; + + } + + /** + * ### Finds a contextual dependency binding + * + * Returns the target service identifier associated with the specified consumer and dependency. + * + * Returns null when no contextual binding exists. + * @since 1.0.0 + * + * @param class-string $consumer

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

+ * + * @return null|class-string Target service identifier, or null if not found. + */ + public function find (string $consumer, string $dependency):?string { + + return $this->bindings[$consumer][$dependency] ?? null; + + } + + /** + * ### Checks whether a contextual binding exists + * + * Checks whether the specified consumer has an explicitly registered dependency override. + * @since 1.0.0 + * + * @param class-string $consumer

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

+ * + * @return bool True if the binding exists, false otherwise. + */ + public function has (string $consumer, string $dependency):bool { + + return isset($this->bindings[$consumer][$dependency]); + + } + +} \ No newline at end of file diff --git a/src/Internal/ContextualCandidateSelector.php b/src/Internal/ContextualCandidateSelector.php new file mode 100644 index 0000000..79ba54e --- /dev/null +++ b/src/Internal/ContextualCandidateSelector.php @@ -0,0 +1,155 @@ + + * @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 Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\AmbiguousDependencyException; +use FireHub\Runtime; +use ReflectionIntersectionType, ReflectionNamedType, ReflectionType, ReflectionUnionType; + +/** + * ### Selects contextual dependency implementations + * + * Evaluates contextual dependency bindings against constructor parameter types. + * + * Supports named, union, intersection, and disjunctive normal form dependency types without modifying standard + * dependency candidate selection. + * + * Contextual bindings are matched against the exact consumer class. Multiple bindings targeting the same service + * identifier are treated as a single candidate, while distinct candidates are considered ambiguous. + * + * Candidate selection does not instantiate services or validate their runtime types. + * @since 1.0.0 + * + * @internal + */ +final readonly class ContextualCandidateSelector { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ContextualBindingRegistry $bindings

+ * Shared contextual binding registry. + *

+ * + * @return void + */ + public function __construct ( + private ContextualBindingRegistry $bindings + ) {} + + /** + * ### Selects a contextual dependency implementation + * + * Collects contextual bindings associated with the declared parameter types and selects a unique implementation. + * + * Returns null when no contextual binding exists. Multiple distinct implementations produce an ambiguity + * exception, regardless of their registration order. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ContextualCandidateSelector::collect() To collect contextual candidates. + * @uses \FireHub\Runtime\Arr\Inspection::count() To check if multiple candidates are eligible. + * + * @param class-string $consumer

+ * Concrete consumer implementation class. + *

+ * @param \FireHub\Container\Internal\ParameterPlan $plan

+ * Compiled constructor parameter plan. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple distinct contextual + * implementations are eligible. + * + * @return null|class-string Selected contextual implementation identifier. + */ + public function select (string $consumer, ParameterPlan $plan):?string { + + $type = $plan->parameter->getType(); + + if ($type === null) + return null; + + $candidates = $this->collect($consumer, $type); + + return match (Runtime\Arr\Inspection::count($candidates)) { + 0 => null, + 1 => $candidates[0], // @phpstan-ignore offsetAccess.notFound + default => throw new AmbiguousDependencyException( + context: [ + 'class' => $consumer, + 'parameter' => $plan->parameter->getName(), + 'type' => (string)$type, + 'candidates' => $candidates, + ] + ) + }; + + } + + /** + * ### Collects contextual dependency candidates + * + * Traverses named, union, and intersection types recursively, including nested intersection types within + * disjunctive normal form declarations. + * + * Duplicate implementation identifiers are removed while preserving their first occurrence. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ContextualBindingRegistry::find() To find contextual bindings. + * @uses \FireHub\Runtime\Arr\Access::values() To remove duplicate implementation identifiers. + * + * @param class-string $consumer

+ * Concrete consumer implementation class. + *

+ * @param ReflectionType $type

+ * Declared constructor parameter type. + *

+ * + * @return list Distinct contextual implementation identifiers. + */ + private function collect (string $consumer, ReflectionType $type):array { + + if ($type instanceof ReflectionNamedType) { + + if ($type->isBuiltin()) + return []; + + $implementation = $this->bindings->find( + $consumer, + $type->getName() // @phpstan-ignore argument.type + ); + + return $implementation !== null + ? [$implementation] + : []; + + } + + if ( + !$type instanceof ReflectionUnionType + && !$type instanceof ReflectionIntersectionType + ) + return []; + + $candidates = []; + + foreach ($type->getTypes() as $member) + foreach ($this->collect($consumer, $member) as $implementation) + $candidates[$implementation] = $implementation; + + return Runtime\Arr\Access::values($candidates); + + } + +} \ No newline at end of file diff --git a/src/Resolver.php b/src/Resolver.php index 014c7c1..27e8a05 100644 --- a/src/Resolver.php +++ b/src/Resolver.php @@ -19,8 +19,9 @@ Alias, Cls, Factory, Instance, Service }; use FireHub\Container\Internal\ { - Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualResolver, DependencyPlanCache, InstanceStore, - ParameterBindingRegistry, ResolutionContext, ScopeState, TypeMatcher + Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualBindingRegistry, ContextualCandidateSelector, + ContextualResolver, DependencyPlanCache, InstanceStore, ParameterBindingRegistry, ResolutionContext, ScopeState, + TypeMatcher }; use FireHub\Container\Exception\ { ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException @@ -74,13 +75,17 @@ * @param \FireHub\Container\Internal\ParameterBindingRegistry $parameters

* Parameter binding registry. *

+ * @param \FireHub\Container\Internal\ContextualBindingRegistry $contextual

+ * Contextual binding registry. + *

* * @return void */ public function __construct ( private Registry $registry, private InstanceStore $singletons, - ParameterBindingRegistry $parameters + ParameterBindingRegistry $parameters, + ContextualBindingRegistry $contextual ) { $this->metadata = new ConstructorMetadataCache; @@ -101,13 +106,13 @@ public function __construct ( ); $this->autowirer = new Autowirer( - fn (string $id, ResolutionContext $context):object => - $this->resolve($id, $context), + fn (string $id, ResolutionContext $context):object => $this->resolve($id, $context), fn (string $id):bool => $this->has($id), $matcher, $selector, $plans, - $parameters + $parameters, + new ContextualCandidateSelector($contextual) ); } From 5bafef8363e8768c11da7f562db426ac5d4063b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= Date: Wed, 21 Oct 2026 10:54:44 +0200 Subject: [PATCH 25/26] Update changelog and README for version 0.1.0 release --- CHANGELOG.md | 7 ++++++- README.md | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 751d2d4..6f7a7a4 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/Container/pull/1 \ No newline at end of file diff --git a/README.md b/README.md index 6bcfe06..5be8a96 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ /> version From c69ff1ebf4d863c664551c321089e33e9f3542ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Danijel=20Gali=C4=87?= <56673130+dieselxxx@users.noreply.github.com> Date: Wed, 21 Oct 2026 10:55:28 +0200 Subject: [PATCH 26/26] Update README.md to reflect current repository status Removed outdated badges and updated repository purpose details. --- README.md | 86 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 5be8a96..0805643 100644 --- a/README.md +++ b/README.md @@ -22,10 +22,6 @@ /> -[![PHPStan](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPStan.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPStan.yml) -[![PHPUnit](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPUnit.yml/badge.svg?branch=develop)](https://github.com/The-FireHub-Project/Container/actions/workflows/PHPUnit.yml) -[![Codecov](https://codecov.io/gh/The-FireHub-Project/Container/branch/develop/graph/badge.svg?token=XW2YEONF51)](https://app.codecov.io/gh/The-FireHub-Project/Container/tree/develop) -

FireHub Icon Development Branch +## FireHub Icon Repository Purpose + +FireHub Container is the dependency injection and service resolution component of the FireHub ecosystem. + +Built on the contracts provided by FireHub Core, it supplies the mechanisms required to register, construct, resolve, and manage application services. + +The purpose of this repository is to centralize dependency management while preserving separation of concerns between service definitions, runtime implementation, and application execution. -⚠️ **This is the `develop` branch** +The Container architecture is designed to support: -- Unstable -- APIs may change without notice -- Not intended for production use +- Service registration and dependency resolution +- Interface-to-implementation bindings +- Instance, factory, and class-based service definitions +- Singleton and transient service lifetimes +- Dependency graph resolution and circular dependency detection +- Immutable service registrations after container construction +- Future support for autowiring and execution-scoped services -## FireHub Icon Branch Purpose +FireHub Container follows the architectural principles of The FireHub Project by separating responsibilities between contracts, dependency management, and application lifecycle orchestration. -The `develop` branch is the **primary integration branch** for all ongoing development. +The Container does not control application execution or kernel lifecycle transitions. Instead, it provides dependency management services that can be integrated into applications through the appropriate bootstrap and composition mechanisms. -It serves as the staging area where: -- Feature branches are merged -- Bug fixes are integrated -- Experimental work is stabilized -- Code is prepared for upcoming releases +## FireHub Icon Security -All **release branches** are created **from `develop`**. +For security reporting and guidelines, please refer to our [SECURITY.md](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file. -## Stability Guarantee +## FireHub Icon Community -❌ No backward compatibility guarantee -❌ APIs may change without notice -❌ Behavior may be incomplete or inconsistent -❌ Breaking changes are expected +![GitHub watchers](https://img.shields.io/github/watchers/The-FireHub-Project/Container?style=social) +![GitHub Repo stars](https://img.shields.io/github/stars/The-FireHub-Project/Container?style=social) +![GitHub forks](https://img.shields.io/github/forks/The-FireHub-Project/Container?style=social) -This branch is intended **only for contributors and advanced testers**. +- Be sure to review the [Code of Conduct](https://github.com/The-FireHub-Project/.github/blob/master/.github/CODE_OF_CONDUCT.md). +- Read a [Contributing](https://github.com/The-FireHub-Project/.github/blob/master/.github/CONTRIBUTING.md) file to see how you can contribute to this project. +- Read our [Security policy](https://github.com/The-FireHub-Project/.github/blob/master/.github/SECURITY.md) file to see how you can report a vulnerability. +- Review the project [Governance model](https://github.com/The-FireHub-Project/.github/blob/master/.github/GOVERNANCE.md), including roles, decision-making, RFC process, and releases. -## FireHub Icon Composer Usage (Not Recommended) +## FireHub Icon Support + +

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

-For internal testing only: +For help with FireHub products, please refer to the following resources: -```json -{ - "require": { - "the-firehub-project/container": "dev-develop" - } -} -``` -⚠️ Never use dev-develop in production. +- **General questions & discussions**: [GitHub Discussions](https://github.com/orgs/The-FireHub-Project/discussions) +- **Bug reports & issues**: [Support](https://github.com/The-FireHub-Project/Container/issues) +- **Documentation & guides**: [FireHub Docs](https://the-firehub-project.github.io) +- **Commercial support** (Pro/Enterprise): contact your account manager ## FireHub Icon Authors and Contributors @@ -134,4 +152,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).