diff --git a/CHANGELOG.md b/CHANGELOG.md index 751d2d4..6f7a7a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,4 +3,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). \ No newline at end of file +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [v0.1.0] – 2026-10-21 + +### Added +* Implement initial functionality by @dieselxxx in https://github.com/The-FireHub-Project/Container/pull/1 \ No newline at end of file diff --git a/README.md b/README.md index eb1a2c9..0805643 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ /> version diff --git a/composer.json b/composer.json index 20e7ad2..cb7ba03 100644 --- a/composer.json +++ b/composer.json @@ -8,8 +8,8 @@ "require": { "php": "^8.5", - "the-firehub-project/runtime": ">=0.1.0 <1.0.0", - "the-firehub-project/core-standard": "dev-develop" + "the-firehub-project/core-standard": "dev-develop", + "the-firehub-project/runtime": ">=0.1.0 <1.0.0" }, "require-dev": { "phpstan/phpstan": "^2.0", diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..0f227c5 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,692 @@ + + * @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\ { + Container\Registrar, Container\Scope as ScopeBoundary, Container as ContainerBoundary +}; +use FireHub\Core\Meta\Enum\Container\Lifetime; +use FireHub\Container\Definition\ { + Alias, Cls, Factory, Instance +}; +use FireHub\Container\Internal\ { + ContextualBindingRegistry, InstanceStore, ParameterBindingRegistry +}; +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 ContainerBoundary, Registrar { + + /** + * ### Service registry + * @since 1.0.0 + */ + private readonly Registry $registry; + + /** + * ### Service resolver + * @since 1.0.0 + */ + private readonly Resolver $resolver; + + /** + * ### Constructor parameter binding registry + * @since 1.0.0 + */ + private readonly ParameterBindingRegistry $parameters; + + /** + * ### Contextual dependency binding registry + * @since 1.0.0 + */ + private readonly ContextualBindingRegistry $contextual; + + /** + * ### Container freeze state + * @var bool + */ + private bool $frozen = false; + + /** + * ### Constructor + * @since 1.0.0 + * + * @return void + */ + public function __construct () { + + $this->registry = new Registry; + $this->parameters = new ParameterBindingRegistry; + $this->contextual = new ContextualBindingRegistry; + + $this->resolver = new Resolver( + $this->registry, + new InstanceStore, + $this->parameters, + $this->contextual + ); + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $scope = $container->createScope(); + * + * + * @since 1.0.0 + */ + public function createScope ():ScopeBoundary { + + 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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->register(Test::class, new Cls(Test::class)); + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::assertMutable() To check if the container is frozen. + * @uses \FireHub\Container\Registry::register() To register a service definition. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

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

