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