+ * Service definition. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + * + * @return $this Container instance. + */ + public function register (string $id, Definition $definition):self { + + $this->assertMutable(); + + $this->registry->register($id, $definition); + + return $this; + + } + + /** + * ### Registers a contextual dependency binding + * + * Associates a specific consumer class with an implementation to be used when resolving the specified dependency. + * + * Contextual bindings apply only to the exact consumer class and do not affect global service registrations. + * + * The target service retains its configured lifetime. + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::assertMutable() + * @uses \FireHub\Container\Internal\ContextualBindingRegistry::register() + * + * @param class-string $consumer

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

+ * @param class-string $implementation

+ * Target service identifier. + *

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateContextualBindingException If the binding already exists. + * + * @return $this Container instance. + */ + public function bindContextual (string $consumer, string $dependency, string $implementation):self { + + $this->assertMutable(); + + $this->contextual->register( + $consumer, + $dependency, + $implementation + ); + + return $this; + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->bind(Test::class, Test::class); + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + */ + 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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $first = $container->get(Test::class); + * $second = $container->get(Test::class); + * + * // $first === $second + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::bind() To register a class-based service. + * + * @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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->scoped(Test::class); + * + * $firstScope = $container->createScope(); + * $secondScope = $container->createScope(); + * + * $first = $firstScope->get(Test::class); + * $second = $secondScope->get(Test::class); + * + * // $first !== $second + * + * $firstScope->close(); + * $secondScope->close(); + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::bind() To register a class-based service. + * + * @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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->transient(Test::class); + * + * $first = $container->get(Test::class); + * $second = $container->get(Test::class); + * + * // $first !== $second + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::bind() To register a class-based service. + * + * @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 + ); + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->factory( + * Test::class, + * static fn ($resolver):Test => new Test + * ); + * + * $service = $container->get(Test::class); + * + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Logger::class); + * + * $container->factory( + * Test::class, + * static fn ($resolver):Test => new Test( + * $resolver->get(Logger::class) + * ) + * ); + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + */ + public function factory (string $id, Closure $factory, Lifetime $lifetime = Lifetime::TRANSIENT):self { + + return $this->register( + $id, + new Factory($factory, $lifetime) + ); + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $instance = new Test; + * + * $container->instance(Test::class, $instance); + * + * $service = $container->get(Test::class); + * + * // $service === $instance + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + */ + public function instance (string $id, object $instance):self { + + return $this->register( + $id, + new Instance($instance) + ); + + } + + /** + * {@inheritDoc} + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * $container->alias(TestInterface::class, Test::class); + * + * $service = $container->get(TestInterface::class); + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::register() To register a service definition. + * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * @throws \FireHub\Container\Exception\DuplicateServiceException If the identifier is already registered. + */ + public function alias (string $id, string $target):self { + + return $this->register( + $id, + new Alias($target) + ); + + } + + /** + * ### Registers explicit constructor parameter bindings + * + * Associates an implementation class with explicitly configured constructor argument values. + * + * Explicit bindings take precedence over automatic dependency resolution and constructor default values. + * + * Bindings are associated with the implementation class rather than a service identifier, allowing them to work + * with both registered and implicitly constructed services. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->bindParameters( + * Test::class, + * [ + * 'name' => 'FireHub', + * 'enabled' => true + * ] + * ); + * + * $service = $container->get(Test::class); + * + * + * @since 1.0.0 + * + * @uses \FireHub\Container\Container::assertMutable() To ensure the container accepts configuration changes. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::register() To register parameter bindings. + * + * @param class-string $class

+ * Implementation class. + *

+ * @param array $parameters

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

+ * + * @throws \FireHub\Container\Exception\ContainerFrozenException If the container is frozen. + * + * @return $this Container instance. + */ + public function bindParameters (string $class, array $parameters):self { + + $this->assertMutable(); + + $this->parameters->register($class, $parameters); + + return $this; + + } + + /** + * ### Resolves a registered service + * + * 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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $service = $container->get(Test::class); + * + * + * @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 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. + * @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. + */ + 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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $registered = $container->has(Test::class); + * + * // true + * + * + * @since 1.0.0 + * + * @param class-string $id

+ * 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. + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->singleton(Test::class); + * + * $container->freeze(); + * + * // Existing services remain available. + * $service = $container->get(Test::class); + * + * // Further registrations are prohibited. + * // $container->singleton(AnotherTest::class); + * + * + * @since 1.0.0 + * + * @return void + */ + public function freeze ():void { + + $this->frozen = true; + + } + + /** + * ### Checks whether the container is frozen + * + * + * use FireHub\Container\Container; + * + * $container = new Container; + * + * $container->isFrozen(); // false + * + * $container->freeze(); + * + * $container->isFrozen(); // true + * + * + * @since 1.0.0 + * + * @return bool True if frozen, false otherwise. + */ + 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/Definition.php b/src/Definition.php new file mode 100644 index 0000000..27cedb2 --- /dev/null +++ b/src/Definition.php @@ -0,0 +1,22 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.0 + * @package Container + */ + +namespace FireHub\Container; + +/** + * ### Defines a service definition + * + * Describes the lifetime of a registered service without prescribing how its instance is created or resolved. + * @since 1.0.0 + */ +interface Definition {} \ No newline at end of file diff --git a/src/Definition/Alias.php b/src/Definition/Alias.php new file mode 100644 index 0000000..687b0fa --- /dev/null +++ b/src/Definition/Alias.php @@ -0,0 +1,42 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Container\Definition; + +/** + * ### Defines a service alias + * + * Redirects service resolution to another registered service identifier. + * + * Aliases do not own service instances or maintain independent lifetimes. + * @since 1.0.0 + */ +final readonly class Alias implements Definition { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param class-string $target

+ * Target service identifier. + *

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

+ * Concrete implementation class. + *

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

+ * Service lifetime. + *

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

+ * Service factory. + *

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

+ * Service lifetime. + *

+ * + * @return void + */ + public function __construct ( + public Closure $factory, + private Lifetime $lifetime = Lifetime::TRANSIENT + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function lifetime ():Lifetime { + + return $this->lifetime; + + } + +} \ No newline at end of file diff --git a/src/Definition/Instance.php b/src/Definition/Instance.php new file mode 100644 index 0000000..ea48c31 --- /dev/null +++ b/src/Definition/Instance.php @@ -0,0 +1,51 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Core\Meta\Enum\Container\Lifetime; + +/** + * ### Defines an existing service instance + * + * Represents an already constructed object registered as a singleton service. + * @since 1.0.0 + */ +final readonly class Instance implements Service { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param object $instance

+ * Registered service instance. + *

+ * + * @return void + */ + public function __construct ( + public object $instance + ) {} + + /** + * @inheritDoc + * + * @since 1.0.0 + */ + public function lifetime ():Lifetime { + + return Lifetime::SINGLETON; + + } + +} \ No newline at end of file diff --git a/src/Definition/Service.php b/src/Definition/Service.php new file mode 100644 index 0000000..5c53b16 --- /dev/null +++ b/src/Definition/Service.php @@ -0,0 +1,42 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.4 + * @package Container + */ + +namespace FireHub\Container\Definition; + +use FireHub\Core\Meta\Enum\Container\Lifetime; +use FireHub\Container\Definition; + +/** + * ### Defines a service definition with a managed lifetime + * + * Represents a registered service definition that specifies how long its resolved instance should be retained + * and reused by the dependency injection container. + * + * Service definitions provide lifetime metadata without prescribing how service instances are constructed, + * cached, or resolved. + * @since 1.0.0 + */ +interface Service extends Definition { + + /** + * ### Gets the service lifetime + * + * Returns the lifetime associated with this service definition, determining whether resolved instances are + * shared across the container, isolated within individual scopes, or created for each resolution request. + * @since 1.0.0 + * + * @return \FireHub\Core\Meta\Enum\Container\Lifetime Configured service lifetime. + */ + public function lifetime ():Lifetime; + +} \ No newline at end of file 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/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/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/DuplicateContextualBindingException.php b/src/Exception/DuplicateContextualBindingException.php new file mode 100644 index 0000000..0309a6d --- /dev/null +++ b/src/Exception/DuplicateContextualBindingException.php @@ -0,0 +1,29 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.1 + * @package Container + */ + +namespace FireHub\Container\Exception; + +use FireHub\Core\Exception\Runtime\System\InvariantException; + +/** + * ### Thrown when a contextual dependency binding is already registered + * + * Indicates that a contextual dependency registration would violate the registry invariant requiring each + * consumer and dependency combination to have exactly one associated implementation. + * @since 1.0.0 + */ +final class DuplicateContextualBindingException extends InvariantException { + + protected const string DEFAULT_MESSAGE = 'A contextual binding for the specified consumer and dependency is already registered'; + +} \ No newline at end of file diff --git a/src/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/ScopeActiveException.php b/src/Exception/ScopeActiveException.php new file mode 100644 index 0000000..9ed842c --- /dev/null +++ b/src/Exception/ScopeActiveException.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 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 = 'The dependency injection scope is currently active.'; + +} \ No newline at end of file diff --git a/src/Exception/ScopeClosedException.php b/src/Exception/ScopeClosedException.php new file mode 100644 index 0000000..95482c7 --- /dev/null +++ b/src/Exception/ScopeClosedException.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 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 access a closed dependency injection 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/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/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/Autowirer.php b/src/Internal/Autowirer.php new file mode 100644 index 0000000..14bf5d0 --- /dev/null +++ b/src/Internal/Autowirer.php @@ -0,0 +1,332 @@ + + * @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, ReflectionIntersectionType, ReflectionUnionType; + +/** + * ### Constructs services using compiled dependency plans + * + * Executes cached dependency construction plans to instantiate services and resolve their constructor dependencies. + * + * Static parameter analysis is performed by the dependency plan cache, while service availability, contextual + * bindings, and candidate selection are evaluated dynamically within the active resolution context. + * + * Explicit parameter bindings take precedence over contextual bindings and automatic dependency resolution. + * @since 1.0.0 + * + * @internal + */ +final readonly class Autowirer { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param Closure(class-string, \FireHub\Container\Internal\ResolutionContext):object $resolve

+ * Context-aware service resolver. + *

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

+ * Explicit service registration checker. + *

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

+ * Dependency type matcher. + *

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

+ * Dependency candidate selector. + *

+ * @param \FireHub\Container\Internal\DependencyPlanCache $plans

+ * Shared dependency plan cache. + *

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

+ * Shared parameter binding registry. + *

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

+ * Shared contextual dependency selector. + *

+ * + * @return void + */ + public function __construct ( + private Closure $resolve, + private Closure $has, + private TypeMatcher $matcher, + private CandidateSelector $selector, + private DependencyPlanCache $plans, + private ParameterBindingRegistry $bindings, + private ContextualCandidateSelector $contextual_selector + ) {} + + /** + * ### Constructs a service using its compiled dependency plan + * + * Resolves constructor dependencies dynamically while reusing previously compiled parameter information. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\DependencyPlanCache::get() To retrieve the compiled construction plan. + * @uses \FireHub\Container\Internal\ConstructorMetadataCache::get() To get constructor metadata. + * + * @param class-string $class

+ * Concrete service class. + *

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

+ * Active dependency resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot + * be resolved. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * + * @return object Constructed service instance. + */ + public function construct (string $class, ResolutionContext $context):object { + + $plan = $this->plans->get($class); + + $metadata = $plan->metadata; + + if (!$metadata->instantiable) + throw new ServiceInstantiationException; + + if (!$metadata->hasConstructor) + return $metadata->reflection->newInstance(); + + $arguments = []; + + foreach ($plan->parameters as $parameter) + $arguments[] = $this->resolveParameter( + $class, + $parameter, + $context + ); + + return $metadata->reflection->newInstanceArgs($arguments); + + } + + /** + * ### Resolves a compiled constructor parameter + * + * Resolves constructor arguments according to the following precedence: + * + * 1. Explicit parameter binding. + * 2. Contextual dependency binding. + * 3. Registered or implicitly constructible dependency. + * 4. Constructor default value. + * + * Contextual bindings apply only to named dependency types and the exact consumer implementation class. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check explicit parameter bindings. + * @uses \FireHub\Container\Internal\ParameterBindingRegistry::get() To retrieve explicitly bound values. + * @uses \FireHub\Container\Internal\ContextualBindingRegistry::find() To find contextual dependency bindings. + * @uses \FireHub\Container\Internal\Autowirer::selectDependency() To select a dependency identifier. + * @uses \FireHub\Container\Internal\Autowirer::resolveDependency() To resolve selected dependencies. + * @uses \FireHub\Container\Internal\Autowirer::unresolvedParameter() To create resolution exceptions. + * + * @param class-string $class

+ * Concrete service class. + *

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

+ * Compiled constructor parameter plan. + *

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

+ * Active resolution context. + *

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

+ * Compiled constructor parameter plan. + *

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

+ * Selected dependency identifier. + *

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

+ * Compiled constructor parameter plan. + *

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

+ * Active resolution context. + *

+ * @param bool $contextual [optional]

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

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

+ * Compiled constructor parameter plan. + *

+ * + * @return \FireHub\Container\Exception\ServiceInstantiationException Parameter resolution exception. + */ + private function unresolvedParameter (ParameterPlan $plan):ServiceInstantiationException { + + $parameter = $plan->parameter; + + $type = $parameter->getType(); + + return new ServiceInstantiationException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName(), + 'position' => $parameter->getPosition(), + 'type' => $type?->__toString(), + 'nullable' => $type?->allowsNull(), + 'optional' => $parameter->isOptional(), + 'has_default' => $plan->hasDefault + ] + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/CandidateSelector.php b/src/Internal/CandidateSelector.php new file mode 100644 index 0000000..a700f11 --- /dev/null +++ b/src/Internal/CandidateSelector.php @@ -0,0 +1,289 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\ { + AmbiguousDependencyException, ServiceInstantiationException +}; +use FireHub\Runtime; +use Closure, ReflectionIntersectionType, ReflectionNamedType, + ReflectionParameter, ReflectionType, ReflectionUnionType; + +/** + * ### Selects dependency injection candidates + * + * Evaluates available service candidates for union and intersection constructor dependency types. + * + * Explicit registrations take precedence over implicitly constructible classes when resolving union dependencies. + * + * Intersection dependencies are matched against statically known registered implementation types. + * + * Candidate selection does not instantiate services. + * @since 1.0.0 + * + * @internal + */ +final readonly class CandidateSelector { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\TypeMatcher $matcher

+ * Dependency type matcher- + *

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

+ * Explicit service registration checker. + *

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

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

+ */ + public function __construct ( + private TypeMatcher $matcher, + private Closure $has, + private Closure $candidates + ) {} + + /** + * ### Selects a union or DNF dependency + * + * Selects a unique eligible service from the declared union alternatives. + * + * Registered candidates take precedence over implicitly constructible alternatives. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation + * satisfies a dependency type. + * @uses \FireHub\Container\Internal\CandidateSelector::ambiguous() To report multiple candidates. + * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() Checks if a class is implicitly constructible. + * @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 (($this->candidates)() as $id => $implementation) { + + if ($this->matchesType($implementation, $parameter, $type)) + $registered[$id] = true; + + } + + foreach ($type->getTypes() as $member) { + + if (!$member instanceof ReflectionNamedType) + continue; + + $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\CandidateSelector::matchesType() To check if a service implementation + * satisfies a dependency type. + * @uses \FireHub\Runtime\Arr\Inspection::count() To check if the union contains multiple candidates. + * @uses \FireHub\Runtime\Arr\Access::keys() To get the union member identifiers. + * @uses \FireHub\Runtime\Arr\Access::firstKey() To get the first union member identifier. + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionIntersectionType $type

+ * Intersection dependency type. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates satisfy the dependency. + * + * @return null|class-string Selected service identifier. + */ + public function intersection (ReflectionParameter $parameter, ReflectionIntersectionType $type):?string { + + /** @var array $matches */ + $matches = []; + + foreach (($this->candidates)() as $id => $implementation) { + + if ($this->matchesType($implementation, $parameter, $type)) + $matches[$id] = true; + + } + + if (Runtime\Arr\Inspection::count($matches) > 1) + $this->ambiguous( + $parameter, + $type, + Runtime\Arr\Access::keys($matches) + ); + + return Runtime\Arr\Access::firstKey($matches); + + } + + /** + * ### Checks whether an implementation satisfies a dependency type + * + * Evaluates named, intersection, and union dependency types recursively. + * + * Intersection types require all members to match, while union types require at least one matching member. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\TypeMatcher::classId() Resolves a parameter class identifier. + * @uses \FireHub\Container\Internal\TypeMatcher::matches() To check if a service implementation satisfies a + * contract. + * + * @param class-string $implementation

+ * Concrete implementation class. + *

+ * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionType $type

+ * Dependency type. + *

+ * + * @return bool True if the implementation satisfies the dependency type. + */ + public function matchesType (string $implementation, ReflectionParameter $parameter, ReflectionType $type):bool { + + if ($type instanceof ReflectionNamedType) { + + $contract = $this->matcher->classId($parameter, $type); + + return $contract !== null + && $this->matcher->matches($implementation, $contract); + + } + + if ($type instanceof ReflectionIntersectionType) { + + foreach ($type->getTypes() as $member) { + + if (!$this->matchesType($implementation, $parameter, $member)) + return false; + + } + + return true; + + } + + if ($type instanceof ReflectionUnionType) { + + foreach ($type->getTypes() as $member) { + + if ($this->matchesType($implementation, $parameter, $member)) + return true; + + } + + return false; + + } + + return false; + + } + + /** + * ### Throws an ambiguous dependency exception + * + * Reports multiple eligible candidates for a constructor parameter. + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Constructor parameter. + *

+ * @param ReflectionUnionType|ReflectionIntersectionType $type

+ * Requested dependency type. + *

+ * @param list $candidates

+ * Matching service identifiers. + *

+ * + * @throws \FireHub\Container\Exception\AmbiguousDependencyException Always. + * + * @return never + */ + private function ambiguous (ReflectionParameter $parameter, ReflectionUnionType|ReflectionIntersectionType $type, array $candidates):never { + + throw new AmbiguousDependencyException( + context: [ + 'class' => $parameter->getDeclaringClass()?->getName(), + 'parameter' => $parameter->getName(), + 'type' => (string)$type, + 'candidates' => $candidates + ] + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/ConstructorMetadata.php b/src/Internal/ConstructorMetadata.php new file mode 100644 index 0000000..bee5caf --- /dev/null +++ b/src/Internal/ConstructorMetadata.php @@ -0,0 +1,57 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionClass, ReflectionParameter; + +/** + * ### Represents cached constructor metadata + * + * Provides immutable reflection metadata for a service implementation. + * + * Stores the reflected class, its insatiability, and constructor parameters without resolving dependencies or + * creating service instances. + * @since 1.0.0 + * + * @internal + */ +final readonly class ConstructorMetadata { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param ReflectionClass $reflection

+ * Reflected service implementation. + *

+ * @param bool $instantiable

+ * Whether the implementation can be instantiated. + *

+ * @param bool $hasConstructor

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

+ * @param list $parameters

+ * Constructor parameters. + *

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

+ * Service implementation class. + *

+ * + * @return \FireHub\Container\Internal\ConstructorMetadata Cached constructor metadata. + */ + public function get (string $class):ConstructorMetadata { + + if (isset($this->metadata[$class])) + return $this->metadata[$class]; + + $reflection = new ReflectionClass($class); + + $constructor = $reflection->getConstructor(); + + return $this->metadata[$class] = new ConstructorMetadata( + $reflection, + $reflection->isInstantiable(), + $constructor !== null, + $constructor?->getParameters() ?? [] + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/ContextualBindingRegistry.php b/src/Internal/ContextualBindingRegistry.php new file mode 100644 index 0000000..03e69db --- /dev/null +++ b/src/Internal/ContextualBindingRegistry.php @@ -0,0 +1,113 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\DuplicateContextualBindingException; + +/** + * ### Stores contextual dependency bindings + * + * Associates consumer implementation classes with specific dependency contracts and their target service identifiers. + * + * Contextual bindings do not replace global definitions and are evaluated dynamically during constructor autowiring. + * @since 1.0.0 + * + * @internal + */ +final class ContextualBindingRegistry { + + /** + * ### Contextual dependency bindings + * @since 1.0.0 + * + * @var array> + */ + private array $bindings = []; + + /** + * ### Registers a contextual dependency binding + * + * Associates a consumer class and required dependency with a target service identifier. + * + * Duplicate registrations for the same consumer and dependency are rejected. + * @since 1.0.0 + * + * @param class-string $consumer

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

+ * @param class-string $implementation

+ * Target service identifier. + *

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

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

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

+ * Consumer implementation class. + *

+ * @param class-string $dependency

+ * Required dependency contract. + *

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

+ * Shared contextual binding registry. + *

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

+ * Concrete consumer implementation class. + *

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

+ * Compiled constructor parameter plan. + *

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

+ * Concrete consumer implementation class. + *

+ * @param ReflectionType $type

+ * Declared constructor parameter type. + *

+ * + * @return list Distinct contextual implementation identifiers. + */ + private function collect (string $consumer, ReflectionType $type):array { + + if ($type instanceof ReflectionNamedType) { + + if ($type->isBuiltin()) + return []; + + $implementation = $this->bindings->find( + $consumer, + $type->getName() // @phpstan-ignore argument.type + ); + + return $implementation !== null + ? [$implementation] + : []; + + } + + if ( + !$type instanceof ReflectionUnionType + && !$type instanceof ReflectionIntersectionType + ) + return []; + + $candidates = []; + + foreach ($type->getTypes() as $member) + foreach ($this->collect($consumer, $member) as $implementation) + $candidates[$implementation] = $implementation; + + return Runtime\Arr\Access::values($candidates); + + } + +} \ No newline at end of file diff --git a/src/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/DependencyPlan.php b/src/Internal/DependencyPlan.php new file mode 100644 index 0000000..23bb26a --- /dev/null +++ b/src/Internal/DependencyPlan.php @@ -0,0 +1,46 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Internal; + +/** + * ### Represents a compiled dependency construction plan + * + * Defines the statically analyzed constructor parameters required to instantiate a service. + * + * The plan contains no resolved service instances and is independent of the active resolution context. + * @since 1.0.0 + * + * @internal + */ +final readonly class DependencyPlan { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ConstructorMetadata $metadata

+ * Cached constructor reflection metadata. + *

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

+ * Compiled constructor parameter plans. + *

+ * + * @return void + */ + public function __construct ( + public ConstructorMetadata $metadata, + public array $parameters + ) {} + +} \ No newline at end of file diff --git a/src/Internal/DependencyPlanCache.php b/src/Internal/DependencyPlanCache.php new file mode 100644 index 0000000..3617655 --- /dev/null +++ b/src/Internal/DependencyPlanCache.php @@ -0,0 +1,118 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionNamedType, ReflectionUnionType, ReflectionIntersectionType; + +/** + * ### Compiles and caches dependency construction plans + * + * Analyzes constructor parameters once per implementation class and stores the resulting dependency plans. + * + * Compiled plans contain only static constructor information. Registered service availability, union and + * intersection candidate selection, and dependency instantiation remain dynamic. + * @since 1.0.0 + * + * @internal + */ +final class DependencyPlanCache { + + /** + * ### Cached dependency plans + * @since 1.0.0 + * + * @var array + */ + private array $plans = []; + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ConstructorMetadataCache $metadata

+ * Shared constructor metadata cache. + *

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

+ * Dependency type matcher. + *

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

+ * Service implementation class. + *

+ * + * @return \FireHub\Container\Internal\DependencyPlan Compiled dependency plan. + */ + public function get (string $class):DependencyPlan { + + if (isset($this->plans[$class])) + return $this->plans[$class]; + + $metadata = $this->metadata->get($class); + + $parameters = []; + + foreach ($metadata->parameters as $parameter) { + + if ($parameter->isVariadic()) + continue; + + $type = $parameter->getType(); + + $id = $type instanceof ReflectionNamedType + ? $this->matcher->classId($parameter, $type) + : null; + + $composite = $type instanceof ReflectionUnionType + || $type instanceof ReflectionIntersectionType + ? $type + : null; + + $hasDefault = $parameter->isDefaultValueAvailable(); + + $parameters[] = new ParameterPlan( + $parameter, + $id, + $composite, + $hasDefault, + $hasDefault + ? $parameter->getDefaultValue() + : null + ); + + } + + return $this->plans[$class] = new DependencyPlan( + $metadata, + $parameters + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/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/ParameterBindingRegistry.php b/src/Internal/ParameterBindingRegistry.php new file mode 100644 index 0000000..f3aabd7 --- /dev/null +++ b/src/Internal/ParameterBindingRegistry.php @@ -0,0 +1,102 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=7.4 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Runtime; + +/** + * ### Stores explicit constructor parameter bindings + * + * Maintains parameter values associated with individual implementation classes. + * + * Explicit bindings take precedence over automatic dependency resolution and constructor defaults. + * @since 1.0.0 + * + * @internal + */ +final class ParameterBindingRegistry { + + /** + * ### Parameter bindings + * @since 1.0.0 + * + * @var array> + */ + private array $bindings = []; + + /** + * ### Registers constructor parameter bindings + * + * Associates parameter names with explicitly configured values for an implementation class. + * + * Existing bindings for the same class are replaced. + * @since 1.0.0 + * + * @param class-string $class

+ * Implementation class. + *

+ * @param array $parameters

+ * Parameter names mapped to explicit values. + *

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

+ * Implementation class. + *

+ * @param non-empty-string $parameter

+ * Constructor parameter name. + *

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

+ * Implementation class. + *

+ * @param non-empty-string $parameter

+ * Constructor parameter name. + *

+ * + * @return mixed Explicit parameter value. + */ + public function get (string $class, string $parameter):mixed { + + return $this->bindings[$class][$parameter]; // @phpstan-ignore-line + + } + +} \ No newline at end of file diff --git a/src/Internal/ParameterPlan.php b/src/Internal/ParameterPlan.php new file mode 100644 index 0000000..66e463a --- /dev/null +++ b/src/Internal/ParameterPlan.php @@ -0,0 +1,60 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.2 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use ReflectionParameter, ReflectionUnionType, ReflectionIntersectionType; + +/** + * ### Represents a compiled constructor parameter plan + * + * Stores the statically determined information required to resolve a constructor parameter. + * + * Dependency availability and candidate selection remain dynamic and are evaluated during plan execution. + * @since 1.0.0 + * + * @internal + */ +final readonly class ParameterPlan { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param ReflectionParameter $parameter

+ * Original constructor parameter. + *

+ * @param null|class-string $id

+ * Statically resolved named dependency identifier. + *

+ * @param null|ReflectionUnionType|ReflectionIntersectionType $composite

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

+ * @param bool $hasDefault

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

+ * @param mixed $default

+ * Default parameter value. + *

+ * + * @return void + */ + public function __construct ( + public ReflectionParameter $parameter, + public ?string $id, + public ReflectionUnionType|ReflectionIntersectionType|null $composite, + public bool $hasDefault, + public mixed $default + ) {} + +} \ No newline at end of file diff --git a/src/Internal/ResolutionContext.php b/src/Internal/ResolutionContext.php new file mode 100644 index 0000000..d71208b --- /dev/null +++ b/src/Internal/ResolutionContext.php @@ -0,0 +1,74 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Container + */ + +namespace FireHub\Container\Internal; + +/** + * ### Represents an active dependency resolution context + * + * Maintains the state associated with a single dependency resolution chain. + * + * The context tracks circular dependencies and provides access to the active scoped instance storage. + * + * A new context is created for each independent resolution request and shared by all nested resolutions + * belonging to that request. + * @since 1.0.0 + * + * @internal + */ +final readonly class ResolutionContext { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ResolutionStack $stack

+ * Active dependency resolution stack. + *

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

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

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

+ * Associated scope lifecycle state. + *

+ * + * @return void + */ + public function __construct ( + public ResolutionStack $stack = new ResolutionStack, + public ?InstanceStore $scoped = null, + public ?ScopeState $scope = null + ) {} + + /** + * ### Creates a root resolution context + * + * Returns a context that does not provide access to scoped instances while preserving the current + * dependency resolution stack. + * @since 1.0.0 + * + * @return self Root resolution context. + */ + public function root ():self { + + if ($this->scoped === null) + return $this; + + return new self( + stack: $this->stack, + scope: $this->scope + ); + + } + +} \ No newline at end of file diff --git a/src/Internal/ResolutionStack.php b/src/Internal/ResolutionStack.php new file mode 100644 index 0000000..bac440f --- /dev/null +++ b/src/Internal/ResolutionStack.php @@ -0,0 +1,89 @@ + + * @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 = []; + + /** + * ### Active service identifiers + * @since 1.0.0 + * + * @var array + */ + private array $active = []; + + /** + * ### Starts resolving a service + * @since 1.0.0 + * + * @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 (isset($this->active[$id])) + throw new CircularDependencyException( + context: [ + 'stack' => $this->stack + ] + ); + + $this->stack[] = $id; + + $this->active[$id] = true; + + } + + /** + * ### 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 { + + $id = Runtime\Arr\Mutation::pop($this->stack); + + if ($id !== null) + unset($this->active[$id]); + + } + +} \ No newline at end of file diff --git a/src/Internal/ScopeState.php b/src/Internal/ScopeState.php new file mode 100644 index 0000000..4f77c09 --- /dev/null +++ b/src/Internal/ScopeState.php @@ -0,0 +1,167 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Container + */ + +namespace FireHub\Container\Internal; + +use FireHub\Container\Exception\ { + ScopeActiveException, ScopeClosedException +}; + +/** + * ### Manages dependency injection scope lifecycle + * + * Tracks whether a scope is closed and prevents its closure while service resolution is active. + * @since 1.0.0 + * + * @internal + */ +final class ScopeState { + + /** + * ### Scope closure state + * @since 1.0.0 + */ + private bool $closed = false; + + /** + * ### Active resolution count + * @since 1.0.0 + */ + private int $active = 0; + + /** + * ### Scope execution state + * @since 1.0.0 + */ + private bool $running = false; + + /** + * ### Begins a resolution operation + * + * Prevents resolution through a closed scope and increments + * the number of active resolution operations. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ScopeState::assertOpen() To check if the scope is open. + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * + * @return void + */ + public function enter ():void { + + $this->assertOpen(); + + $this->active++; + + } + + /** + * ### Completes a resolution operation + * @since 1.0.0 + * + * @return void + */ + public function leave ():void { + + if ($this->active > 0) + $this->active--; + + } + + /** + * ### Ensures the scope is open + * @since 1.0.0 + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * + * @return void + */ + public function assertOpen ():void { + + if ($this->closed) + throw new ScopeClosedException; + + } + + /** + * ### Closes the scope + * + * Prevents closure while service resolution is active. + * @since 1.0.0 + * + * @throws \FireHub\Container\Exception\ScopeActiveException If service resolution is active. + * + * @return void + */ + public function close ():void { + + if ($this->closed) + return; + + if ($this->running || $this->active > 0) + throw new ScopeActiveException; + + $this->closed = true; + + } + + /** + * ### Checks whether the scope is closed + * @since 1.0.0 + * + * @return bool True if closed, false otherwise. + */ + public function isClosed ():bool { + + return $this->closed; + + } + + /** + * ### Begins scope execution + * + * Prevents concurrent or nested execution of the same scope. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\ScopeState::assertOpen() To check if the scope is open. + * + * @throws \FireHub\Container\Exception\ScopeClosedException If the scope is closed. + * @throws \FireHub\Container\Exception\ScopeActiveException If scope execution is already in progress. + * + * @return void + */ + public function beginRun ():void { + + $this->assertOpen(); + + if ($this->running || $this->active > 0) + throw new ScopeActiveException; + + $this->running = true; + + } + + /** + * ### Ends scope execution + * @since 1.0.0 + * + * @return void + */ + public function endRun ():void { + + $this->running = false; + + } + +} \ No newline at end of file diff --git a/src/Internal/TypeMatcher.php b/src/Internal/TypeMatcher.php new file mode 100644 index 0000000..21529d6 --- /dev/null +++ b/src/Internal/TypeMatcher.php @@ -0,0 +1,132 @@ + + * @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 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 { + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Internal\ConstructorMetadataCache $metadata

+ * Shared constructor metadata cache. + *

+ * + * @return void + */ + public function __construct ( + private ConstructorMetadataCache $metadata + ) {} + + /** + * ### Resolves a parameter class identifier + * + * 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\Container\Internal\ConstructorMetadataCache::get() To retrieve constructor metadata. + * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. + * + * @param class-string $class

+ * 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 $this->metadata->get($class)->instantiable; + + } + + /** + * ### 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/Registry.php b/src/Registry.php new file mode 100644 index 0000000..35cba35 --- /dev/null +++ b/src/Registry.php @@ -0,0 +1,174 @@ + + * @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\Definition\ { + Cls, Instance +}; +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 + * + * @internal + */ +final class Registry { + + /** + * ### Registered service definitions + * @since 1.0.0 + * + * @var array + */ + private array $definitions = []; + + /** + * ### Registers a service definition + * + * Associates the specified service identifier with its definition. + * + * An identifier may only be registered once within the registry. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

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

+ * Service definition. + *

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

+ * Service identifier. + *

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

+ * Service identifier. + *

+ * + * @return bool True if the service identifier is registered. + */ + public function has (string $id):bool { + + return isset($this->definitions[$id]); + + } + + /** + * ### Finds a registered service definition + * + * Returns the service definition associated with the specified identifier without throwing an exception when + * the identifier is not registered. + * @since 1.0.0 + * + * @param class-string $id

+ * Service identifier. + *

+ * + * @return null|\FireHub\Container\Definition Registered service definition, or null if absent. + */ + public function find (string $id):?Definition { + + return $this->definitions[$id] ?? null; + + } + + /** + * ### Gets all registered service definitions + * + * 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; + + } + + /** + * ### 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 new file mode 100644 index 0000000..27e8a05 --- /dev/null +++ b/src/Resolver.php @@ -0,0 +1,557 @@ + + * @copyright 2026-present The FireHub Project - All rights reserved + * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0 + * + * @php-version >=8.5 + * @package Container + */ + +namespace FireHub\Container; + +use FireHub\Core\Boundary\Resolver as ResolverBoundary; +use FireHub\Core\Meta\Enum\Container\Lifetime; +use FireHub\Container\Definition\ { + Alias, Cls, Factory, Instance, Service +}; +use FireHub\Container\Internal\ { + Autowirer, CandidateSelector, ConstructorMetadataCache, ContextualBindingRegistry, ContextualCandidateSelector, + ContextualResolver, DependencyPlanCache, InstanceStore, ParameterBindingRegistry, ResolutionContext, ScopeState, + TypeMatcher +}; +use FireHub\Container\Exception\ { + ScopeRequiredException, ServiceNotFoundException, ServiceTypeMismatchException, UnsupportedDefinitionException +}; +use FireHub\Runtime; + +/** + * ### Resolves registered dependency injection services + * + * Creates and retrieves service instances from registered definitions while enforcing their configured lifetimes. + * + * Supports singleton, scoped, and transient services, including class-based construction, factory invocation, + * existing instances, and service aliases. + * + * Singleton instances are shared across all resolution contexts, scoped instances are isolated within their + * associated scope, and transient instances are created for each resolution request. + * + * The resolver detects circular dependencies and validates resolved objects against their service identifiers. + * @since 1.0.0 + * + * @internal + */ +final readonly class Resolver implements ResolverBoundary { + + /** + * ### Constructor metadata cache + * @since 1.0.0 + */ + private ConstructorMetadataCache $metadata; + + /** + * ### Reusable dependency autowirer + * @since 1.0.0 + */ + private Autowirer $autowirer; + + /** + * ### Constructor + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::has() To check for service existence. + * @uses \FireHub\Container\Resolver::get() To retrieve a service definition. + * @uses \FireHub\Container\Registry::implementations() To get all registered implementations. + * + * @param \FireHub\Container\Registry $registry

+ * Registry containing service definitions. + *

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

+ * Shared singleton instance storage. + *

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

+ * Parameter binding registry. + *

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

+ * Contextual binding registry. + *

+ * + * @return void + */ + public function __construct ( + private Registry $registry, + private InstanceStore $singletons, + ParameterBindingRegistry $parameters, + ContextualBindingRegistry $contextual + ) { + + $this->metadata = new ConstructorMetadataCache; + + $matcher = new TypeMatcher( + $this->metadata + ); + + $selector = new CandidateSelector( + $matcher, + fn (string $id):bool => $this->has($id), + fn ():iterable => $this->registry->implementations() + ); + + $plans = new DependencyPlanCache( + $this->metadata, + $matcher + ); + + $this->autowirer = new Autowirer( + fn (string $id, ResolutionContext $context):object => $this->resolve($id, $context), + fn (string $id):bool => $this->has($id), + $matcher, + $selector, + $plans, + $parameters, + new ContextualCandidateSelector($contextual) + ); + + } + + /** + * ### 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. + * @uses \FireHub\Container\Internal\InstanceStore::get() To retrieve a previously resolved instance. + * + * @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 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. + * @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. + */ + public function get (string $id):object { + + $instance = $this->singletons->get($id); + + if ($instance !== null) + /** @var TService $instance */ + return $instance; + + return $this->resolve( + $id, + new ResolutionContext + ); + + } + + /** + * ### Resolves a service within a scope + * @since 1.0.0 + * + * @uses \FireHub\Container\Resolver::resolve() To resolve a service. + * @uses \FireHub\Container\Internal\ScopeState::enter() To mark the scope as being currently being resolved. + * @uses \FireHub\Container\Internal\ScopeState::leave() To remove the scope from the resolution stack. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

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

+ * Scoped instance storage. + *

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

+ * Scope lifecycle state. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the 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. + * @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. + */ + public function getScoped (string $id, InstanceStore $scoped, ScopeState $scope):object { + + $scope->enter(); + + try { + + return $this->resolve( + $id, + new ResolutionContext( + scoped: $scoped, + scope: $scope + ) + ); + + } finally { + + $scope->leave(); + + } + + } + + /** + * ### 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::find() To retrieve a service definition. + * @uses \FireHub\Container\Internal\ResolutionStack::enter() To mark the service as being currently being resolved. + * @uses \FireHub\Container\Internal\ResolutionStack::leave() To remove the service from the resolution stack. + * @uses \FireHub\Container\Resolver::resolveService() To resolve a service. + * @uses \FireHub\Container\Resolver::resolveImplicit() To resolve an implicitly constructed service. + * + * @template TService of object + * + * @param class-string $id

+ * Service identifier. + *

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

+ * Active resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service identifier is not registered. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the 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\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * + * @return TService Resolved service. + */ + private function resolve (string $id, ResolutionContext $context):object { + + $definition = $this->registry->find($id); + + if ($definition === null) + return $this->resolveImplicit($id, $context); + + 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(); + + } + + } + + /** + * ### 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\Container\Internal\ConstructorMetadataCache::get() To retrieve constructor metadata. + * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class. + * + * @template TService of object + * + * @param class-string $id

+ * Unregistered concrete class identifier. + *

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

+ * Active resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the identifier is not an instantiable class. + * @throws \FireHub\Container\Exception\CircularDependencyException If a circular dependency is detected. + * @throws \FireHub\Container\Exception\ServiceTypeMismatchException If the constructed instance does not match + * the identifier. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * + * @return TService Constructed service. + */ + private function resolveImplicit (string $id, ResolutionContext $context):object { + + if (!Runtime\ObjectModel\Inspection::isClass($id)) + throw new ServiceNotFoundException; + + if (!$this->metadata->get($id)->instantiable) + 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 + * + * 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. + * @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. + *

+ * + * @return \FireHub\Container\Internal\ContextualResolver Context-bound resolver. + */ + private function contextual (ResolutionContext $context):ContextualResolver { + + return new ContextualResolver( + 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); + + } + ); + + } + + /** + * ### Creates a service instance + * + * 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

+ * Service definition. + *

+ * + * @throws \FireHub\Container\Exception\UnsupportedDefinitionException If the service definition is not supported. + * @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 \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * + * @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->autowire( + $definition->class, + $context + ), + default => throw new UnsupportedDefinitionException + }; + + } + + /** + * ### Constructs a service using constructor autowiring + * + * Automatically resolves registered constructor dependencies while preserving the current resolution context. + * @since 1.0.0 + * + * @uses \FireHub\Container\Internal\Autowirer::construct() To construct an instance using autowiring. + * + * @param class-string $class

+ * Concrete implementation class. + *

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

+ * Active resolution context. + *

+ * + * @throws \FireHub\Container\Exception\ServiceInstantiationException If the parameter is not optional and cannot + * be resolved. + * @throws \FireHub\Container\Exception\ServiceNotFoundException If the service is not registered. + * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible. + * + * @return object Constructed service instance. + */ + private function autowire (string $class, ResolutionContext $context):object { + + return $this->autowirer->construct($class, $context); + + } + +} \ No newline at end of file diff --git a/src/Scope.php b/src/Scope.php new file mode 100644 index 0000000..6ddf816 --- /dev/null +++ b/src/Scope.php @@ -0,0 +1,229 @@ + + * @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\Container\Scope as ScopeBoundary; +use FireHub\Container\Internal\ { + InstanceStore, ScopeState +}; +use Closure; + +/** + * ### 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 + */ +final readonly class Scope implements ScopeBoundary { + + /** + * ### Scoped instance store + * @since 1.0.0 + */ + private InstanceStore $instances; + + /** + * ### Scope lifecycle state + * @since 1.0.0 + */ + private ScopeState $state; + + /** + * ### Constructor + * @since 1.0.0 + * + * @param \FireHub\Container\Resolver $resolver

+ * Parent container's service resolver. + *

+ * + * @return void + */ + public function __construct ( + private Resolver $resolver + ) { + + $this->instances = new InstanceStore; + + $this->state = new ScopeState; + + } + + /** + * ### 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\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 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. + * + * @return TService Resolved service. + */ + public function get (string $id):object { + + return $this->resolver->getScoped( + $id, + $this->instances, + $this->state + ); + + } + + /** + * ### 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\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

+ * 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->state->assertOpen(); + + return $this->resolver->has($id); + + } + + /** + * ### Executes a callback within the scope + * + * Executes the provided callback using this scope as its dependency resolver. + * + * The scope is automatically closed after callback execution, including when the callback throws an exception. + * + * Nested execution of the same scope is not permitted. + * @since 1.0.0 + * + * @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. + * + * @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 run (Closure $callback):mixed { + + $this->state->beginRun(); + + try { + + return $callback($this); + + } finally { + + try { + + $this->state->endRun(); + + } finally { + + $this->close(); + + } + + } + + } + + /** + * ### 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 + * + * 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\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 + */ + public function close ():void { + + $this->state->close(); + + $this->instances->clear(); + + } + +} \ No newline at end of file