> Registered parameter bindings.
+ */
+ public function all ():array {
+
+ return $this->bindings;
+
+ }
+
}
\ No newline at end of file
diff --git a/src/Validation/ContextualBinding.php b/src/Validation/ContextualBinding.php
new file mode 100644
index 0000000..aea384c
--- /dev/null
+++ b/src/Validation/ContextualBinding.php
@@ -0,0 +1,355 @@
+
+ * @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\Validation;
+
+use FireHub\Container\Registry;
+use FireHub\Container\Definition\ {
+ Alias, Cls, Factory, Instance
+};
+use FireHub\Container\Internal\ {
+ ContextualBindingRegistry, DependencyPlanCache, ParameterBindingRegistry
+};
+use FireHub\Container\Exception\ServiceInstantiationException;
+use FireHub\Runtime;
+use ReflectionClass, ReflectionIntersectionType, ReflectionNamedType, ReflectionType, ReflectionUnionType;
+
+/**
+ * ### Validates contextual dependency binding registrations
+ *
+ * Performs static validation of contextual dependency configuration independently of dependency graph traversal.
+ *
+ * Checks consumer classes, dependency contracts, implementation availability, and statically determinable type
+ * compatibility without instantiating services or executing factory callbacks.
+ * @since 1.0.0
+ *
+ * @internal
+ */
+final readonly class ContextualBinding {
+
+ /**
+ * ### Constructor dependency plan cache
+ * @since 1.0.0
+ */
+ private DependencyPlanCache $plans;
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param \FireHub\Container\Registry $registry
+ * Registered service definitions.
+ *
+ * @param \FireHub\Container\Internal\ContextualBindingRegistry $bindings
+ * Registered contextual dependency bindings.
+ *
+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $parameters
+ * Registered constructor parameter bindings.
+ *
+ * @param \FireHub\Container\Internal\DependencyPlanCache $plans
+ * Compiled dependency plan cache.
+ *
+ *
+ * @return void
+ */
+ public function __construct (
+ private Registry $registry,
+ private ContextualBindingRegistry $bindings,
+ private ParameterBindingRegistry $parameters,
+ DependencyPlanCache $plans
+ ) {
+
+ $this->plans = $plans;
+
+ }
+
+ /**
+ * ### Validates contextual dependency bindings
+ *
+ * Inspects every contextual registration and reports structurally invalid bindings.
+ *
+ * Factory implementations are not executed because their return types cannot be verified statically.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\ContextualBindingRegistry::all() To retrieve contextual bindings.
+ * @uses \FireHub\Container\Validation\ContextualBinding::implementationOf() To resolve implementations.
+ * @uses \FireHub\Container\Validation\ContextualBinding::isUsed() To check if a dependency is used.
+ * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class.
+ * @uses \FireHub\Runtime\ObjectModel\Inspection::isInterface() To check if the identifier is an interface.
+ * @uses \FireHub\Runtime\ObjectModel\Relation::instanceOf() To check if the implementation satisfies the contract.
+ *
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If a constructor parameter contains invalid
+ * injection attributes or unsupported dependency metadata.
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Detected validation issues.
+ */
+ public function validate ():array {
+
+ $issues = [];
+
+ foreach ($this->bindings->all() as $consumer => $bindings) {
+
+ if (!Runtime\ObjectModel\Inspection::isClass($consumer)) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_CONTEXTUAL_BINDING,
+ Severity::ERROR,
+ $consumer,
+ "Contextual binding consumer '$consumer' does not exist."
+ );
+
+ continue;
+
+ }
+
+ $reflection = new ReflectionClass($consumer);
+
+ if (!$reflection->isInstantiable()) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_CONTEXTUAL_BINDING,
+ Severity::ERROR,
+ $consumer,
+ "Contextual binding consumer '$consumer' is not instantiable."
+ );
+
+ continue;
+
+ }
+
+ foreach ($bindings as $dependency => $implementation) {
+
+ if (
+ !Runtime\ObjectModel\Inspection::isClass($dependency)
+ && !Runtime\ObjectModel\Inspection::isInterface($dependency)
+ ) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_CONTEXTUAL_BINDING,
+ Severity::ERROR,
+ $consumer,
+ "Contextual dependency contract '$dependency' does not exist."
+ );
+
+ continue;
+
+ }
+
+ $resolved = $this->implementationOf($implementation);
+
+ if ($resolved === false) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_CONTEXTUAL_BINDING,
+ Severity::ERROR,
+ $consumer,
+ "Contextual implementation '$implementation' cannot be resolved."
+ );
+
+ continue;
+
+ }
+
+ if (
+ $resolved !== null
+ && !Runtime\ObjectModel\Relation::instanceOf($resolved, $dependency)
+ ) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_CONTEXTUAL_BINDING,
+ Severity::ERROR,
+ $consumer,
+ "Contextual implementation '$resolved' does not satisfy dependency '$dependency'."
+ );
+
+ continue;
+
+ }
+
+ try {
+
+ $used = $this->isUsed($consumer, $dependency);
+
+ } catch (ServiceInstantiationException) {
+
+ continue;
+
+ }
+
+ if (!$used) {
+
+ $issues[] = new Issue(
+ IssueType::UNUSED_CONTEXTUAL_BINDING,
+ Severity::WARNING,
+ $consumer,
+ "Contextual dependency '$dependency' is not used by constructor injection in '$consumer'."
+ );
+
+ }
+
+ }
+
+ }
+
+ return $issues;
+
+ }
+
+ /**
+ * ### Resolves a statically known contextual implementation
+ *
+ * Follows registered service aliases and returns their concrete implementation types.
+ *
+ * Returns null for registered factories because their runtime return types cannot be determined without
+ * executing the callback. Returns false for missing, invalid, or circular alias targets.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Registry::find() To retrieve service definitions.
+ * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class.
+ *
+ * @param class-string $id
+ * Contextual implementation identifier.
+ *
+ *
+ * @return class-string|false|null Statically known implementation, invalid target, or unverifiable factory.
+ */
+ private function implementationOf (string $id):string|false|null {
+
+ $visited = [];
+
+ while (true) {
+
+ if (isset($visited[$id])) return false;
+
+ $visited[$id] = true;
+
+ $definition = $this->registry->find($id);
+
+ if ($definition instanceof Alias) {
+
+ if (!$this->registry->has($definition->target)) return false;
+
+ $id = $definition->target;
+
+ continue;
+
+ }
+
+ if ($definition instanceof Cls) return $definition->class;
+
+ if ($definition instanceof Instance) return $definition->instance::class;
+
+ if ($definition instanceof Factory) return null;
+
+ if (!Runtime\ObjectModel\Inspection::isClass($id)) return false;
+
+ $reflection = new ReflectionClass($id);
+
+ return $reflection->isInstantiable()
+ ? $id
+ : false;
+
+ }
+
+ }
+
+ /**
+ * ### Checks whether a contextual binding can participate in constructor injection
+ *
+ * Inspects compiled constructor parameter metadata and determines whether a contextual dependency contract is
+ * referenced by an eligible parameter.
+ *
+ * Named, union, intersection, and DNF parameter types are inspected recursively.
+ *
+ * Explicit parameter bindings and collection injection attributes take precedence over contextual resolution.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\DependencyPlanCache::get() To retrieve compiled constructor metadata.
+ * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check explicit parameter bindings.
+ * @uses \FireHub\Container\Validation\ContextualBindingValidator::containsType() To inspect declared types.
+ *
+ * @param class-string $consumer
+ * Consumer implementation class.
+ *
+ * @param class-string $dependency
+ * Contextual dependency contract.
+ *
+ *
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If a constructor parameter contains invalid
+ * injection attributes or unsupported dependency metadata.
+ *
+ * @return bool True if the contextual binding can be selected for at least one constructor parameter.
+ */
+ private function isUsed (string $consumer, string $dependency):bool {
+
+ $plan = $this->plans->get($consumer);
+
+ foreach ($plan->parameters as $parameter) {
+
+ $name = $parameter->parameter->getName();
+
+ if ($this->parameters->has($consumer, $name))
+ continue;
+
+ if ($parameter->tag !== null || $parameter->collection !== null)
+ continue;
+
+ $type = $parameter->parameter->getType();
+
+ if (
+ $type !== null
+ && $this->containsType($type, $dependency)
+ ) return true;
+
+ }
+
+ return false;
+
+ }
+
+ /**
+ * ### Checks whether a declared parameter type references a dependency contract
+ *
+ * Traverses named, union, intersection, and DNF types recursively without resolving service instances.
+ * @since 1.0.0
+ *
+ * @param ReflectionType $type
+ * Declared constructor parameter type.
+ *
+ * @param class-string $dependency
+ * Required dependency contract.
+ *
+ *
+ * @return bool True if the dependency contract occurs within the declared type.
+ */
+ private function containsType (ReflectionType $type, string $dependency):bool {
+
+ if ($type instanceof ReflectionNamedType)
+ return !$type->isBuiltin() && $type->getName() === $dependency;
+
+ if (
+ $type instanceof ReflectionUnionType
+ || $type instanceof ReflectionIntersectionType
+ ) {
+
+ foreach ($type->getTypes() as $member)
+ if ($this->containsType($member, $dependency))
+ return true;
+
+ }
+
+ return false;
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Validation/DependencyGraph.php b/src/Validation/DependencyGraph.php
new file mode 100644
index 0000000..8293b64
--- /dev/null
+++ b/src/Validation/DependencyGraph.php
@@ -0,0 +1,992 @@
+
+ * @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\Validation;
+
+use FireHub\Container\Registry;
+use FireHub\Container\Definition\ {
+ Alias, Cls, Factory, Instance
+};
+use FireHub\Container\Internal\ {
+ CandidateSelector, CanonicalIdCache, ContextualBindingRegistry, ContextualCandidateSelector, DependencyPlan,
+ DependencyPlanCache, ParameterBindingRegistry, ParameterPlan, TagRegistry, TypeMatcher
+};
+use FireHub\Container\Exception\ {
+ AmbiguousDependencyException, ServiceInstantiationException
+};
+use FireHub\Runtime;
+use ReflectionIntersectionType, ReflectionUnionType;
+
+/**
+ * ### Validates constructor dependency graphs
+ *
+ * Performs static analysis of constructor dependencies using the same compiled dependency metadata used by the
+ * container's runtime autowiring system.
+ *
+ * Detects unresolved required dependencies and circular dependency relationships without constructing services,
+ * resolving instances, or executing factory callbacks.
+ *
+ * Explicit parameter bindings are respected and take precedence over automatic dependency resolution.
+ *
+ * This initial implementation analyzes named dependencies. Composite types, contextual bindings, and collection
+ * injection are reserved for later validation stages.
+ * @since 1.0.0
+ *
+ * @internal
+ */
+final readonly class DependencyGraph {
+
+ /**
+ * ### Compiled dependency plan cache
+ * @since 1.0.0
+ */
+ private DependencyPlanCache $plans;
+
+ /**
+ * ### Dependency type matcher
+ * @since 1.0.0
+ */
+ private TypeMatcher $matcher;
+
+ /**
+ * ### Contextual dependency candidate selector
+ *
+ * Selects contextual dependency implementations using the same rules as runtime dependency resolution.
+ * @since 1.0.0
+ */
+ private ContextualCandidateSelector $contextual_selector;
+
+ /**
+ * ### Dependency candidate selector
+ *
+ * Selects and validates dependency implementations using the same type compatibility rules as runtime resolution.
+ * @since 1.0.0
+ */
+ private CandidateSelector $selector;
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Registry::has() To check if a service is registered.
+ * @uses \FireHub\Container\Registry::implementations() To retrieve all registered service implementations.
+ *
+ * @param \FireHub\Container\Registry $registry
+ * Registered service definitions.
+ *
+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $bindings
+ * Explicit constructor parameter bindings.
+ *
+ * @param \FireHub\Container\Internal\ContextualBindingRegistry $contextual
+ * Contextual dependency binding registry.
+ *
+ * @param \FireHub\Container\Internal\TagRegistry $tags
+ * Registered service tags.
+ *
+ * @param \FireHub\Container\Internal\DependencyPlanCache $plans
+ * Compiled dependency plan cache.
+ *
+ * @param \FireHub\Container\Internal\TypeMatcher $matcher
+ * Dependency type matcher.
+ *
+ * @param \FireHub\Container\Internal\CanonicalIdCache $canonical
+ * Canonical service identifier cache.
+ *
+ *
+ * @return void
+ */
+ public function __construct (
+ private Registry $registry,
+ private ParameterBindingRegistry $bindings,
+ ContextualBindingRegistry $contextual,
+ private TagRegistry $tags,
+ DependencyPlanCache $plans,
+ TypeMatcher $matcher,
+ private CanonicalIdCache $canonical
+ ) {
+
+ $this->plans = $plans;
+
+ $this->matcher = $matcher;
+
+ $this->contextual_selector = new ContextualCandidateSelector(
+ $contextual
+ );
+
+ $this->selector = new CandidateSelector(
+ $this->matcher,
+ fn (string $id):bool => $this->registry->has($id),
+ fn ():iterable => $this->registry->implementations()
+ );
+
+ }
+
+ /**
+ * ### Validates registered constructor dependency graphs
+ *
+ * Traverses class-based service definitions and their statically resolvable constructor dependencies.
+ *
+ * Each dependency path is analyzed independently to detect cycles without executing the corresponding
+ * constructors.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspect() To validate dependency paths.
+ * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check if a class is constructible.
+ * @uses \FireHub\Container\Registry::all() To retrieve all registered service definitions.
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Detected dependency validation issues.
+ */
+ public function validate ():array {
+
+ $issues = []; $reported_cycles = [];
+ foreach ($this->registry->all() as $id => $definition) {
+
+ if (!$definition instanceof Cls)
+ continue;
+
+ if (!$this->matcher->isConstructible($definition->class))
+ continue;
+
+ $this->inspect(
+ $id,
+ [],
+ $issues,
+ $reported_cycles
+ );
+
+ }
+
+ return $issues;
+
+ }
+
+ /**
+ * ### Inspects a service dependency path
+ *
+ * Recursively examines constructor dependencies while tracking the active service path.
+ *
+ * A dependency is considered circular when its service identifier already appears in the active path.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\DependencyGraph::selectCompositeDependency() To select a composite dependency.
+ * @uses \FireHub\Container\Validation\DependencyGraph::reportCycle() To report a dependency cycle.
+ * @uses \FireHub\Container\Validation\DependencyGraph::canonicalId() To resolve a canonical service identifier.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspectTaggedDependencies() To validate tagged dependencies.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspectCollectionDependencies() To validate collection
+ * dependencies.
+ * @uses \FireHub\Container\Registry::find() To retrieve a service definition.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspect() To recursively validate child dependencies.
+ * @uses \FireHub\Container\Validation\DependencyGraph::selectContextualDependency() To select contextual
+ * dependencies.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspectContextualDependency() To validate contextual
+ * dependencies.
+ * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check if a class is constructible.
+ * @uses \FireHub\Container\Internal\DependencyPlanCache::get() To retrieve a compiled dependency plan.
+ * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check if a parameter is explicitly bound.
+ * @uses \FireHub\Runtime\Arr\Inspection::inArray() To check if a service identifier is already in the path.
+ * @uses \FireHub\Runtime\Str\SB\Delimiter::implode() To join path segments.
+ *
+ * @param class-string $id
+ * Service identifier being inspected.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported_cycles
+ * Canonical signatures of dependency cycles already reported during the current validation run.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the union contains an unsupported
+ * intersection member.
+ *
+ * @return void
+ */
+ private function inspect (string $id, array $path, array &$issues, array &$reported_cycles):void {
+
+ $canonical = $this->canonicalId($id);
+
+ if ($canonical === null) return;
+
+ $id = $canonical;
+
+ if (Runtime\Arr\Inspection::inArray($path, $id)) {
+
+ $this->reportCycle(
+ $id,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ return;
+
+ }
+
+ $path[] = $id;
+
+ $definition = $this->registry->find($id);
+
+ if ($definition instanceof Instance || $definition instanceof Factory)
+ return;
+
+ $class = $definition instanceof Cls
+ ? $definition->class
+ : $id;
+
+ if (!$this->matcher->isConstructible($class))
+ return;
+
+ $plan = $this->safeDependencyPlan(
+ $class,
+ $path,
+ $issues
+ );
+
+ if ($plan === null) return;
+
+ foreach ($plan->parameters as $parameter) {
+
+ $name = $parameter->parameter->getName();
+
+ if ($this->bindings->has($class, $name)) continue;
+
+ if ($parameter->tag !== null) {
+
+ $this->inspectTaggedDependencies(
+ $parameter->tag,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ continue;
+
+ }
+
+ if ($parameter->collection !== null) {
+
+ $this->inspectCollectionDependencies(
+ $parameter->collection,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ continue;
+
+ }
+
+ [$ambiguous, $contextual] = $this->selectContextualDependency(
+ $class,
+ $parameter,
+ $path,
+ $issues
+ );
+
+ if ($ambiguous) continue;
+
+ if ($contextual !== null) {
+
+ $this->inspectContextualDependency(
+ $class,
+ $contextual,
+ $parameter,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ continue;
+
+ }
+
+ if ($parameter->composite !== null) {
+
+ [$ambiguous, $dependency] = $this->selectCompositeDependency(
+ $class,
+ $parameter,
+ $path,
+ $issues
+ );
+
+ if ($ambiguous) continue;
+
+ } else $dependency = $parameter->id;
+
+ if ($dependency === null) {
+
+ if (!$parameter->hasDefault) {
+
+ $issues[] = new Issue(
+ IssueType::MISSING_DEPENDENCY,
+ Severity::ERROR,
+ $id,
+ "Required constructor parameter '$name' of '$class' cannot be automatically resolved.",
+ $path
+ );
+
+ }
+
+ continue;
+
+ }
+
+ if (
+ !$this->registry->has($dependency)
+ && !$this->matcher->isConstructible($dependency)
+ ) {
+
+ if (!$parameter->hasDefault) {
+
+ $issues[] = new Issue(
+ IssueType::MISSING_DEPENDENCY,
+ Severity::ERROR,
+ $id,
+ "Required dependency '$dependency' of '$class' is not registered or constructible.",
+ [...$path, $dependency]
+ );
+
+ }
+
+ continue;
+
+ }
+
+ $this->inspect(
+ $dependency,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ }
+
+ }
+
+ /**
+ * ### Selects a contextual constructor dependency
+ *
+ * Evaluates contextual bindings for the specified consumer and compiled constructor parameter.
+ *
+ * Contextual candidate selection follows the same rules as runtime dependency resolution, including support for
+ * named, union, intersection, and disjunctive normal form dependency types.
+ *
+ * Ambiguous contextual bindings are reported as validation issues without interrupting the validation of other
+ * services.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\ContextualCandidateSelector::select() To select contextual dependencies.
+ *
+ * @param class-string $consumer
+ * Concrete consumer implementation class.
+ *
+ * @param \FireHub\Container\Internal\ParameterPlan $parameter
+ * Compiled constructor parameter.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ *
+ * @return array{0: bool, 1: null|class-string} Whether contextual selection was ambiguous and the selected
+ * dependency identifier, if available.
+ */
+ private function selectContextualDependency (string $consumer, ParameterPlan $parameter, array $path, array &$issues):array {
+
+ try {
+
+ return [
+ false,
+ $this->contextual_selector->select(
+ $consumer,
+ $parameter
+ )
+ ];
+
+ } catch (AmbiguousDependencyException) {
+
+ $name = $parameter->parameter->getName();
+
+ $issues[] = new Issue(
+ IssueType::AMBIGUOUS_DEPENDENCY,
+ Severity::ERROR,
+ $consumer,
+ "Multiple contextual implementations are eligible for parameter '$name' of '$consumer'.",
+ $path
+ );
+
+ return [true, null];
+
+ }
+
+ }
+
+ /**
+ * ### Inspects a contextually selected dependency
+ *
+ * Validates a contextual dependency target and continues dependency graph traversal when its implementation
+ * can be determined statically.
+ *
+ * Registered factory targets cannot be fully inspected without execution.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Registry::has() To check if a service is registered.
+ * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check if a class is constructible.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspect() To recursively validate child dependencies.
+ * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation
+ * satisfies a dependency type.
+ * @uses \FireHub\Container\Registry::has() To check if a service is registered.
+ * @uses \FireHub\Container\Registry::implementations() To retrieve all registered service implementations.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspect() To recursively validate child dependencies.
+ * @uses \FireHub\Container\Validation\DependencyGraph::implementationOf() To find the statically known
+ * implementation.
+ * @uses \FireHub\Container\Internal\CandidateSelector::matchesType() To check if a service implementation.
+ *
+ * @param class-string $consumer
+ * Consumer implementation class.
+ *
+ * @param class-string $dependency
+ * Selected contextual dependency identifier.
+ *
+ * @param \FireHub\Container\Internal\ParameterPlan $parameter
+ * Compiled constructor parameter.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported_cycles
+ * Canonical signatures of dependency cycles already reported during the current validation run.
+ *
+ *
+ * @return void
+ */
+ private function inspectContextualDependency (string $consumer, string $dependency, ParameterPlan $parameter, array $path, array &$issues, array &$reported_cycles):void {
+
+ if (
+ !$this->registry->has($dependency)
+ && !$this->matcher->isConstructible($dependency)
+ ) {
+
+ $issues[] = new Issue(
+ IssueType::MISSING_DEPENDENCY,
+ Severity::ERROR,
+ $consumer,
+ "Contextual dependency '$dependency' of '$consumer' is not registered or constructible.",
+ [...$path, $dependency]
+ );
+
+ return;
+
+ }
+
+ $implementation = $this->implementationOf($dependency);
+
+ if ($implementation !== null) {
+
+ $type = $parameter->parameter->getType();
+
+ if (
+ $type !== null
+ && !$this->selector->matchesType(
+ $implementation,
+ $parameter->parameter,
+ $type
+ )
+ ) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_CONTEXTUAL_BINDING,
+ Severity::ERROR,
+ $consumer,
+ "Contextual implementation '$implementation' does not satisfy parameter '{$parameter->parameter->getName()}' of '$consumer'.",
+ [...$path, $dependency]
+ );
+
+ return;
+
+ }
+
+ }
+ $this->inspect(
+ $dependency,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ }
+
+ /**
+ * ### Finds the statically known implementation of a service
+ *
+ * Follows registered alias chains and returns the concrete implementation class when it can be determined without
+ * resolving the service.
+ *
+ * Factory definitions are excluded because their runtime return types cannot be guaranteed statically.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Registry::find() To retrieve a service definition.
+ * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check if a class is constructible.
+ *
+ * @param class-string $id
+ * Service identifier.
+ *
+ *
+ * @return null|class-string Statically known implementation class, or null when unavailable.
+ */
+ private function implementationOf (string $id):?string {
+
+ $visited = [];
+
+ while (true) {
+
+ if (isset($visited[$id]))
+ return null;
+
+ $visited[$id] = true;
+
+ $definition = $this->registry->find($id);
+
+ if ($definition instanceof Alias) {
+
+ $id = $definition->target;
+
+ continue;
+
+ }
+
+ if ($definition instanceof Cls)
+ return $definition->class;
+
+ if ($definition instanceof Instance)
+ return $definition->instance::class;
+
+ if ($definition instanceof Factory)
+ return null;
+
+ return $this->matcher->isConstructible($id)
+ ? $id
+ : null;
+
+ }
+
+ }
+
+ /**
+ * ### Resolves a canonical service identifier
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\Canonical::get() To retrieve the canonical identifier.
+ *
+ * @param class-string $id
+ * Service identifier.
+ *
+ *
+ * @return null|class-string Canonical service identifier.
+ */
+ private function canonicalId (string $id):?string {
+
+ return $this->canonical->get($id);
+
+ }
+
+ /**
+ * ### Safely retrieves a constructor dependency plan
+ *
+ * Compiles constructor dependency metadata and converts invalid dependency plans into validation issues.
+ *
+ * Compilation failures do not interrupt the validation of other registered services.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\DependencyPlanCache::get() To retrieve the dependency plan.
+ *
+ * @param class-string $class
+ * Implementation class.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ *
+ * @return null|\FireHub\Container\Internal\DependencyPlan Compiled dependency plan, or null when invalid.
+ */
+ private function safeDependencyPlan (string $class, array $path, array &$issues):?DependencyPlan {
+
+ try {
+
+ return $this->plans->get($class);
+
+ } catch (ServiceInstantiationException $exception) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_DEPENDENCY_PLAN,
+ Severity::ERROR,
+ $class,
+ "Unable to compile constructor dependency plan for '$class': {$exception->getMessage()}",
+ $path
+ );
+
+ return null;
+
+ }
+
+ }
+
+ /**
+ * ### Reports a unique circular dependency
+ *
+ * Normalizes directed dependency cycles to prevent duplicate reports caused by different traversal starting points.
+ *
+ * Cyclic rotations are considered equivalent, while reversed paths remain distinct.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Runtime\Arr\Access::search() To find the starting index of a cycle.
+ * @uses \FireHub\Runtime\Arr\Structure::slice() To extract a subarray of a cycle.
+ * @uses \FireHub\Runtime\Arr\Inspection::count() To determine the length of a cycle.
+ * @uses \FireHub\Runtime\Arr\Ordering::sort() To sort a cycle signature array.
+ * @uses \FireHub\Runtime\Str\SB\Delimiter::implode() To join a cycle signature array.
+ * @uses \FireHub\Runtime\Data::serialize() To serialize signatures.
+ *
+ * @param class-string $id
+ * Service identifier closing the cycle.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported_cycles
+ * Previously reported cycle signatures.
+ *
+ *
+ * @return void
+ */
+ private function reportCycle (string $id, array $path, array &$issues, array &$reported_cycles):void {
+
+ $start = Runtime\Arr\Access::search($path, $id);
+
+ if ($start === false)
+ return;
+
+ $cycle = Runtime\Arr\Structure::slice($path, $start);
+
+ $count = Runtime\Arr\Inspection::count($cycle);
+
+ if ($count === 0) return;
+
+ $signatures = [];
+
+ for ($index = 0; $index < $count; $index++) {
+
+ $rotation = [
+ ...Runtime\Arr\Structure::slice($cycle, $index),
+ ...Runtime\Arr\Structure::slice($cycle, 0, $index)
+ ];
+
+ $signatures[] = Runtime\Data::serialize($rotation);
+
+ }
+
+ Runtime\Arr\Ordering::sort($signatures, type: Runtime\Type\Arr\SortType::STRING);
+
+ /** @var non-empty-string $signature */
+ $signature = $signatures[0]; // @phpstan-ignore offsetAccess.notFound
+
+ if (isset($reported_cycles[$signature])) return;
+
+ $reported_cycles[$signature] = true;
+
+ $cycle[] = $id;
+
+ $issues[] = new Issue(
+ IssueType::CIRCULAR_DEPENDENCY,
+ Severity::ERROR,
+ $id,
+ 'Circular dependency detected: ' . Runtime\Str\SB\Delimiter::implode($cycle, ' -> '),
+ $cycle
+ );
+
+ }
+
+ /**
+ * ### Selects a composite constructor dependency
+ *
+ * Resolves a statically identifiable service candidate for a constructor parameter declared with a union,
+ * intersection, or disjunctive normal form type.
+ *
+ * Candidate selection follows the same rules as runtime dependency resolution by delegating to the shared
+ * candidate selector.
+ *
+ * Union types require a uniquely identifiable eligible implementation. Intersection types require an implementation
+ * satisfying every declared constituent type. Disjunctive normal form types are evaluated through their union
+ * representation, allowing intersection branches to participate in candidate selection.
+ *
+ * Ambiguous candidate selection is converted into a validation issue without interrupting dependency graph analysis.
+ *
+ * The method does not instantiate services, execute factories, or modify container registrations.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\CandidateSelector::union() To select union and DNF dependencies.
+ * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select intersection dependencies.
+ *
+ * @param class-string $consumer
+ * Concrete consumer implementation class.
+ *
+ * @param \FireHub\Container\Internal\ParameterPlan $parameter
+ * Compiled constructor parameter.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the union contains an unsupported
+ * intersection member.
+ *
+ * @return array{0: bool, 1: null|class-string} Whether candidate selection was ambiguous and the selected
+ * dependency identifier, if available.
+ */
+ private function selectCompositeDependency (string $consumer, ParameterPlan $parameter, array $path, array &$issues):array {
+
+ $type = $parameter->composite;
+
+ if ($type === null)
+ return [false, null];
+
+ try {
+
+ $dependency = match (true) {
+
+ $type instanceof ReflectionUnionType => $this->selector->union(
+ $parameter->parameter,
+ $type
+ ),
+
+ $type instanceof ReflectionIntersectionType => $this->selector->intersection( // @phpstan-ignore instanceof.alwaysTrue
+ $parameter->parameter,
+ $type
+ )
+
+ };
+
+ return [false, $dependency];
+
+ } catch (AmbiguousDependencyException) {
+
+ $name = $parameter->parameter->getName();
+
+ $issues[] = new Issue(
+ IssueType::AMBIGUOUS_DEPENDENCY,
+ Severity::ERROR,
+ $consumer,
+ "Multiple implementations are eligible for composite parameter '$name' of '$consumer'.",
+ $path
+ );
+
+ return [true, null];
+
+ }
+
+ }
+
+ /**
+ * ### Inspects tagged constructor dependencies
+ *
+ * Recursively validates all services associated with the specified tag.
+ *
+ * Tagged service identifiers are retrieved in registration order using the shared tag registry. Each service is
+ * inspected within the active dependency path to detect missing dependencies, invalid dependency plans, and circular
+ * constructor dependencies.
+ *
+ * An unknown or empty tag produces no dependency validation issues because tagged injection permits empty
+ * collections.
+ *
+ * Service instances are not constructed, and factory callbacks are never executed during validation.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\TagRegistry::get() To retrieve tagged service identifiers.
+ * @uses \FireHub\Container\Registry::has() To verify registered service identifiers.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspect() To recursively validate tagged dependencies.
+ * @uses \FireHub\Runtime\Arr\Inspection::count() To determine the number of tagged services.
+ *
+ * @param non-empty-string $tag
+ * Tag identifying the registered services.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported_cycles
+ * Previously reported dependency cycle signatures.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the union contains an unsupported
+ * intersection member.
+ *
+ * @return void
+ */
+ private function inspectTaggedDependencies (string $tag, array $path, array &$issues, array &$reported_cycles):void {
+
+ foreach ($this->tags->get($tag) as $dependency) {
+
+ if (!$this->registry->has($dependency)) {
+
+ $issues[] = new Issue(
+ IssueType::MISSING_DEPENDENCY,
+ Severity::ERROR,
+ $path[Runtime\Arr\Inspection::count($path) - 1], // @phpstan-ignore offsetAccess.notFound
+ "Tagged dependency '$dependency' associated with tag '$tag' is not registered.",
+ [...$path, $dependency]
+ );
+
+ continue;
+
+ }
+
+ $this->inspect(
+ $dependency,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ }
+
+ }
+
+ /**
+ * ### Inspects collection constructor dependencies
+ *
+ * Recursively validates registered services matching the requested collection element type.
+ *
+ * Service identifiers are resolved to their canonical targets before candidate selection. Duplicate canonical
+ * targets are ignored, preserving the same collection membership rules used by runtime resolution.
+ *
+ * Matching services are inspected without constructing instances or executing factory callbacks.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Registry::all() To retrieve registered service definitions.
+ * @uses \FireHub\Container\Validation\DependencyGraph::canonicalId() To resolve canonical service identifiers.
+ * @uses \FireHub\Container\Validation\DependencyGraph::matchesCollectionType() To check collection compatibility.
+ * @uses \FireHub\Container\Validation\DependencyGraph::inspect() To recursively validate matching dependencies.
+ *
+ * @param class-string $type
+ * Required collection element type.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported_cycles
+ * Previously reported dependency cycle signatures.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the union contains an unsupported
+ * intersection member.
+ *
+ * @return void
+ */
+ private function inspectCollectionDependencies (string $type, array $path, array &$issues, array &$reported_cycles):void {
+
+ /** @var array $resolved */
+ $resolved = [];
+
+ foreach ($this->registry->all() as $id => $definition) {
+
+ $target = $this->canonicalId($id);
+
+ if ($target === null) continue;
+
+ if (isset($resolved[$target])) continue;
+
+ if (!$this->matchesCollectionType($id, $target, $type)) continue;
+
+ $this->inspect(
+ $id,
+ $path,
+ $issues,
+ $reported_cycles
+ );
+
+ $resolved[$target] = true;
+
+ }
+
+ }
+
+ /**
+ * ### Checks collection candidate compatibility
+ *
+ * Determines whether a registered service satisfies the requested collection element type.
+ *
+ * Compatibility is evaluated against the original service identifier, its canonical identifier, and the statically
+ * known implementation type.
+ *
+ * Factory definitions without a statically known implementation are matched only through their service identifiers.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\TypeMatcher::matches() To check type compatibility.
+ * @uses \FireHub\Container\Registry::find() To retrieve canonical service definitions.
+ *
+ * @param class-string $id
+ * Registered service identifier.
+ *
+ * @param class-string $target
+ * Canonical service identifier.
+ *
+ * @param class-string $type
+ * Required collection element type.
+ *
+ *
+ * @return bool True if the registered service matches the collection element type.
+ */
+ private function matchesCollectionType (string $id, string $target, string $type):bool {
+
+ if ($this->matcher->matches($id, $type))
+ return true;
+
+ if ($this->matcher->matches($target, $type))
+ return true;
+
+ $definition = $this->registry->find($target);
+
+ return match (true) {
+ $definition instanceof Cls => $this->matcher->matches($definition->class, $type),
+ $definition instanceof Instance => $definition->instance instanceof $type,
+ default => false
+ };
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Validation/Issue.php b/src/Validation/Issue.php
new file mode 100644
index 0000000..1244541
--- /dev/null
+++ b/src/Validation/Issue.php
@@ -0,0 +1,83 @@
+
+ * @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\Validation;
+
+/**
+ * ### Represents a container validation issue
+ *
+ * Describes a configuration problem or warning discovered during static analysis of service definitions.
+ *
+ * Validation issues are immutable and contain enough diagnostic information to identify the affected service and
+ * dependency path.
+ * @since 1.0.0
+ *
+ * @phpstan-type DependencyPath list
+ */
+final readonly class Issue {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param \FireHub\Container\Validation\IssueType $type
+ * Category of the validation issue.
+ *
+ * @param \FireHub\Container\Validation\Severity $severity
+ * Severity of the validation issue.
+ *
+ * @param non-empty-string $service
+ * Identifier of the affected service.
+ *
+ * @param non-empty-string $message
+ * Human-readable diagnostic message.
+ *
+ * @param list $path [optional]
+ * Dependency path associated with the issue.
+ *
+ *
+ * @return void
+ */
+ public function __construct (
+ public IssueType $type,
+ public Severity $severity,
+ public string $service,
+ public string $message,
+ public array $path = []
+ ) {}
+
+ /**
+ * ### Checks whether the issue is an error
+ * @since 1.0.0
+ *
+ * @return bool True if the issue represents an error.
+ */
+ public function isError ():bool {
+
+ return $this->severity === Severity::ERROR;
+
+ }
+
+ /**
+ * ### Checks whether the issue is a warning
+ * @since 1.0.0
+ *
+ * @return bool True if the issue represents a warning.
+ */
+ public function isWarning ():bool {
+
+ return $this->severity === Severity::WARNING;
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Validation/IssueType.php b/src/Validation/IssueType.php
new file mode 100644
index 0000000..f12f6d1
--- /dev/null
+++ b/src/Validation/IssueType.php
@@ -0,0 +1,162 @@
+
+ * @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\Validation;
+
+/**
+ * ### Defines container validation issue types
+ *
+ * Categorizes configuration and dependency problems discovered during static validation of dependency injection
+ * service definitions.
+ *
+ * Validation issue types identify the nature of a detected problem independently of its severity, affected service,
+ * diagnostic message, or dependency path.
+ *
+ * The validation process does not instantiate services, execute factories, or modify container configuration.
+ *
+ * Some issues represent confirmed configuration errors, while others indicate limitations of static analysis.
+ * Their severity is determined separately through ValidationSeverity.
+ * @since 1.0.0
+ */
+enum IssueType {
+
+ /**
+ * ### Missing class
+ *
+ * A referenced class, interface, or other required type cannot be found.
+ * @since 1.0.0
+ */
+ case MISSING_CLASS;
+
+ /**
+ * ### Non-instantiable class
+ *
+ * A class-based service definition references a type that cannot be instantiated directly, such as an abstract
+ * class or interface, without a suitable concrete implementation.
+ * @since 1.0.0
+ */
+ case NON_INSTANTIABLE_CLASS;
+
+ /**
+ * ### Type mismatch
+ *
+ * A service definition or binding references an implementation that is incompatible with its declared service
+ * type.
+ * @since 1.0.0
+ */
+ case TYPE_MISMATCH;
+
+ /**
+ * ### Missing dependency
+ *
+ * A required dependency cannot be satisfied through registered bindings, explicit parameter bindings,
+ * constructor defaults, or supported automatic resolution.
+ * @since 1.0.0
+ */
+ case MISSING_DEPENDENCY;
+
+ /**
+ * ### Ambiguous dependency
+ *
+ * Multiple dependency resolution candidates satisfy a required type, and the available configuration does not
+ * identify a unique implementation.
+ *
+ * This includes ambiguity involving union, intersection, and disjunctive normal form types.
+ * @since 1.0.0
+ */
+ case AMBIGUOUS_DEPENDENCY;
+
+ /**
+ * ### Circular dependency
+ *
+ * A dependency graph contains a cycle that prevents the involved services from being constructed through
+ * ordinary dependency resolution.
+ * @since 1.0.0
+ */
+ case CIRCULAR_DEPENDENCY;
+
+ /**
+ * ### Invalid alias
+ *
+ * An alias references a target that cannot be resolved according to the container's alias resolution rules.
+ * @since 1.0.0
+ */
+ case INVALID_ALIAS;
+
+ /**
+ * ### Circular alias
+ *
+ * Two or more service aliases form a resolution cycle without reaching a concrete service definition.
+ * @since 1.0.0
+ */
+ case CIRCULAR_ALIAS;
+
+ /**
+ * ### Invalid parameter binding
+ *
+ * An explicit constructor parameter binding references an unknown parameter or provides a value that is
+ * incompatible with the parameter's declared type.
+ * @since 1.0.0
+ */
+ case INVALID_PARAMETER_BINDING;
+
+ /**
+ * ### Invalid contextual binding
+ *
+ * A contextual dependency binding references an invalid consumer, dependency, or implementation, or defines
+ * a relationship incompatible with the dependency's declared type.
+ * @since 1.0.0
+ */
+ case INVALID_CONTEXTUAL_BINDING;
+
+ /**
+ * ### Lifetime mismatch
+ *
+ * A service dependency relationship violates the container's lifetime rules.
+ *
+ * Lifetime compatibility must be evaluated according to the actual resolution and ownership semantics of
+ * the container rather than assuming that every dependency between different lifetimes is invalid.
+ * @since 1.0.0
+ */
+ case LIFETIME_MISMATCH;
+
+ /**
+ * ### Unverifiable factory
+ *
+ * A factory-based service definition cannot be fully validated without executing its factory callback.
+ *
+ * This issue indicates a limitation of static analysis rather than a confirmed configuration error.
+ * @since 1.0.0
+ */
+ case UNVERIFIABLE_FACTORY;
+
+ /**
+ * ### Invalid dependency plan
+ *
+ * Constructor dependency metadata cannot be compiled because of invalid or unsupported injection configuration.
+ * @since 1.0.0
+ */
+ case INVALID_DEPENDENCY_PLAN;
+
+ /**
+ * ### Unused contextual binding
+ *
+ * A contextual dependency binding is structurally valid but cannot participate in constructor dependency resolution
+ * because its dependency contract is not referenced by an eligible constructor parameter.
+ *
+ * This includes bindings shadowed by higher-priority explicit parameter bindings or collection injection attributes.
+ * @since 1.0.0
+ */
+ case UNUSED_CONTEXTUAL_BINDING;
+
+}
\ No newline at end of file
diff --git a/src/Validation/Lifetime.php b/src/Validation/Lifetime.php
new file mode 100644
index 0000000..8031c8b
--- /dev/null
+++ b/src/Validation/Lifetime.php
@@ -0,0 +1,573 @@
+
+ * @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\Validation;
+
+use FireHub\Core\Meta\Enum\Container\Lifetime as LifetimeService;
+use FireHub\Container\Registry;
+use FireHub\Container\Definition\ {
+ Cls, Factory, Instance
+};
+use FireHub\Container\Internal\ {
+ CandidateSelector, CanonicalIdCache, ContextualBindingRegistry, ContextualCandidateSelector, DependencyPlanCache,
+ ParameterBindingRegistry, ParameterPlan, TagRegistry, TypeMatcher
+};
+use FireHub\Container\Exception\ {
+ AmbiguousDependencyException, ServiceInstantiationException
+};
+use FireHub\Runtime;
+use ReflectionIntersectionType, ReflectionUnionType;
+
+/**
+ * ### Validates service lifetime relationships
+ *
+ * Performs static analysis of constructor dependency graphs to detect singleton services that capture scoped
+ * dependencies.
+ *
+ * Supports direct and transitive dependency relationships without constructing services or executing factories.
+ *
+ * Explicit constructor parameter bindings are excluded because their values are supplied independently of
+ * automatic dependency resolution.
+ *
+ * This initial implementation analyzes named constructor dependencies. Composite, contextual, tagged, and
+ * collection dependencies are reserved for later validation stages.
+ * @since 1.0.0
+ *
+ * @internal
+ */
+final readonly class Lifetime {
+
+ /**
+ * ### Compiled dependency plan cache
+ * @since 1.0.0
+ */
+ private DependencyPlanCache $plans;
+
+ /**
+ * ### Dependency type matcher
+ * @since 1.0.0
+ */
+ private TypeMatcher $matcher;
+
+ /**
+ * ### Contextual dependency candidate selector
+ * @since 1.0.0
+ */
+ private ContextualCandidateSelector $contextual_selector;
+
+ /**
+ * ### Composite dependency candidate selector
+ * @since 1.0.0
+ */
+ private CandidateSelector $selector;
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param \FireHub\Container\Registry $registry
+ * Registered service definitions.
+ *
+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $bindings
+ * Explicit constructor parameter bindings.
+ *
+ * @param \FireHub\Container\Internal\ContextualBindingRegistry $contextual
+ * Contextual dependency binding registry.
+ *
+ * @param \FireHub\Container\Internal\TagRegistry $tags
+ * Registered service tags.
+ *
+ * @param \FireHub\Container\Internal\DependencyPlanCache $plans
+ * Compiled dependency plans.
+ *
+ * @param \FireHub\Container\Internal\TypeMatcher $matcher
+ * Dependency type matcher.
+ *
+ * @param \FireHub\Container\Internal\CanonicalIdCache $canonical
+ * Canonical service identifier cache.
+ *
+ *
+ * @return void
+ */
+ public function __construct (
+ private Registry $registry,
+ private ParameterBindingRegistry $bindings,
+ ContextualBindingRegistry $contextual,
+ private TagRegistry $tags,
+ DependencyPlanCache $plans,
+ TypeMatcher $matcher,
+ private CanonicalIdCache $canonical
+ ) {
+
+ $this->plans = $plans;
+
+ $this->matcher = $matcher;
+
+ $this->contextual_selector = new ContextualCandidateSelector(
+ $contextual
+ );
+
+ $this->selector = new CandidateSelector(
+ $this->matcher,
+ fn (string $id):bool => $this->registry->has($id),
+ fn ():iterable => $this->registry->implementations()
+ );
+
+ }
+
+ /**
+ * ### Validates registered service lifetime relationships
+ *
+ * Traverses singleton dependency graphs and reports scoped dependencies captured directly or transitively.
+ *
+ * Each singleton is analyzed independently, and repeated dependency paths are deduplicated.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\Lifetime::inspect() To validate dependency paths.
+ * @uses \FireHub\Container\Registry::all() To retrieve all registered service definitions.
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the composite type is unsupported.
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Detected lifetime issues.
+ */
+ public function validate ():array {
+
+ $issues = []; $reported = [];
+ foreach ($this->registry->all() as $id => $definition) {
+
+ if (!$definition instanceof Cls) continue;
+
+ if ($definition->lifetime() !== LifetimeService::SINGLETON) continue;
+
+ $this->inspect(
+ $id,
+ $id,
+ [],
+ $issues,
+ $reported
+ );
+
+ }
+
+ return $issues;
+
+ }
+
+ /**
+ * ### Inspects a singleton dependency path
+ *
+ * Traverses statically resolvable constructor dependencies and reports scoped services encountered along
+ * the active path.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\Lifetime::canonicalId() To resolve a canonical service identifier.
+ * @uses \FireHub\Container\Registry::find() To retrieve a service definition.
+ * @uses \FireHub\Container\Definition\Cls::lifetime() To check the service lifetime.
+ * @uses \FireHub\Container\Internal\TypeMatcher::isConstructible() To check if a class is constructible.
+ * @uses \FireHub\Container\Internal\DependencyPlanCache::get() To retrieve a dependency plan.
+ * @uses \FireHub\Container\Internal\ParameterBindingRegistry::has() To check if a parameter is explicitly bound.
+ * @uses \FireHub\Runtime\Arr\Inspection::inArray() To check if a service identifier is already in the path.
+ * @uses \FireHub\Container\Validation\Lifetime::inspectTagged() To recursively validate tagged dependencies.
+ * @uses \FireHub\Container\Validation\Lifetime::inspectCollection() To recursively validate collection dependencies.
+ * @uses \FireHub\Container\Validation\Lifetime::selectComposite() To select composite dependencies.
+
+ *
+ * @param class-string $root
+ * Root singleton service identifier.
+ *
+ * @param class-string $id
+ * Current dependency identifier.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported
+ * Previously reported lifetime relationships.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the composite type is unsupported.
+ *
+ * @return void
+ */
+ private function inspect (string $root, string $id, array $path, array &$issues, array &$reported):void {
+
+ $id = $this->canonicalId($id);
+
+ if ($id === null) return;
+
+ if (Runtime\Arr\Inspection::inArray($path, $id)) return;
+
+ $path[] = $id;
+
+ $definition = $this->registry->find($id);
+
+ if ($definition instanceof Cls) {
+
+ if ($definition->lifetime() === LifetimeService::SCOPED) {
+
+ $key = $root . "\0" . $id;
+
+ if (!isset($reported[$key])) {
+
+ $reported[$key] = true;
+
+ $issues[] = new Issue(
+ IssueType::LIFETIME_MISMATCH,
+ Severity::ERROR,
+ $root,
+ "Singleton service '$root' captures scoped dependency '$id'.",
+ $path
+ );
+
+ }
+
+ return;
+
+ }
+
+ $class = $definition->class;
+
+ } elseif ($definition instanceof Instance || $definition instanceof Factory) {
+
+ return;
+
+ } else {
+
+ $class = $id;
+
+ }
+
+ if (!$this->matcher->isConstructible($class))
+ return;
+
+ try {
+
+ $plan = $this->plans->get($class);
+
+ } catch (ServiceInstantiationException) {
+
+ return;
+
+ }
+
+ foreach ($plan->parameters as $parameter) {
+
+ $name = $parameter->parameter->getName();
+
+ if ($this->bindings->has($class, $name))
+ continue;
+
+ if ($parameter->tag !== null) {
+
+ $this->inspectTagged(
+ $root,
+ $parameter->tag,
+ $path,
+ $issues,
+ $reported
+ );
+
+ continue;
+
+ }
+
+ if ($parameter->collection !== null) {
+
+ $this->inspectCollection(
+ $root,
+ $parameter->collection,
+ $path,
+ $issues,
+ $reported
+ );
+
+ continue;
+
+ }
+
+ try {
+
+ $dependency = $this->contextual_selector->select(
+ $class,
+ $parameter
+ );
+
+ } catch (AmbiguousDependencyException) {
+
+ continue;
+
+ }
+
+ if ($dependency === null) {
+
+ if ($parameter->composite !== null) {
+
+ try {
+
+ $dependency = $this->selectComposite($parameter);
+
+ } catch (AmbiguousDependencyException) {
+
+ continue;
+
+ }
+
+ } else {
+
+ $dependency = $parameter->id;
+
+ }
+
+ }
+
+ if ($dependency === null) continue;
+
+ if (
+ !$this->registry->has($dependency)
+ && !$this->matcher->isConstructible($dependency)
+ ) continue;
+
+ $this->inspect(
+ $root,
+ $dependency,
+ $path,
+ $issues,
+ $reported
+ );
+
+ }
+
+ }
+
+ /**
+ * ### Resolves a canonical service identifier
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\Canonical::get() To retrieve the canonical identifier.
+ *
+ * @param class-string $id
+ * Service identifier.
+ *
+ *
+ * @return null|class-string Canonical service identifier.
+ */
+ private function canonicalId (string $id):?string {
+
+ return $this->canonical->get($id);
+
+ }
+
+ /**
+ * ### Selects a composite dependency
+ *
+ * Resolves a unique service candidate for union, intersection, and DNF constructor parameter types.
+ *
+ * Candidate selection follows the same rules as runtime dependency resolution.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\CandidateSelector::union() To select union and DNF dependencies.
+ * @uses \FireHub\Container\Internal\CandidateSelector::intersection() To select intersection dependencies.
+ *
+ * @param \FireHub\Container\Internal\ParameterPlan $parameter
+ * Compiled constructor parameter.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the composite type is unsupported.
+ *
+ * @return null|class-string Selected dependency identifier.
+ */
+ private function selectComposite (ParameterPlan $parameter):?string {
+
+ $type = $parameter->composite;
+
+ if ($type === null)
+ return null;
+
+ if ($type instanceof ReflectionUnionType)
+ return $this->selector->union(
+ $parameter->parameter,
+ $type
+ );
+
+ if ($type instanceof ReflectionIntersectionType) // @phpstan-ignore instanceof.alwaysTrue
+ return $this->selector->intersection(
+ $parameter->parameter,
+ $type
+ );
+
+ return null; // @phpstan-ignore deadCode.unreachable
+
+ }
+
+ /**
+ * ### Inspects tagged dependency lifetimes
+ *
+ * Traverses registered services associated with a tag and validates their lifetime relationships.
+ *
+ * Missing services are ignored because dependency graph validation reports them separately.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\TagRegistry::get() To retrieve tagged services.
+ * @uses \FireHub\Container\Registry::has() To check if a tagged service exists.
+ * @uses \FireHub\Container\Validation\Lifetime::inspect() To inspect dependency lifetimes.
+ *
+ * @param class-string $root
+ * Root singleton service identifier.
+ *
+ * @param non-empty-string $tag
+ * Service tag.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported
+ * Previously reported lifetime relationships.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the composite type is unsupported.
+ *
+ * @return void
+ */
+ private function inspectTagged (string $root, string $tag, array $path, array &$issues, array &$reported):void {
+
+ foreach ($this->tags->get($tag) as $dependency) {
+
+ if (!$this->registry->has($dependency))
+ continue;
+
+ $this->inspect(
+ $root,
+ $dependency,
+ $path,
+ $issues,
+ $reported
+ );
+
+ }
+
+ }
+
+ /**
+ * ### Inspects collection dependency lifetimes
+ *
+ * Traverses all registered services compatible with the requested collection element type.
+ *
+ * Alias targets are deduplicated, and factory callbacks are never executed.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Registry::all() To retrieve registered services.
+ * @uses \FireHub\Container\Validation\Lifetime::canonicalId() To resolve aliases.
+ * @uses \FireHub\Container\Validation\Lifetime::matchesCollectionType() To check collection compatibility.
+ * @uses \FireHub\Container\Validation\Lifetime::inspect() To inspect dependency lifetimes.
+ *
+ * @param class-string $root
+ * Root singleton service identifier.
+ *
+ * @param class-string $type
+ * Requested collection element type.
+ *
+ * @param list $path
+ * Active dependency path.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param array $reported
+ * Previously reported lifetime relationships.
+ *
+ *
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the composite type is unsupported.
+ *
+ * @return void
+ */
+ private function inspectCollection (string $root, string $type, array $path, array &$issues, array &$reported):void {
+
+ /** @var array $resolved */
+ $resolved = [];
+
+ foreach ($this->registry->all() as $id => $definition) {
+
+ $target = $this->canonicalId($id);
+
+ if ($target === null) continue;
+
+ if (isset($resolved[$target])) continue;
+
+ if (!$this->matchesCollectionType($id, $target, $type)) continue;
+
+ $resolved[$target] = true;
+
+ $this->inspect(
+ $root,
+ $id,
+ $path,
+ $issues,
+ $reported
+ );
+
+ }
+
+ }
+
+ /**
+ * ### Checks collection candidate compatibility
+ *
+ * Matches registered service identifiers, canonical identifiers, and known implementation types against a
+ * requested collection element contract.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\TypeMatcher::matches() To check type compatibility.
+ * @uses \FireHub\Container\Registry::find() To retrieve registered definitions.
+ *
+ * @param class-string $id
+ * Original service identifier.
+ *
+ * @param class-string $target
+ * Canonical service identifier.
+ *
+ * @param class-string $type
+ * Requested collection element type.
+ *
+ *
+ * @return bool True if the service matches the collection element type.
+ */
+ private function matchesCollectionType (string $id, string $target, string $type):bool {
+
+ if ($this->matcher->matches($id, $type))
+ return true;
+
+ if ($this->matcher->matches($target, $type))
+ return true;
+
+ $definition = $this->registry->find($target);
+
+ return match (true) {
+ $definition instanceof Cls => $this->matcher->matches($definition->class, $type),
+ $definition instanceof Instance => $definition->instance instanceof $type,
+ default => false
+ };
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Validation/ParameterBinding.php b/src/Validation/ParameterBinding.php
new file mode 100644
index 0000000..eb57136
--- /dev/null
+++ b/src/Validation/ParameterBinding.php
@@ -0,0 +1,262 @@
+
+ * @copyright 2026-present The FireHub Project - All rights reserved
+ * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
+ *
+ * @php-version >=8.2
+ * @package Container
+ */
+
+namespace FireHub\Container\Validation;
+
+use FireHub\Container\Internal\ParameterBindingRegistry;
+use ReflectionClass, ReflectionException, ReflectionIntersectionType, ReflectionNamedType, ReflectionParameter,
+ ReflectionType, ReflectionUnionType;
+
+/**
+ * ### Validates explicit constructor parameter bindings
+ *
+ * Performs static validation of configured constructor parameter values without instantiating services.
+ *
+ * Validates parameter existence and compatibility with declared PHP parameter types, including nullable, union,
+ * intersection, and disjunctive normal form types.
+ *
+ * Validation does not modify bindings or execute application code.
+ * @since 1.0.0
+ *
+ * @internal
+ */
+final readonly class ParameterBinding {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $bindings
+ * Registered constructor parameter bindings.
+ *
+ *
+ * @return void
+ */
+ public function __construct (
+ private ParameterBindingRegistry $bindings
+ ) {}
+
+ /**
+ * ### Validates registered parameter bindings
+ *
+ * Checks that configured constructor parameters exist and their values satisfy the declared parameter types.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Internal\ParameterBindingRegistry::all() To retrieve registered bindings.
+ * @uses \FireHub\Container\Validation\ParameterBinding::findParameter() To find constructor parameters.
+ * @uses \FireHub\Container\Validation\ParameterBinding::matchesType() To check value compatibility.
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Detected validation issues.
+ */
+ public function validate ():array {
+
+ $issues = [];
+
+ foreach ($this->bindings->all() as $class => $bindings) {
+
+ try {
+
+ $reflection = new ReflectionClass($class);
+
+ } catch (ReflectionException) { // @phpstan-ignore catch.neverThrown
+
+ $issues[] = new Issue(
+ IssueType::INVALID_PARAMETER_BINDING,
+ Severity::ERROR,
+ $class,
+ "Parameter bindings reference an unknown class '$class'."
+ );
+
+ continue;
+
+ }
+
+ $constructor = $reflection->getConstructor();
+
+ foreach ($bindings as $name => $value) {
+
+ $parameter = $constructor !== null
+ ? $this->findParameter($constructor->getParameters(), $name)
+ : null;
+
+ if ($parameter === null) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_PARAMETER_BINDING,
+ Severity::ERROR,
+ $class,
+ "Constructor parameter '$name' does not exist in '$class'."
+ );
+
+ continue;
+
+ }
+
+ $type = $parameter->getType();
+
+ if (
+ $type !== null
+ && !$this->matchesType($parameter, $type, $value)
+ ) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_PARAMETER_BINDING,
+ Severity::ERROR,
+ $class,
+ "Bound value for parameter '$name' of '$class' is incompatible with type '$type'."
+ );
+
+ }
+
+ }
+
+ }
+
+ return $issues;
+
+ }
+
+ /**
+ * ### Finds a constructor parameter
+ * @since 1.0.0
+ *
+ * @param list $parameters
+ * Constructor parameters.
+ *
+ * @param non-empty-string $name
+ * Parameter name.
+ *
+ *
+ * @return null|ReflectionParameter Matching parameter.
+ */
+ private function findParameter (array $parameters, string $name):?ReflectionParameter {
+
+ foreach ($parameters as $parameter)
+ if ($parameter->getName() === $name)
+ return $parameter;
+
+ return null;
+
+ }
+
+ /**
+ * ### Checks parameter value compatibility
+ *
+ * Evaluates whether an explicitly bound value satisfies the declared PHP parameter type.
+ *
+ * Union types require at least one matching member, while intersection types require every member to match.
+ * Nested combinations are evaluated recursively to support disjunctive normal form types.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\ParameterBinding::matchesNamedType() To validate named types.
+ *
+ * @param \ReflectionParameter $parameter
+ * Constructor parameter.
+ *
+ * @param \ReflectionType $type
+ * Declared parameter type.
+ *
+ * @param mixed $value
+ * Explicitly bound parameter value.
+ *
+ *
+ * @return bool True if the value satisfies the declared type.
+ */
+ private function matchesType (ReflectionParameter $parameter, ReflectionType $type, mixed $value):bool {
+
+ if ($value === null)
+ return $type->allowsNull();
+
+ if ($type instanceof ReflectionUnionType) {
+
+ foreach ($type->getTypes() as $member)
+ if ($this->matchesType($parameter, $member, $value))
+ return true;
+
+ return false;
+
+ }
+
+ if ($type instanceof ReflectionIntersectionType) {
+
+ foreach ($type->getTypes() as $member)
+ if (!$this->matchesType($parameter, $member, $value))
+ return false;
+
+ return true;
+
+ }
+
+ return $type instanceof ReflectionNamedType
+ && $this->matchesNamedType($parameter, $type, $value);
+
+ }
+
+ /**
+ * ### Checks named parameter type compatibility
+ *
+ * Validates scalar, built-in, and class-based parameter types against an explicitly configured value.
+ *
+ * Relative class types are resolved against the class declaring the constructor parameter.
+ * @since 1.0.0
+ *
+ * @param \ReflectionParameter $parameter
+ * Constructor parameter.
+ *
+ * @param \ReflectionNamedType $type
+ * Declared named type.
+ *
+ * @param mixed $value
+ * Explicitly bound parameter value.
+ *
+ *
+ * @return bool True if the value satisfies the named type.
+ */
+ private function matchesNamedType (ReflectionParameter $parameter, ReflectionNamedType $type, mixed $value):bool {
+
+ $name = $type->getName();
+
+ if (!$type->isBuiltin()) {
+
+ $declaring_class = $parameter->getDeclaringClass();
+ $parent = $declaring_class?->getParentClass();
+
+ $class = match ($name) {
+ 'self' => $declaring_class?->getName(),
+ 'parent' => $parent !== false ? $parent?->getName() : null,
+ default => $name
+ };
+
+ return $class !== null && $value instanceof $class;
+
+ }
+
+ return match ($name) {
+ 'array' => is_array($value),
+ 'bool' => is_bool($value),
+ 'callable' => is_callable($value),
+ 'false' => $value === false,
+ 'float' => is_float($value) || is_int($value),
+ 'int' => is_int($value),
+ 'iterable' => is_iterable($value),
+ 'mixed' => true,
+ 'null' => $value === null,
+ 'object' => is_object($value),
+ 'string' => is_string($value),
+ 'true' => $value === true,
+ default => false
+ };
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Validation/Result.php b/src/Validation/Result.php
new file mode 100644
index 0000000..626f0c7
--- /dev/null
+++ b/src/Validation/Result.php
@@ -0,0 +1,173 @@
+
+ * @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\Validation;
+
+use FireHub\Runtime;
+use Countable, IteratorAggregate, Traversable;
+
+/**
+ * ### Represents the result of container validation
+ *
+ * Collects errors and warnings discovered while statically analyzing registered service definitions.
+ *
+ * A validation result is considered valid when it contains no errors. Warnings do not invalidate the result.
+ *
+ * The result is immutable and supports iteration over all issues.
+ * @since 1.0.0
+ *
+ * @implements IteratorAggregate
+ */
+final readonly class Result implements Countable, IteratorAggregate {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues [optional]
+ * Validation issues.
+ *
+ */
+ public function __construct (
+ private array $issues = []
+ ) {}
+
+ /**
+ * ### Checks whether validation succeeded
+ *
+ * Validation succeeds when no errors are present. Warnings do not cause validation failure.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\Issue::isError() To check for error status.
+ *
+ * @return bool True if no errors were detected.
+ */
+ public function isValid ():bool {
+
+ foreach ($this->issues as $issue) {
+
+ if ($issue->isError())
+ return false;
+
+ }
+
+ return true;
+
+ }
+
+ /**
+ * ### Checks whether warnings are present
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\Issue::isWarning() To check for warning status.
+ *
+ * @return bool True if at least one warning exists.
+ */
+ public function hasWarnings ():bool {
+
+ foreach ($this->issues as $issue) {
+
+ if ($issue->isWarning())
+ return true;
+
+ }
+
+ return false;
+
+ }
+
+ /**
+ * ### Gets all validation issues
+ * @since 1.0.0
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Validation issues.
+ */
+ public function issues ():array {
+
+ return $this->issues;
+
+ }
+
+ /**
+ * ### Gets validation errors
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\Issue::isError() To check for error status.
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Validation errors.
+ */
+ public function errors ():array {
+
+ $errors = [];
+
+ foreach ($this->issues as $issue) {
+
+ if ($issue->isError())
+ $errors[] = $issue;
+
+ }
+
+ return $errors;
+
+ }
+
+ /**
+ * ### Gets validation warnings
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\Issue::isWarning() To check for warning status.
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Validation warnings.
+ */
+ public function warnings ():array {
+
+ $warnings = [];
+
+ foreach ($this->issues as $issue) {
+
+ if ($issue->isWarning())
+ $warnings[] = $issue;
+
+ }
+
+ return $warnings;
+
+ }
+
+ /**
+ * ### Counts validation issues
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Runtime\Arr\Inspection::count() To count the number of issues.
+ *
+ * @return non-negative-int Number of validation issues.
+ */
+ public function count ():int {
+
+ return Runtime\Arr\Inspection::count($this->issues);
+
+ }
+
+ /**
+ * ### Iterates over validation issues
+ * @since 1.0.0
+ *
+ * @return Traversable Validation issues.
+ */
+ public function getIterator ():Traversable {
+
+ yield from $this->issues;
+
+ }
+
+}
\ No newline at end of file
diff --git a/src/Validation/Severity.php b/src/Validation/Severity.php
new file mode 100644
index 0000000..147e7c1
--- /dev/null
+++ b/src/Validation/Severity.php
@@ -0,0 +1,41 @@
+
+ * @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\Validation;
+
+/**
+ * ### Defines container validation severity levels
+ *
+ * Distinguishes confirmed configuration errors from warnings that do not necessarily prevent successful service
+ * resolution.
+ * @since 1.0.0
+ */
+enum Severity {
+
+ /**
+ * ### Error
+ *
+ * Represents a confirmed configuration problem.
+ * @since 1.0.0
+ */
+ case ERROR;
+
+ /**
+ * ### Warning
+ *
+ * Represents a potential problem or a limitation of static validation.
+ * @since 1.0.0
+ */
+ case WARNING;
+
+}
\ No newline at end of file
diff --git a/src/Validator.php b/src/Validator.php
new file mode 100644
index 0000000..7ab9176
--- /dev/null
+++ b/src/Validator.php
@@ -0,0 +1,442 @@
+
+ * @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\Validation\ {
+ ContextualBinding, DependencyGraph, Issue, IssueType, Lifetime, ParameterBinding, Result, Severity
+};
+use FireHub\Container\Definition\ {
+ Alias, Cls, Factory, Instance
+};
+use FireHub\Container\Internal\ {
+ CanonicalIdCache, ConstructorMetadataCache, ContextualBindingRegistry, DependencyPlanCache,
+ ParameterBindingRegistry, ParameterPlanCompiler, TagRegistry, TypeMatcher
+};
+use FireHub\Runtime;
+use ReflectionClass, ReflectionException;
+
+/**
+ * ### Validates container service definitions
+ *
+ * Performs static analysis of registered dependency injection service definitions without constructing services,
+ * resolving dependencies, or executing factory callbacks.
+ *
+ * The validator checks class-based definitions, existing instances, and alias chains for structural correctness and
+ * type compatibility.
+ *
+ * Factory definitions are reported as statically unverifiable because their execution behavior cannot be determined
+ * without invoking the factory callback.
+ *
+ * Validation does not modify the service registry or container lifecycle state.
+ * @since 1.0.0
+ *
+ * @internal
+ */
+final readonly class Validator {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param \FireHub\Container\Registry $registry
+ * Registry containing service definitions.
+ *
+ * @param \FireHub\Container\Internal\ParameterBindingRegistry $bindings
+ * Explicit constructor parameter bindings.
+ *
+ * @param \FireHub\Container\Internal\ContextualBindingRegistry $contextual
+ * Contextual dependency binding registry.
+ *
+ * @param \FireHub\Container\Internal\TagRegistry $tags
+ * Registered service tags.
+ *
+ *
+ * @return void
+ */
+ public function __construct (
+ private Registry $registry,
+ private ParameterBindingRegistry $bindings,
+ private ContextualBindingRegistry $contextual,
+ private TagRegistry $tags
+ ) {}
+
+ /**
+ * ### Validates registered service definitions
+ *
+ * Inspects registered service definitions and collects all independently detectable structural problems.
+ *
+ * Constructor dependencies and service lifetime relationships are not analyzed at this stage.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validation\DependencyGraph::validate() To validate dependency graphs.
+ * @uses \FireHub\Container\Internal\ParameterBinding::validate() To validate parameter bindings.
+ * @uses \FireHub\Container\Internal\ContextualBinding::validate() To validate contextual bindings.
+ * @uses \FireHub\Container\Internal\Lifetime::validate() To validate service lifetimes.
+ * @uses \FireHub\Container\Registry::all() To retrieve all registered service definitions.
+ * @uses \FireHub\Container\Validator::validateClass() To validate class-based definitions.
+ * @uses \FireHub\Container\Validator::validateInstance() To validate existing instance definitions.
+ * @uses \FireHub\Container\Validator::validateAlias() To validate alias chains.
+ * @uses \FireHub\Runtime\Arr\Mutation::push() To append validation issues.
+ *
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If a constructor parameter contains invalid
+ * injection attributes or unsupported dependency metadata.
+ * @throws \FireHub\Container\Exception\AmbiguousDependencyException If multiple candidates are eligible.
+ * @throws \FireHub\Container\Exception\ServiceInstantiationException If the composite type is unsupported.
+ *
+ * @return \FireHub\Container\Validation\Result Validation result.
+ */
+ public function validate ():Result {
+
+ $metadata = new ConstructorMetadataCache;
+
+ $matcher = new TypeMatcher($metadata);
+
+ $plans = new DependencyPlanCache(
+ $metadata,
+ new ParameterPlanCompiler($matcher)
+ );
+
+ $issues = [];
+
+ foreach ($this->registry->all() as $id => $definition) {
+
+ if ($definition instanceof Cls) {
+
+ $this->validateClass($id, $definition, $issues);
+
+ } elseif ($definition instanceof Instance) {
+
+ $this->validateInstance($id, $definition, $issues);
+
+ } elseif ($definition instanceof Alias) {
+
+ $this->validateAlias($id, $issues);
+
+ } elseif ($definition instanceof Factory) {
+
+ $issues[] = new Issue(
+ IssueType::UNVERIFIABLE_FACTORY,
+ Severity::WARNING,
+ $id,
+ 'Factory execution cannot be statically validated.'
+ );
+
+ }
+
+ }
+
+ $canonical = new CanonicalIdCache(
+ $this->registry
+ );
+
+ $dependency_issues = new DependencyGraph(
+ $this->registry,
+ $this->bindings,
+ $this->contextual,
+ $this->tags,
+ $plans,
+ $matcher,
+ $canonical
+ )->validate();
+
+ Runtime\Arr\Mutation::push($issues, ...$dependency_issues);
+
+ $parameter_issues = new ParameterBinding(
+ $this->bindings
+ )->validate();
+
+ Runtime\Arr\Mutation::push($issues, ...$parameter_issues);
+
+ $contextual_issues = new ContextualBinding(
+ $this->registry,
+ $this->contextual,
+ $this->bindings,
+ $plans
+ )->validate();
+
+ Runtime\Arr\Mutation::push($issues, ...$contextual_issues);
+
+ $lifetime_issues = new Lifetime(
+ $this->registry,
+ $this->bindings,
+ $this->contextual,
+ $this->tags,
+ $plans,
+ $matcher,
+ $canonical
+ )->validate();
+
+ Runtime\Arr\Mutation::push($issues, ...$lifetime_issues);
+
+ /** @var list<\FireHub\Container\Validation\Issue> $issues */
+ return new Result($issues);
+
+ }
+
+ /**
+ * ### Validates a class-based service definition
+ *
+ * Verifies that the implementation exists, is instantiable, and is compatible with the registered service
+ * identifier when the identifier represents an existing PHP class or interface.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validator::validateTypeCompatibility() To check type compatibility.
+ * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class.
+ *
+ * @param class-string $id
+ * Registered service identifier.
+ *
+ * @param \FireHub\Container\Definition\Cls $definition
+ * Class-based service definition.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ *
+ * @return void
+ */
+ private function validateClass (string $id, Cls $definition, array &$issues):void {
+
+ $class = $definition->class;
+
+ if (!Runtime\ObjectModel\Inspection::isClass($class)) {
+
+ $issues[] = new Issue(
+ IssueType::MISSING_CLASS,
+ Severity::ERROR,
+ $id,
+ "Implementation class '$class' does not exist."
+ );
+
+ return;
+
+ }
+
+ try {
+
+ $reflection = new ReflectionClass($class);
+
+ } catch (ReflectionException) { // @phpstan-ignore catch.neverThrown
+
+ $issues[] = new Issue(
+ IssueType::MISSING_CLASS,
+ Severity::ERROR,
+ $id,
+ "Implementation class '$class' cannot be reflected."
+ );
+
+ return;
+
+ }
+
+ if (!$reflection->isInstantiable()) {
+
+ $issues[] = new Issue(
+ IssueType::NON_INSTANTIABLE_CLASS,
+ Severity::ERROR,
+ $id,
+ "Implementation class '$class' is not instantiable."
+ );
+
+ }
+
+ $this->validateTypeCompatibility(
+ $id,
+ $class,
+ $issues
+ );
+
+ }
+
+ /**
+ * ### Validates an existing service instance
+ *
+ * Verifies that the registered object is compatible with its service identifier when the identifier
+ * represents an existing PHP class or interface.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validator::validateTypeCompatibility() To check type compatibility.
+ *
+ * @param class-string $id
+ * Registered service identifier.
+ *
+ * @param \FireHub\Container\Definition\Instance $definition
+ * Existing instance definition.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ *
+ * @return void
+ */
+ private function validateInstance (string $id, Instance $definition, array &$issues):void {
+
+ $this->validateTypeCompatibility(
+ $id,
+ $definition->instance::class,
+ $issues
+ );
+
+ }
+
+ /**
+ * ### Validates a service alias
+ *
+ * Traverses the alias chain to detect missing targets, circular references, and incompatible terminal
+ * implementations.
+ *
+ * Alias traversal does not resolve or instantiate services.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Container\Validator::validateTypeCompatibility() To check type compatibility.
+ * @uses \FireHub\Container\Registry::find() To retrieve the target service definition.
+ * @uses \FireHub\Runtime\Str\SB\Delimiter::implode() To join path segments.
+ *
+ * @param class-string $id
+ * Alias service identifier.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ *
+ * @return void
+ */
+ private function validateAlias (string $id, array &$issues):void {
+
+ $visited = [];
+ $path = [];
+
+ $current = $id;
+
+ while (true) {
+
+ if (isset($visited[$current])) {
+
+ $path[] = $current;
+
+ $issues[] = new Issue(
+ IssueType::CIRCULAR_ALIAS,
+ Severity::ERROR,
+ $id,
+ 'Circular alias chain detected: ' . Runtime\Str\SB\Delimiter::implode($path, ' -> '),
+ $path
+ );
+
+ return;
+
+ }
+
+ $visited[$current] = true;
+ $path[] = $current;
+
+ $definition = $this->registry->find($current);
+
+ if ($definition === null) {
+
+ $issues[] = new Issue(
+ IssueType::INVALID_ALIAS,
+ Severity::ERROR,
+ $id,
+ "Alias target '$current' is not registered.",
+ $path
+ );
+
+ return;
+
+ }
+
+ if ($definition instanceof Alias) {
+
+ $current = $definition->target;
+
+ continue;
+
+ }
+
+ if ($definition instanceof Cls) {
+
+ $this->validateTypeCompatibility(
+ $id,
+ $definition->class,
+ $issues,
+ $path
+ );
+
+ } elseif ($definition instanceof Instance) {
+
+ $this->validateTypeCompatibility(
+ $id,
+ $definition->instance::class,
+ $issues,
+ $path
+ );
+
+ }
+
+ return;
+
+ }
+
+ }
+
+ /**
+ * ### Validates service type compatibility
+ *
+ * Checks whether the implementation satisfies the service identifier when the identifier represents an
+ * existing class or interface.
+ *
+ * Identifiers that do not represent loadable PHP types are not checked for type compatibility.
+ * @since 1.0.0
+ *
+ * @uses \FireHub\Runtime\ObjectModel\Inspection::isClass() To check if the identifier is a class.
+ * @uses \FireHub\Runtime\ObjectModel\Inspection::isInterface() To check if the identifier is an interface.
+ * @uses \FireHub\Runtime\ObjectModel\Relation::instanceOf() To check if the implementation satisfies the contract.
+ *
+ * @param class-string $id
+ * Registered service identifier.
+ *
+ * @param class-string $implementation
+ * Implementation class.
+ *
+ * @param list<\FireHub\Container\Validation\Issue> $issues
+ * Collected validation issues.
+ *
+ * @param list $path [optional]
+ * Dependency or alias path associated with the issue.
+ *
+ *
+ * @return void
+ */
+ private function validateTypeCompatibility (string $id, string $implementation, array &$issues, array $path = []):void {
+
+ if (
+ !Runtime\ObjectModel\Inspection::isClass($id)
+ && !Runtime\ObjectModel\Inspection::isInterface($id)
+ ) return;
+
+ if (
+ Runtime\ObjectModel\Relation::instanceOf($implementation, $id)
+ ) return;
+
+ $issues[] = new Issue(
+ IssueType::TYPE_MISMATCH,
+ Severity::ERROR,
+ $id,
+ "Implementation '$implementation' is not compatible with service '$id'.",
+ $path
+ );
+
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/CollectionValidationCircular.php b/tests/Stubs/CollectionValidationCircular.php
new file mode 100644
index 0000000..be58615
--- /dev/null
+++ b/tests/Stubs/CollectionValidationCircular.php
@@ -0,0 +1,35 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+use FireHub\Container\Attribute\InjectAll;
+
+/**
+ * ### Circular collection validation consumer
+ * @since 1.0.0
+ */
+final readonly class CollectionValidationCircular implements CollectionValidationContract {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param list $services Collection services.
+ */
+ public function __construct (
+ #[InjectAll(CollectionValidationContract::class)]
+ public array $services
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/CollectionValidationConsumer.php b/tests/Stubs/CollectionValidationConsumer.php
new file mode 100644
index 0000000..efe5f09
--- /dev/null
+++ b/tests/Stubs/CollectionValidationConsumer.php
@@ -0,0 +1,35 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+use FireHub\Container\Attribute\InjectAll;
+
+/**
+ * ### Consumer of collection validation services
+ * @since 1.0.0
+ */
+final readonly class CollectionValidationConsumer {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param list $services Collection services.
+ */
+ public function __construct(
+ #[InjectAll(CollectionValidationContract::class)]
+ public array $services
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/CollectionValidationContract.php b/tests/Stubs/CollectionValidationContract.php
new file mode 100644
index 0000000..99d9ac4
--- /dev/null
+++ b/tests/Stubs/CollectionValidationContract.php
@@ -0,0 +1,20 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Collection validation contract
+ * @since 1.0.0
+ */
+interface CollectionValidationContract {}
\ No newline at end of file
diff --git a/tests/Stubs/CollectionValidationFirst.php b/tests/Stubs/CollectionValidationFirst.php
new file mode 100644
index 0000000..c2b8b34
--- /dev/null
+++ b/tests/Stubs/CollectionValidationFirst.php
@@ -0,0 +1,20 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### First collection validation implementation
+ * @since 1.0.0
+ */
+final readonly class CollectionValidationFirst implements CollectionValidationContract {}
\ No newline at end of file
diff --git a/tests/Stubs/CollectionValidationMissingContract.php b/tests/Stubs/CollectionValidationMissingContract.php
new file mode 100644
index 0000000..8c295f1
--- /dev/null
+++ b/tests/Stubs/CollectionValidationMissingContract.php
@@ -0,0 +1,20 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Missing collection dependency contract
+ * @since 1.0.0
+ */
+interface CollectionValidationMissingContract {}
diff --git a/tests/Stubs/CollectionValidationMissingDependency.php b/tests/Stubs/CollectionValidationMissingDependency.php
new file mode 100644
index 0000000..0b94aa7
--- /dev/null
+++ b/tests/Stubs/CollectionValidationMissingDependency.php
@@ -0,0 +1,32 @@
+
+ * @copyright 2026-present The FireHub Project - All rights reserved
+ * @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
+ *
+ * @php-version >=8.2
+ * @package Container\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Collection member with an unresolved dependency
+ * @since 1.0.0
+ */
+final readonly class CollectionValidationMissingDependency implements CollectionValidationContract {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param CollectionValidationMissingContract $dependency Missing dependency.
+ */
+ public function __construct (
+ public CollectionValidationMissingContract $dependency
+ ) {}
+
+}
diff --git a/tests/Stubs/CollectionValidationNested.php b/tests/Stubs/CollectionValidationNested.php
new file mode 100644
index 0000000..69b48b0
--- /dev/null
+++ b/tests/Stubs/CollectionValidationNested.php
@@ -0,0 +1,30 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Nested collection validation consumer
+ * @since 1.0.0
+ */
+final readonly class CollectionValidationNested {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ */
+ public function __construct (
+ public CollectionValidationConsumer $consumer
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/CollectionValidationSecond.php b/tests/Stubs/CollectionValidationSecond.php
new file mode 100644
index 0000000..a1b77fd
--- /dev/null
+++ b/tests/Stubs/CollectionValidationSecond.php
@@ -0,0 +1,20 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Second collection validation implementation
+ * @since 1.0.0
+ */
+final readonly class CollectionValidationSecond implements CollectionValidationContract {}
\ No newline at end of file
diff --git a/tests/Stubs/ContextualValidationOtherContract.php b/tests/Stubs/ContextualValidationOtherContract.php
new file mode 100644
index 0000000..42de1d6
--- /dev/null
+++ b/tests/Stubs/ContextualValidationOtherContract.php
@@ -0,0 +1,20 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Contextual validation another contract
+ * @since 1.0.0
+ */
+interface ContextualValidationOtherContract {}
\ No newline at end of file
diff --git a/tests/Stubs/ContextualValidationUnusedConsumer.php b/tests/Stubs/ContextualValidationUnusedConsumer.php
new file mode 100644
index 0000000..9429bd4
--- /dev/null
+++ b/tests/Stubs/ContextualValidationUnusedConsumer.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.2
+ * @package Container\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Contextual validation of unused consumer
+ * @since 1.0.0
+ */
+final readonly class ContextualValidationUnusedConsumer {
+
+ public function __construct (
+ public string $name = 'default'
+ ) {}
+
+}
diff --git a/tests/Stubs/LifetimeValidationExplicitConsumer.php b/tests/Stubs/LifetimeValidationExplicitConsumer.php
new file mode 100644
index 0000000..d3eb955
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationExplicitConsumer.php
@@ -0,0 +1,25 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationExplicitConsumer {
+
+ public function __construct (
+ public LifetimeValidationScoped $dependency
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationFactoryConsumer.php b/tests/Stubs/LifetimeValidationFactoryConsumer.php
new file mode 100644
index 0000000..11f7f4f
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationFactoryConsumer.php
@@ -0,0 +1,25 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationFactoryConsumer {
+
+ public function __construct (
+ public LifetimeValidationScoped $dependency
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationNestedSingleton.php b/tests/Stubs/LifetimeValidationNestedSingleton.php
new file mode 100644
index 0000000..589c24e
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationNestedSingleton.php
@@ -0,0 +1,25 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationNestedSingleton {
+
+ public function __construct (
+ public LifetimeValidationTransient $dependency
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationSafeConsumer.php b/tests/Stubs/LifetimeValidationSafeConsumer.php
new file mode 100644
index 0000000..d25d176
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationSafeConsumer.php
@@ -0,0 +1,25 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationSafeConsumer {
+
+ public function __construct (
+ public LifetimeValidationSafeSingleton $dependency
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationSafeSingleton.php b/tests/Stubs/LifetimeValidationSafeSingleton.php
new file mode 100644
index 0000000..02b00da
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationSafeSingleton.php
@@ -0,0 +1,21 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationSafeSingleton {
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationScoped.php b/tests/Stubs/LifetimeValidationScoped.php
new file mode 100644
index 0000000..c528ea7
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationScoped.php
@@ -0,0 +1,21 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationScoped {
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationSingleton.php b/tests/Stubs/LifetimeValidationSingleton.php
new file mode 100644
index 0000000..4271d17
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationSingleton.php
@@ -0,0 +1,25 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationSingleton {
+
+ public function __construct (
+ public LifetimeValidationScoped $dependency
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/LifetimeValidationTransient.php b/tests/Stubs/LifetimeValidationTransient.php
new file mode 100644
index 0000000..d5c6f12
--- /dev/null
+++ b/tests/Stubs/LifetimeValidationTransient.php
@@ -0,0 +1,25 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final readonly class LifetimeValidationTransient {
+
+ public function __construct (
+ public LifetimeValidationScoped $dependency
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/ParameterBindingValidationComposite.php b/tests/Stubs/ParameterBindingValidationComposite.php
new file mode 100644
index 0000000..0a2dd86
--- /dev/null
+++ b/tests/Stubs/ParameterBindingValidationComposite.php
@@ -0,0 +1,36 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Composite parameter binding validation service
+ * @since 1.0.0
+ */
+final readonly class ParameterBindingValidationComposite {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param int|string $union Union value.
+ * @param \Countable&\IteratorAggregate $intersection Intersection value.
+ * @param (\Countable&\IteratorAggregate)|string $dnf DNF value.
+ */
+ public function __construct (
+ public int|string $union,
+ public \Countable&\IteratorAggregate $intersection,
+ public (\Countable&\IteratorAggregate)|string $dnf
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/ParameterBindingValidationContract.php b/tests/Stubs/ParameterBindingValidationContract.php
new file mode 100644
index 0000000..5cb1fc8
--- /dev/null
+++ b/tests/Stubs/ParameterBindingValidationContract.php
@@ -0,0 +1,20 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Parameter binding validation contract
+ * @since 1.0.0
+ */
+interface ParameterBindingValidationContract {}
\ No newline at end of file
diff --git a/tests/Stubs/ParameterBindingValidationService.php b/tests/Stubs/ParameterBindingValidationService.php
new file mode 100644
index 0000000..5fc01c5
--- /dev/null
+++ b/tests/Stubs/ParameterBindingValidationService.php
@@ -0,0 +1,38 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Parameter binding validation service
+ * @since 1.0.0
+ */
+final readonly class ParameterBindingValidationService implements ParameterBindingValidationContract {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param int $port Port number.
+ * @param null|string $host Host name.
+ * @param float $timeout Timeout value.
+ * @param null|ParameterBindingValidationContract $dependency Optional dependency.
+ */
+ public function __construct (
+ public int $port = 3306,
+ public ?string $host = null,
+ public float $timeout = 1.0,
+ public ?ParameterBindingValidationContract $dependency = null
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/TaggedCircularConsumer.php b/tests/Stubs/TaggedCircularConsumer.php
new file mode 100644
index 0000000..4f1931e
--- /dev/null
+++ b/tests/Stubs/TaggedCircularConsumer.php
@@ -0,0 +1,35 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+use FireHub\Container\Attribute\Tagged;
+
+/**
+ * ### Consumer of tagged services with circular dependencies
+ * @since 1.0.0
+ */
+final readonly class TaggedCircularConsumer {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param list $services Tagged services.
+ */
+ public function __construct (
+ #[Tagged('validation.circular')]
+ public array $services
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/TaggedValidationConsumer.php b/tests/Stubs/TaggedValidationConsumer.php
new file mode 100644
index 0000000..58dbda8
--- /dev/null
+++ b/tests/Stubs/TaggedValidationConsumer.php
@@ -0,0 +1,35 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+use FireHub\Container\Attribute\Tagged;
+
+/**
+ * ### Consumer of tagged validation services
+ * @since 1.0.0
+ */
+final readonly class TaggedValidationConsumer {
+
+ /**
+ * ### Constructor
+ * @since 1.0.0
+ *
+ * @param list $services Tagged services.
+ */
+ public function __construct (
+ #[Tagged('validation.services')]
+ public array $services
+ ) {}
+
+}
\ No newline at end of file
diff --git a/tests/Stubs/ValidationConsoleLogger.php b/tests/Stubs/ValidationConsoleLogger.php
new file mode 100644
index 0000000..1a1d25f
--- /dev/null
+++ b/tests/Stubs/ValidationConsoleLogger.php
@@ -0,0 +1,19 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final class ValidationConsoleLogger implements ValidationLoggerInterface {}
\ No newline at end of file
diff --git a/tests/Stubs/ValidationConstructionProbe.php b/tests/Stubs/ValidationConstructionProbe.php
new file mode 100644
index 0000000..175d531
--- /dev/null
+++ b/tests/Stubs/ValidationConstructionProbe.php
@@ -0,0 +1,36 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * ### Tracks constructor execution during validation
+ * @internal
+ */
+final class ValidationConstructionProbe {
+
+ /**
+ * @since 1.0.0
+ */
+ public static int $constructions = 0;
+
+ /**
+ * @since 1.0.0
+ */
+ public function __construct () {
+
+ self::$constructions++;
+
+ }
+
+}
diff --git a/tests/Stubs/ValidationFileLogger.php b/tests/Stubs/ValidationFileLogger.php
new file mode 100644
index 0000000..4124cbb
--- /dev/null
+++ b/tests/Stubs/ValidationFileLogger.php
@@ -0,0 +1,19 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final class ValidationFileLogger implements ValidationLoggerInterface {}
\ No newline at end of file
diff --git a/tests/Stubs/ValidationLoggerInterface.php b/tests/Stubs/ValidationLoggerInterface.php
new file mode 100644
index 0000000..afde813
--- /dev/null
+++ b/tests/Stubs/ValidationLoggerInterface.php
@@ -0,0 +1,19 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+interface ValidationLoggerInterface {}
\ No newline at end of file
diff --git a/tests/Stubs/ValidationUnrelatedService.php b/tests/Stubs/ValidationUnrelatedService.php
new file mode 100644
index 0000000..41ab399
--- /dev/null
+++ b/tests/Stubs/ValidationUnrelatedService.php
@@ -0,0 +1,19 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Stubs;
+
+/**
+ * @since 1.0.0
+ */
+final class ValidationUnrelatedService {}
\ No newline at end of file
diff --git a/tests/Unit/AutowiringTest.php b/tests/Unit/AutowiringTest.php
index 70d8214..60d74d9 100644
--- a/tests/Unit/AutowiringTest.php
+++ b/tests/Unit/AutowiringTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/CollectionInjectionTest.php b/tests/Unit/CollectionInjectionTest.php
index a8198fb..689234b 100644
--- a/tests/Unit/CollectionInjectionTest.php
+++ b/tests/Unit/CollectionInjectionTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/CollectionServicesTest.php b/tests/Unit/CollectionServicesTest.php
index e4c2768..20b3e1e 100644
--- a/tests/Unit/CollectionServicesTest.php
+++ b/tests/Unit/CollectionServicesTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/ContainerLifecycleTest.php b/tests/Unit/ContainerLifecycleTest.php
index 7ce9572..4723686 100644
--- a/tests/Unit/ContainerLifecycleTest.php
+++ b/tests/Unit/ContainerLifecycleTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/ContainerValidationTest.php b/tests/Unit/ContainerValidationTest.php
new file mode 100644
index 0000000..4fe7575
--- /dev/null
+++ b/tests/Unit/ContainerValidationTest.php
@@ -0,0 +1,628 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Unit;
+
+use FireHub\Testing\FireHubTestCase;
+use FireHub\Container\Container;
+use FireHub\Container\Validation\ {
+ Issue, IssueType, Result, Severity
+};
+use FireHub\Tests\Container\Stubs\ {
+ Logger, LoggerInterface, TestFileLogger, TestConsoleLogger, ValidationConsoleLogger, ValidationConstructionProbe,
+ ValidationFileLogger, ValidationLoggerInterface, ValidationUnrelatedService
+};
+use PHPUnit\Framework\Attributes\ {
+ CoversClass, Group, Small
+};
+
+/**
+ * ### Tests static container definition validation
+ * @since 1.0.0
+ */
+#[Small]
+#[Group('validation')]
+#[CoversClass(Container::class)]
+final class ContainerValidationTest extends FireHubTestCase {
+
+ /**
+ * ### Validates an empty container
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testEmptyContainerIsValid ():void {
+
+ $container = new Container;
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertFalse($result->hasWarnings());
+ self::assertSame([], $result->issues());
+ self::assertSame([], $result->errors());
+ self::assertSame([], $result->warnings());
+ self::assertCount(0, $result);
+
+ }
+
+ /**
+ * ### Validates a concrete class definition
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidClassDefinition ():void {
+
+ $container = new Container;
+
+ $container->singleton(Logger::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertCount(0, $result);
+
+ }
+
+ /**
+ * ### Validates an interface implementation
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidInterfaceImplementation ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ ValidationLoggerInterface::class,
+ ValidationFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertSame([], $result->errors());
+
+ }
+
+ /**
+ * ### Detects a missing implementation class
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMissingImplementationClass ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ LoggerInterface::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingLogger'
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+ self::assertCount(1, $result->errors());
+
+ self::assertSame(
+ IssueType::MISSING_CLASS,
+ $result->errors()[0]->type
+ );
+
+ self::assertSame(
+ Severity::ERROR,
+ $result->errors()[0]->severity
+ );
+
+ }
+
+ /**
+ * ### Detects a non-instantiable implementation
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testNonInstantiableClass ():void {
+
+ $container = new Container;
+
+ $container->singleton(LoggerInterface::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::MISSING_CLASS,
+ $result->errors()[0]->type
+ );
+
+ }
+
+ /**
+ * ### Detects an incompatible class implementation
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIncompatibleImplementation ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ LoggerInterface::class,
+ Logger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::TYPE_MISMATCH,
+ $result->errors()[0]->type
+ );
+
+ }
+
+ /**
+ * ### Validates an existing service instance
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidInstanceDefinition ():void {
+
+ $container = new Container;
+
+ $container->instance(
+ ValidationLoggerInterface::class,
+ new ValidationFileLogger()
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Detects an incompatible service instance
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIncompatibleInstanceDefinition ():void {
+
+ $container = new Container;
+
+ $container->instance(
+ LoggerInterface::class,
+ new Logger
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::TYPE_MISMATCH,
+ $result->errors()[0]->type
+ );
+
+ }
+
+ /**
+ * ### Validates a direct alias
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidDirectAlias ():void {
+
+ $container = new Container;
+
+ $container->singleton(ValidationFileLogger::class);
+
+ $container->alias(
+ ValidationLoggerInterface::class,
+ ValidationFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates a chained alias
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidAliasChain ():void {
+
+ $container = new Container;
+
+ $container->singleton(ValidationFileLogger::class);
+
+ $container->alias(
+ ValidationLoggerInterface::class,
+ ValidationFileLogger::class
+ );
+
+ $container->alias(
+ 'FireHub\\Tests\\Container\\Stubs\\LoggerAlias',
+ ValidationLoggerInterface::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Detects an alias with a missing target
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMissingAliasTarget ():void {
+
+ $container = new Container;
+
+ $container->alias(
+ LoggerInterface::class,
+ TestFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::INVALID_ALIAS,
+ $result->errors()[0]->type
+ );
+
+ }
+
+ /**
+ * ### Detects a self-referencing alias
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testSelfReferencingAlias ():void {
+
+ $container = new Container;
+
+ $container->alias(
+ LoggerInterface::class,
+ LoggerInterface::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::CIRCULAR_ALIAS,
+ $result->errors()[0]->type
+ );
+
+ }
+
+ /**
+ * ### Detects a circular alias chain
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularAliasChain ():void {
+
+ $container = new Container;
+
+ $container->alias(
+ LoggerInterface::class,
+ TestFileLogger::class
+ );
+
+ $container->alias(
+ TestFileLogger::class,
+ LoggerInterface::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::CIRCULAR_ALIAS,
+ $result->errors()[0]->type
+ );
+
+ self::assertSame(
+ [
+ LoggerInterface::class,
+ TestFileLogger::class,
+ LoggerInterface::class
+ ],
+ $result->errors()[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects an incompatible alias target
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIncompatibleAliasTarget ():void {
+
+ $container = new Container;
+
+ $container->singleton(Logger::class);
+
+ $container->alias(
+ LoggerInterface::class,
+ Logger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertSame(
+ IssueType::TYPE_MISMATCH,
+ $result->errors()[0]->type
+ );
+
+ }
+
+ /**
+ * ### Reports factory validation limitations
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testFactoryProducesWarning ():void {
+
+ $container = new Container;
+
+ $container->factory(
+ LoggerInterface::class,
+ fn($resolver) => new TestFileLogger
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertTrue($result->hasWarnings());
+
+ self::assertCount(1, $result->warnings());
+
+ self::assertSame(
+ IssueType::UNVERIFIABLE_FACTORY,
+ $result->warnings()[0]->type
+ );
+
+ self::assertSame(
+ Severity::WARNING,
+ $result->warnings()[0]->severity
+ );
+
+ }
+
+ /**
+ * ### Collects multiple independent validation issues
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCollectsMultipleIssues ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ ValidationLoggerInterface::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingLogger'
+ );
+
+ $container->alias(
+ ValidationFileLogger::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingAliasTarget'
+ );
+
+ $container->factory(
+ ValidationConsoleLogger::class,
+ fn($resolver) => new TestConsoleLogger
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+ self::assertTrue($result->hasWarnings());
+
+ self::assertCount(2, $result->errors());
+ self::assertCount(1, $result->warnings());
+
+ self::assertSame(
+ [
+ IssueType::MISSING_CLASS,
+ IssueType::INVALID_ALIAS
+ ],
+ array_map(
+ static fn(Issue $issue):IssueType => $issue->type,
+ $result->errors()
+ )
+ );
+
+ self::assertSame(
+ IssueType::UNVERIFIABLE_FACTORY,
+ $result->warnings()[0]->type
+ );
+
+
+ }
+
+ /**
+ * ### Validation does not freeze the container
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidationDoesNotFreezeContainer ():void {
+
+ $container = new Container;
+
+ $container->singleton(Logger::class);
+
+ $container->validate();
+
+ self::assertFalse($container->isFrozen());
+
+ $container->singleton(TestFileLogger::class);
+
+ self::assertFalse($container->isFrozen());
+
+ }
+
+ /**
+ * ### Validation does not instantiate registered services
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidationDoesNotInstantiateServices ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ ValidationConstructionProbe::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertSame(
+ 0,
+ ValidationConstructionProbe::$constructions
+ );
+
+ }
+
+ /**
+ * ### Validation does not execute factory callbacks
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidationDoesNotExecuteFactories ():void {
+
+ $executions = 0;
+
+ $container = new Container;
+
+ $container->factory(
+ LoggerInterface::class,
+ function ($resolver) use (&$executions) {
+
+ $executions++;
+
+ return new TestFileLogger;
+
+ }
+ );
+
+ $result = $container->validate();
+
+ self::assertSame(0, $executions);
+ self::assertTrue($result->isValid());
+ self::assertTrue($result->hasWarnings());
+
+ }
+
+ /**
+ * ### Validates frozen container definitions
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidatesFrozenContainer ():void {
+
+ $container = new Container;
+
+ $container->singleton(Logger::class);
+ $container->freeze();
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertTrue($container->isFrozen());
+
+ }
+
+ /**
+ * ### Validation result supports iteration
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidationResultIteration ():void {
+
+ $issue = new Issue(
+ IssueType::MISSING_CLASS,
+ Severity::ERROR,
+ Logger::class,
+ 'Missing implementation.'
+ );
+
+ $result = new Result([$issue]);
+
+ self::assertCount(1, $result);
+ self::assertSame([$issue], iterator_to_array($result));
+
+ }
+
+ /**
+ * ### Warnings do not invalidate validation results
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testWarningsDoNotInvalidateResult ():void {
+
+ $issue = new Issue(
+ IssueType::UNVERIFIABLE_FACTORY,
+ Severity::WARNING,
+ LoggerInterface::class,
+ 'Factory cannot be statically verified.'
+ );
+
+ $result = new Result([$issue]);
+
+ self::assertTrue($result->isValid());
+ self::assertTrue($result->hasWarnings());
+ self::assertSame([], $result->errors());
+ self::assertCount(1, $result->warnings());
+
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Unit/ContextualBindingTest.php b/tests/Unit/ContextualBindingTest.php
index 24a4834..5d06451 100644
--- a/tests/Unit/ContextualBindingTest.php
+++ b/tests/Unit/ContextualBindingTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/DependencyGraphValidationTest.php b/tests/Unit/DependencyGraphValidationTest.php
new file mode 100644
index 0000000..a02c344
--- /dev/null
+++ b/tests/Unit/DependencyGraphValidationTest.php
@@ -0,0 +1,2311 @@
+
+ * @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\Tests
+ */
+
+namespace FireHub\Tests\Container\Unit;
+
+use FireHub\Testing\FireHubTestCase;
+use FireHub\Container\Container;
+use FireHub\Container\Validation\ {
+ Issue, IssueType, Result, Severity
+};
+use FireHub\Tests\Container\Stubs\ {
+ CircularA, CircularB, CollectionInjectionConflictingAttributesConsumer, CollectionValidationCircular,
+ CollectionValidationConsumer, CollectionValidationContract, CollectionValidationFirst,
+ CollectionValidationMissingDependency, CollectionValidationNested, CollectionValidationSecond, ContextualCache,
+ ContextualCacheService, ContextualCircularA, ContextualCircularB, ContextualDnfConsumer, ContextualFileLogger,
+ ContextualIntersectionConsumer, ContextualLogger, ContextualNamedConsumer, ContextualUnionConsumer, DnfCacheService,
+ DnfDefaultConsumer, DnfFileLogger, DnfFirstBranchConsumer, DnfNestedConsumer, DnfNoCandidateConsumer,
+ DnfRequiredNullableConsumer, DnfSecondBranchConsumer, DnfSerializableCache, DnfTwoIntersectionsConsumer,
+ DnfUniversalService, IntersectionConsumer, Logger, LoggerInterface, ParameterBindingValidationComposite,
+ ParameterBindingValidationService, TaggedCircularConsumer, TaggedValidationConsumer, TestFileLogger, TestLogger,
+ UnionA, UnionAmbiguous, UnionB, UnionNullable, UnionOnlyA, ValidationConstructionProbe
+};
+use PHPUnit\Framework\Attributes\ {
+ CoversClass, Group, Small
+};
+
+/**
+ * ### Tests static constructor dependency graph validation
+ * @since 1.0.0
+ */
+#[Small]
+#[Group('validation')]
+#[CoversClass(Container::class)]
+final class DependencyGraphValidationTest extends FireHubTestCase {
+
+ /**
+ * ### Validates an acyclic dependency graph
+ *
+ * A constructor dependency graph without unresolved dependencies or circular relationships must produce no
+ * validation errors.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidDependencyGraph ():void {
+
+ $container = new Container;
+
+ $container->singleton(Logger::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+ self::assertSame([], $result->errors());
+
+ }
+
+ /**
+ * ### Detects a circular constructor dependency
+ *
+ * Two services depending on each other must produce a circular dependency validation error.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularDependency ():void {
+
+ $container = new Container;
+
+ $container->singleton(CircularA::class);
+ $container->singleton(CircularB::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ $issues = $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ Severity::ERROR,
+ $issues[0]->severity
+ );
+
+ }
+
+ /**
+ * ### Deduplicates circular dependency rotations
+ *
+ * The same directed dependency cycle must be reported only once even when multiple registered services provide
+ * different traversal starting points.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularDependencyDeduplication ():void {
+
+ $container = new Container;
+
+ $container->singleton(CircularA::class);
+ $container->singleton(CircularB::class);
+
+ $result = $container->validate();
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Preserves circular dependency paths
+ *
+ * Circular dependency issues must contain the dependency path, including the service identifier that closes the
+ * cycle.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularDependencyPath ():void {
+
+ $container = new Container;
+
+ $container->singleton(CircularA::class);
+ $container->singleton(CircularB::class);
+
+ $result = $container->validate();
+
+ $issues = $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ );
+
+ self::assertCount(1, $issues);
+
+ $path = $issues[0]->path;
+
+ self::assertGreaterThanOrEqual(3, count($path));
+
+ self::assertSame(
+ $path[0],
+ $path[count($path) - 1]
+ );
+
+ self::assertContains(CircularA::class, $path);
+ self::assertContains(CircularB::class, $path);
+
+ }
+
+ /**
+ * ### Detects a circular alias chain
+ *
+ * Circular service aliases must be reported as alias validation errors rather than constructor dependency cycles.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularAliasDoesNotProduceDependencyCycle ():void {
+
+ $container = new Container;
+
+ $container->alias(
+ ValidationFileLogger::class,
+ TestFileLogger::class
+ );
+
+ $container->alias(
+ TestFileLogger::class,
+ ValidationFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertNotEmpty(
+ $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_ALIAS
+ )
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects an invalid alias target
+ *
+ * An alias referencing an unregistered target must produce an invalid alias issue without reporting a
+ * constructor dependency cycle.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidAliasDoesNotProduceDependencyCycle ():void {
+
+ $container = new Container;
+
+ $container->alias(
+ ValidationFileLogger::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingAliasTarget'
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertNotEmpty(
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_ALIAS
+ )
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Does not instantiate services during graph validation
+ *
+ * Constructor dependency graph validation must inspect metadata without creating registered service instances.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDependencyGraphValidationDoesNotInstantiateServices ():void {
+
+ ValidationConstructionProbe::$constructions = 0;
+
+ $container = new Container;
+
+ $container->singleton(
+ ValidationConstructionProbe::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ 0,
+ ValidationConstructionProbe::$constructions
+ );
+
+ }
+
+ /**
+ * ### Does not execute factories during graph validation
+ *
+ * Registered factory callbacks must never be executed during static dependency graph analysis.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDependencyGraphValidationDoesNotExecuteFactories ():void {
+
+ $executions = 0;
+
+ $container = new Container;
+
+ $container->factory(
+ LoggerInterface::class,
+ function ($resolver) use (&$executions) {
+
+ $executions++;
+
+ return new TestFileLogger;
+
+ }
+ );
+
+ $result = $container->validate();
+
+ self::assertSame(0, $executions);
+
+ self::assertTrue($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::UNVERIFIABLE_FACTORY
+ )
+ );
+
+ }
+
+ /**
+ * ### Preserves validation results across repeated executions
+ *
+ * Repeated validation of an unchanged container must produce equivalent diagnostic results without retaining the
+ * cycle detection state.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testRepeatedValidationProducesEquivalentResults ():void {
+
+ $container = new Container;
+
+ $container->singleton(CircularA::class);
+ $container->singleton(CircularB::class);
+
+ $first = $container->validate();
+ $second = $container->validate();
+
+ self::assertEquals(
+ $first->issues(),
+ $second->issues()
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $second,
+ IssueType::CIRCULAR_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates multiple independent services
+ *
+ * A circular dependency in one service graph must not prevent validation of other independently registered
+ * services.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularDependencyDoesNotInterruptOtherValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CircularA::class);
+ $container->singleton(CircularB::class);
+
+ $container->singleton(
+ LoggerInterface::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingLogger'
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ )
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_CLASS
+ )
+ );
+
+ }
+
+ /**
+ * ### Filters validation issues by type
+ *
+ * Returns all validation issues matching the specified issue category.
+ * @since 1.0.0
+ *
+ * @param \FireHub\Container\Validation\Result $result
+ * Container validation result.
+ *
+ * @param \FireHub\Container\Validation\IssueType $type
+ * Requested validation issue type.
+ *
+ *
+ * @return list<\FireHub\Container\Validation\Issue> Matching validation issues.
+ */
+ private function issuesOfType (
+ Result $result,
+ IssueType $type
+ ):array {
+
+ return array_values(
+ array_filter(
+ $result->issues(),
+ static fn (Issue $issue):bool => $issue->type === $type
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects an invalid constructor dependency plan
+ *
+ * Invalid injection attribute configurations must be reported as validation issues instead of interrupting the
+ * validation process.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidDependencyPlan ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ CollectionInjectionConflictingAttributesConsumer::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_DEPENDENCY_PLAN
+ )
+ );
+
+ }
+
+ /**
+ * ### Continues validation after an invalid dependency plan
+ *
+ * A constructor dependency plan compilation failure must not prevent independently registered services from
+ * being validated.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidDependencyPlanDoesNotInterruptValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ CollectionInjectionConflictingAttributesConsumer::class
+ );
+
+ $container->singleton(
+ LoggerInterface::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingLogger'
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_DEPENDENCY_PLAN
+ )
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_CLASS
+ )
+ );
+
+ }
+
+ /**
+ * ### Accepts a contextual factory target
+ *
+ * Contextual bindings referencing registered factories must not be rejected solely because their implementation
+ * types cannot be statically verified.
+ *
+ * Factory callbacks must not execute during validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testContextualFactoryTarget ():void {
+
+ $executions = 0;
+
+ $container = new Container;
+
+ $container->singleton(
+ ContextualNamedConsumer::class
+ );
+
+ $container->factory(
+ ContextualLogger::class,
+ function ($resolver) use (&$executions) {
+
+ $executions++;
+
+ return new ContextualFileLogger;
+
+ }
+ );
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertSame(0, $executions);
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::UNVERIFIABLE_FACTORY
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects a circular contextual dependency
+ *
+ * Circular dependencies introduced through contextual bindings must be detected and reported as constructor
+ * dependency cycles.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularContextualDependency ():void {
+
+ $container = new Container;
+
+ $container->singleton(
+ ContextualCircularA::class
+ );
+
+ $container->singleton(
+ ContextualCircularB::class
+ );
+
+ $container->bindContextual(
+ ContextualCircularA::class,
+ ContextualCircularB::class,
+ ContextualCircularB::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::CIRCULAR_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates a union dependency with one eligible candidate
+ *
+ * A union dependency with a uniquely selectable implementation must pass static dependency graph validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnionDependencyWithSingleCandidate ():void {
+
+ $container = new Container;
+
+ $container->singleton(UnionOnlyA::class);
+ $container->singleton(UnionA::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::AMBIGUOUS_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects ambiguous union dependencies
+ *
+ * Multiple eligible union alternatives must produce an ambiguous dependency validation issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testAmbiguousUnionDependency ():void {
+
+ $container = new Container;
+
+ $container->singleton(UnionAmbiguous::class);
+
+ $container->singleton(UnionA::class);
+ $container->singleton(UnionB::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::AMBIGUOUS_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates nullable union dependencies with default values
+ *
+ * A nullable union dependency with an explicit default value must remain valid when no dependency candidate is
+ * available.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testNullableUnionDependencyWithDefault ():void {
+
+ $container = new Container;
+
+ $container->singleton(UnionNullable::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates an intersection dependency with one eligible candidate
+ *
+ * An intersection dependency must be considered valid when exactly one registered implementation satisfies all
+ * declared contracts.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIntersectionDependencyWithSingleCandidate ():void {
+
+ $container = new Container;
+
+ $container->singleton(IntersectionConsumer::class);
+ $container->singleton(TestFileLogger::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects missing intersection dependencies
+ *
+ * An intersection dependency without an implementation satisfying every required contract must produce a missing
+ * dependency issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIntersectionDependencyWithoutCandidate ():void {
+
+ $container = new Container;
+
+ $container->singleton(IntersectionConsumer::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates the first DNF intersection branch
+ *
+ * A DNF dependency must pass validation when one registered implementation satisfies the first intersection branch.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDnfFirstBranchValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfFirstBranchConsumer::class);
+ $container->singleton(DnfFileLogger::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates the second DNF-named branch
+ *
+ * A DNF dependency must pass validation when one registered implementation satisfies its named alternative.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDnfSecondBranchValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfSecondBranchConsumer::class);
+ $container->singleton(DnfCacheService::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates a DNF dependency containing two intersection branches
+ *
+ * A registered implementation satisfying either complete intersection branch must be accepted by dependency
+ * graph validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDnfTwoIntersectionBranches ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfTwoIntersectionsConsumer::class);
+ $container->singleton(DnfSerializableCache::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates an implementation satisfying multiple DNF branches
+ *
+ * A single registered implementation satisfying more than one DNF branch must not be considered ambiguous.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDnfSingleImplementationMatchingMultipleBranches ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfTwoIntersectionsConsumer::class);
+ $container->singleton(DnfUniversalService::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Detects ambiguous DNF dependencies
+ *
+ * Multiple registered implementations satisfying different DNF alternatives must produce an ambiguous dependency
+ * issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testAmbiguousDnfDependency ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfFirstBranchConsumer::class);
+
+ $container->singleton(DnfFileLogger::class);
+ $container->singleton(DnfCacheService::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::AMBIGUOUS_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects missing DNF dependency candidates
+ *
+ * A required DNF dependency must produce a missing dependency issue when no registered implementation satisfies
+ * any complete branch.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMissingDnfDependency ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfNoCandidateConsumer::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Accepts a DNF dependency with a null default
+ *
+ * A DNF constructor parameter with an explicit null default must remain valid when no eligible implementation is
+ * registered.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDnfDependencyWithNullDefault ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfDefaultConsumer::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Rejects a required nullable DNF dependency without candidates
+ *
+ * Nullable constructor parameters without explicit default values remain required and must produce a missing
+ * dependency issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testRequiredNullableDnfDependency ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfRequiredNullableConsumer::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates nested DNF constructor dependencies
+ *
+ * Dependency graph validation must recursively inspect constructor dependencies selected through DNF types.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testNestedDnfDependencyGraph ():void {
+
+ $container = new Container;
+
+ $container->singleton(DnfNestedConsumer::class);
+ $container->singleton(DnfFileLogger::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::MISSING_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates an empty tagged collection
+ *
+ * An unknown tag must produce an empty collection without introducing
+ * missing dependency validation issues.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testEmptyTaggedCollectionValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(TaggedValidationConsumer::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType($result, IssueType::MISSING_DEPENDENCY)
+ );
+
+ }
+
+ /**
+ * ### Validates multiple tagged services
+ *
+ * Every tagged service must be included in dependency graph validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMultipleTaggedServicesValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(TaggedValidationConsumer::class);
+ $container->singleton(Logger::class);
+ $container->singleton(ValidationConstructionProbe::class);
+
+ $container->tag(
+ 'validation.services',
+ Logger::class,
+ ValidationConstructionProbe::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Detects circular dependencies through tagged injection
+ *
+ * Tagged collection traversal must preserve the active dependency path
+ * so that circular dependencies are detected.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularTaggedDependencyValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(TaggedCircularConsumer::class);
+
+ $container->tag(
+ 'validation.circular',
+ TaggedCircularConsumer::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType($result, IssueType::CIRCULAR_DEPENDENCY)
+ );
+
+ }
+
+ /**
+ * ### Validates tagged aliases
+ *
+ * Tagged aliases must be resolved to their canonical service definitions during dependency graph validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTaggedAliasValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(TaggedValidationConsumer::class);
+ $container->singleton(TestFileLogger::class);
+
+ $container->alias(
+ TestLogger::class,
+ TestFileLogger::class
+ );
+
+ $container->tag(
+ 'validation.services',
+ TestLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Does not execute tagged factories during validation
+ *
+ * Factory callbacks must never be executed while inspecting tagged constructor dependencies.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTaggedFactoryIsNotExecutedDuringValidation ():void {
+
+ $executed = false;
+
+ $container = new Container;
+
+ $container->singleton(TaggedValidationConsumer::class);
+
+ $container->factory(
+ LoggerInterface::class,
+ function ($resolver) use (&$executed):object {
+
+ $executed = true;
+
+ return new Logger;
+
+ }
+ );
+
+ $container->tag(
+ 'validation.services',
+ LoggerInterface::class
+ );
+
+ $container->validate();
+
+ self::assertFalse($executed);
+
+ }
+
+ /**
+ * ### Validates an empty collection dependency
+ *
+ * Collection injection must remain valid when no matching service implementations are registered.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testEmptyCollectionDependencyValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates a collection with one implementation
+ *
+ * A collection containing one compatible registered implementation must pass static validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testSingleCollectionImplementationValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+ $container->singleton(CollectionValidationFirst::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates a collection with multiple implementations
+ *
+ * All compatible registered implementations must be accepted without producing ambiguity errors.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMultipleCollectionImplementationsValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+ $container->singleton(CollectionValidationFirst::class);
+ $container->singleton(CollectionValidationSecond::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType($result, IssueType::AMBIGUOUS_DEPENDENCY)
+ );
+
+ }
+
+ /**
+ * ### Ignores unrelated collection implementations
+ *
+ * Services that do not satisfy the requested collection element type must not affect collection validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnrelatedCollectionImplementationValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+ $container->singleton(Logger::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates collection aliases without duplicate traversal
+ *
+ * Multiple identifiers resolving to the same canonical service must not introduce duplicate dependency issues.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCollectionAliasDeduplicationValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+ $container->singleton(CollectionValidationFirst::class);
+
+ $container->alias(
+ CollectionValidationContract::class,
+ CollectionValidationFirst::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Detects circular dependencies through collection injection
+ *
+ * A service included in its own injected collection must produce a circular dependency validation issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCircularCollectionDependencyValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationCircular::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType($result, IssueType::CIRCULAR_DEPENDENCY)
+ );
+
+ }
+
+ /**
+ * ### Detects missing dependencies inside collection members
+ *
+ * Collection validation must recursively inspect selected implementations and report unresolved required
+ * constructor dependencies.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMissingCollectionMemberDependencyValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+ $container->singleton(CollectionValidationMissingDependency::class);
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertNotEmpty(
+ $this->issuesOfType($result, IssueType::MISSING_DEPENDENCY)
+ );
+
+ }
+
+ /**
+ * ### Does not execute collection factories during validation
+ *
+ * Static collection validation must never invoke registered factory callbacks.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCollectionFactoryIsNotExecutedDuringValidation ():void {
+
+ $executed = false;
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationConsumer::class);
+
+ $container->factory(
+ CollectionValidationContract::class,
+ function ($resolver) use (&$executed):object {
+
+ $executed = true;
+
+ return new CollectionValidationFirst;
+
+ }
+ );
+
+ $container->validate();
+
+ self::assertFalse($executed);
+
+ }
+
+ /**
+ * ### Validates nested collection dependencies
+ *
+ * Collection injection must be analyzed recursively when encountered inside nested constructor dependency graphs.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testNestedCollectionDependencyValidation ():void {
+
+ $container = new Container;
+
+ $container->singleton(CollectionValidationNested::class);
+ $container->singleton(CollectionValidationFirst::class);
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ }
+
+ /**
+ * ### Validates compatible scalar parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidScalarParameterBindings ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ [
+ 'port' => 3307,
+ 'host' => 'localhost',
+ 'timeout' => 2.5
+ ]
+ );
+
+ self::assertTrue($container->validate()->isValid());
+
+ }
+
+ /**
+ * ### Detects incompatible scalar parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidScalarParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['port' => 'invalid']
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType($result, IssueType::INVALID_PARAMETER_BINDING)
+ );
+
+ }
+
+ /**
+ * ### Accepts integer values for float parameters
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIntegerToFloatParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['timeout' => 5]
+ );
+
+ self::assertTrue($container->validate()->isValid());
+
+ }
+
+ /**
+ * ### Accepts null for nullable parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testNullableParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['host' => null]
+ );
+
+ self::assertTrue($container->validate()->isValid());
+
+ }
+
+ /**
+ * ### Rejects null for non-nullable parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidNullableParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['port' => null]
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType($result, IssueType::INVALID_PARAMETER_BINDING)
+ );
+
+ }
+
+ /**
+ * ### Validates compatible object parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidObjectParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['dependency' => new ParameterBindingValidationService]
+ );
+
+ self::assertTrue($container->validate()->isValid());
+
+ }
+
+ /**
+ * ### Detects incompatible object parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidObjectParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['dependency' => new \stdClass]
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType($result, IssueType::INVALID_PARAMETER_BINDING)
+ );
+
+ }
+
+ /**
+ * ### Validates compatible union parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidUnionParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationComposite::class,
+ ['union' => 'value']
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects incompatible union parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidUnionParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationComposite::class,
+ ['union' => new \stdClass]
+ );
+
+ $result = $container->validate();
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType($result, IssueType::INVALID_PARAMETER_BINDING)
+ );
+
+ }
+
+ /**
+ * ### Validates compatible intersection parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidIntersectionParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationComposite::class,
+ ['intersection' => new \ArrayObject]
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects incompatible intersection parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidIntersectionParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationComposite::class,
+ ['intersection' => new \stdClass]
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates compatible DNF parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidDnfParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationComposite::class,
+ ['dnf' => new \ArrayObject]
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects incompatible DNF parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidDnfParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationComposite::class,
+ ['dnf' => new \stdClass]
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects bindings for nonexistent constructor parameters
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testNonexistentParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['unknown' => 'value']
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates explicit bindings before constructor defaults
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testInvalidBindingOverridesDefault ():void {
+
+ $container = new Container;
+
+ $container->singleton(ParameterBindingValidationService::class);
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ ['port' => 'invalid']
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects parameter bindings for unknown implementation classes
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnknownParameterBindingClass ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ 'FireHub\\Tests\\Container\\Stubs\\NonexistentBindingClass',
+ ['value' => 1]
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Collects multiple invalid parameter bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testMultipleInvalidParameterBindings ():void {
+
+ $container = new Container;
+
+ $container->bindParameters(
+ ParameterBindingValidationService::class,
+ [
+ 'port' => 'invalid',
+ 'host' => new \stdClass,
+ 'timeout' => false
+ ]
+ );
+
+ self::assertCount(
+ 3,
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::INVALID_PARAMETER_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates a named contextual dependency binding
+ *
+ * A compatible contextual implementation must satisfy the consumer's declared dependency contract.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidNamedContextualBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualNamedConsumer::class);
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates contextual bindings without consumer registration
+ *
+ * Contextual binding configuration must be validated independently of explicit consumer service registration.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testContextualBindingWithoutRegisteredConsumer ():void {
+
+ $container = new Container;
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates contextual bindings for union dependencies
+ *
+ * A contextual dependency contract matching one union member must be recognized as used.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidUnionContextualBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualUnionConsumer::class);
+
+ $container->bindContextual(
+ ContextualUnionConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates contextual bindings for intersection dependencies
+ *
+ * A contextual implementation satisfying every intersection member must pass static validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidIntersectionContextualBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualIntersectionConsumer::class);
+
+ $container->bindContextual(
+ ContextualIntersectionConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates contextual bindings for DNF dependencies
+ *
+ * Contextual dependency contracts referenced within nested DNF branches must be recognized during validation.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testValidDnfContextualBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualDnfConsumer::class);
+
+ $container->bindContextual(
+ ContextualDnfConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects contextual bindings referencing unknown consumers
+ *
+ * A contextual binding whose consumer class does not exist must produce an invalid contextual binding issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnknownContextualConsumer ():void {
+
+ $container = new Container;
+
+ $container->bindContextual(
+ 'FireHub\\Tests\\Container\\Stubs\\MissingContextualConsumer',
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects contextual bindings referencing unknown dependency contracts
+ *
+ * Contextual dependency contracts must reference existing classes or interfaces.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnknownContextualDependency ():void {
+
+ $container = new Container;
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingContextualContract',
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects contextual bindings referencing unknown implementations
+ *
+ * A contextual implementation must reference a registered service or an implicitly constructible class.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnknownContextualImplementation ():void {
+
+ $container = new Container;
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ 'FireHub\\Tests\\Container\\Stubs\\MissingContextualImplementation'
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects incompatible contextual implementations
+ *
+ * A contextual implementation that does not satisfy its registered dependency contract must be rejected.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIncompatibleContextualImplementation ():void {
+
+ $container = new Container;
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ \stdClass::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Reports contextual bindings unused by constructor injection
+ *
+ * A structurally valid contextual binding must produce a warning when its dependency contract is not referenced
+ * by any eligible constructor parameter.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnusedContextualBinding ():void {
+
+ $container = new Container;
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualCache::class,
+ ContextualCacheService::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ $issues = $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ Severity::WARNING,
+ $issues[0]->severity
+ );
+
+ }
+
+ /**
+ * ### Reports contextual bindings shadowed by explicit parameter bindings
+ *
+ * Explicit constructor parameter bindings take precedence over contextual dependency resolution.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testContextualBindingShadowedByParameterBinding ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualNamedConsumer::class);
+
+ $container->bindParameters(
+ ContextualNamedConsumer::class,
+ [
+ 'logger' => new ContextualFileLogger
+ ]
+ );
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::UNUSED_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Detects ambiguous contextual dependency candidates
+ *
+ * Two contextual bindings selecting distinct implementations for the same union parameter must produce an
+ * ambiguous dependency issue.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testAmbiguousContextualBindings ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualUnionConsumer::class);
+
+ $container->bindContextual(
+ ContextualUnionConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $container->bindContextual(
+ ContextualUnionConsumer::class,
+ ContextualCache::class,
+ ContextualCacheService::class
+ );
+
+ $result = $container->validate();
+
+ self::assertFalse($result->isValid());
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::AMBIGUOUS_DEPENDENCY
+ )
+ );
+
+ }
+
+ /**
+ * ### Validates contextual implementations referenced through service aliases
+ *
+ * A contextual implementation referenced through a valid alias must retain its underlying type compatibility.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testContextualBindingWithAliasImplementation ():void {
+
+ $container = new Container;
+
+ $container->singleton(ContextualFileLogger::class);
+
+ $container->alias(
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ }
+
+ /**
+ * ### Does not execute contextual factory implementations during validation
+ *
+ * Static contextual binding validation must not execute registered factory callbacks.
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testContextualBindingValidationDoesNotExecuteFactories ():void {
+
+ $executions = 0;
+
+ $container = new Container;
+
+ $container->singleton(ContextualNamedConsumer::class);
+
+ $container->factory(
+ ContextualLogger::class,
+ function ($resolver) use (&$executions) {
+
+ $executions++;
+
+ return new ContextualFileLogger;
+
+ }
+ );
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualLogger::class
+ );
+
+ $result = $container->validate();
+
+ self::assertSame(0, $executions);
+
+ self::assertTrue($result->isValid());
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $result,
+ IssueType::INVALID_CONTEXTUAL_BINDING
+ )
+ );
+
+ self::assertCount(
+ 1,
+ $this->issuesOfType(
+ $result,
+ IssueType::UNVERIFIABLE_FACTORY
+ )
+ );
+
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Unit/DnfResolutionTest.php b/tests/Unit/DnfResolutionTest.php
index eb74cdb..d295b54 100644
--- a/tests/Unit/DnfResolutionTest.php
+++ b/tests/Unit/DnfResolutionTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/LifetimeRuntimeTest.php b/tests/Unit/LifetimeRuntimeTest.php
new file mode 100644
index 0000000..86f5f62
--- /dev/null
+++ b/tests/Unit/LifetimeRuntimeTest.php
@@ -0,0 +1,145 @@
+
+ * @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\Tests\Container\Unit;
+
+use FireHub\Container\Container;
+use FireHub\Container\Exception\ScopeRequiredException;
+use FireHub\Testing\FireHubTestCase;
+use FireHub\Tests\Container\Stubs\ {
+ LifetimeValidationNestedSingleton, LifetimeValidationScoped,
+ LifetimeValidationSingleton, LifetimeValidationTransient
+};
+use PHPUnit\Framework\Attributes\ {
+ CoversClass, Group, Small
+};
+
+/**
+ * ### Tests runtime service lifetime protection
+ * @since 1.0.0
+ */
+#[Small]
+#[Group('lifetime')]
+#[CoversClass(Container::class)]
+final class LifetimeRuntimeTest extends FireHubTestCase {
+
+ /**
+ * ### Rejects direct singleton-to-scoped dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDirectSingletonScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->singleton(LifetimeValidationSingleton::class);
+
+ $scope = $container->createScope();
+
+ try {
+
+ $this->expectException(ScopeRequiredException::class);
+
+ $scope->get(LifetimeValidationSingleton::class);
+
+ } finally {
+
+ $scope->close();
+
+ }
+
+ }
+
+ /**
+ * ### Rejects transitive singleton-to-scoped dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTransitiveSingletonScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->transient(LifetimeValidationTransient::class);
+ $container->singleton(LifetimeValidationNestedSingleton::class);
+
+ $scope = $container->createScope();
+
+ try {
+
+ $this->expectException(ScopeRequiredException::class);
+
+ $scope->get(LifetimeValidationNestedSingleton::class);
+
+ } finally {
+
+ $scope->close();
+
+ }
+
+ }
+
+ /**
+ * ### Allows scoped dependencies inside transient services
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTransientScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->transient(LifetimeValidationTransient::class);
+
+ $scope = $container->createScope();
+
+ try {
+
+ $first = $scope->get(LifetimeValidationTransient::class);
+ $second = $scope->get(LifetimeValidationTransient::class);
+
+ self::assertNotSame($first, $second);
+ self::assertSame($first->dependency, $second->dependency);
+
+ } finally {
+
+ $scope->close();
+
+ }
+
+ }
+
+ /**
+ * ### Rejects singleton-to-scoped dependencies from root resolution
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testRootSingletonScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->singleton(LifetimeValidationSingleton::class);
+
+ $this->expectException(ScopeRequiredException::class);
+
+ $container->get(LifetimeValidationSingleton::class);
+
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Unit/LifetimeValidationTest.php b/tests/Unit/LifetimeValidationTest.php
new file mode 100644
index 0000000..47c9d6b
--- /dev/null
+++ b/tests/Unit/LifetimeValidationTest.php
@@ -0,0 +1,422 @@
+
+ * @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\Tests\Container\Unit;
+
+use FireHub\Container\Container;
+use FireHub\Container\Validation\ {
+ Issue, IssueType, Result, Severity
+};
+use FireHub\Testing\FireHubTestCase;
+use FireHub\Tests\Container\Stubs\ {
+ CollectionValidationConsumer, CollectionValidationFirst, ContextualDnfConsumer, ContextualFileLogger,
+ ContextualIntersectionConsumer, ContextualLogger, ContextualNamedConsumer, ContextualUnionConsumer,
+ LifetimeValidationExplicitConsumer, LifetimeValidationFactoryConsumer, LifetimeValidationNestedSingleton,
+ LifetimeValidationSafeConsumer, LifetimeValidationSafeSingleton, LifetimeValidationScoped,
+ LifetimeValidationSingleton, LifetimeValidationTransient, TaggedFileLogger, TaggedValidationConsumer
+};
+use PHPUnit\Framework\Attributes\ {
+ CoversClass, Group, Small
+};
+
+/**
+ * ### Tests service lifetime validation
+ * @since 1.0.0
+ */
+#[Small]
+#[Group('validation')]
+#[CoversClass(Container::class)]
+final class LifetimeValidationTest extends FireHubTestCase {
+
+ /**
+ * ### Gets validation issues of a specified type
+ * @since 1.0.0
+ *
+ * @return list
+ */
+ private function issuesOfType (Result $result, IssueType $type):array {
+
+ return array_values(array_filter(
+ $result->issues(),
+ static fn (Issue $issue):bool => $issue->type === $type
+ ));
+
+ }
+
+ /**
+ * ### Detects direct singleton-to-scoped dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDirectSingletonScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->singleton(LifetimeValidationSingleton::class);
+
+ $result = $container->validate();
+
+ $issues = $this->issuesOfType(
+ $result,
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertFalse($result->isValid());
+ self::assertCount(1, $issues);
+ self::assertSame(Severity::ERROR, $issues[0]->severity);
+ self::assertSame(
+ [
+ LifetimeValidationSingleton::class,
+ LifetimeValidationScoped::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects transitive singleton-to-scoped dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTransitiveSingletonScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->transient(LifetimeValidationTransient::class);
+ $container->singleton(LifetimeValidationNestedSingleton::class);
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ LifetimeValidationNestedSingleton::class,
+ LifetimeValidationTransient::class,
+ LifetimeValidationScoped::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Accepts singleton-to-singleton dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testSingletonDependencyIsValid ():void {
+
+ $container = new Container;
+
+ $container->singleton(LifetimeValidationSafeSingleton::class);
+ $container->singleton(LifetimeValidationSafeConsumer::class);
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ )
+ );
+
+ }
+
+ /**
+ * ### Accepts transient-to-scoped dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTransientScopedDependencyIsValid ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+ $container->transient(LifetimeValidationTransient::class);
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ )
+ );
+
+ }
+
+ /**
+ * ### Ignores explicitly supplied constructor dependencies
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testExplicitParameterBindingDoesNotCaptureScopedService ():void {
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+
+ $container->singleton(LifetimeValidationExplicitConsumer::class);
+
+ $container->bindParameters(
+ LifetimeValidationExplicitConsumer::class,
+ [
+ 'dependency' => new LifetimeValidationScoped
+ ]
+ );
+
+ self::assertSame(
+ [],
+ $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ )
+ );
+
+ }
+
+ /**
+ * ### Does not execute factories during lifetime validation
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testFactoryIsNotExecuted ():void {
+
+ $executions = 0;
+
+ $container = new Container;
+
+ $container->scoped(LifetimeValidationScoped::class);
+
+ $container->factory(
+ LifetimeValidationFactoryConsumer::class,
+ static function () use (&$executions):LifetimeValidationFactoryConsumer {
+
+ $executions++;
+
+ return new LifetimeValidationFactoryConsumer(
+ new LifetimeValidationScoped
+ );
+
+ }
+ );
+
+ $container->validate();
+
+ self::assertSame(0, $executions);
+
+ }
+
+ /**
+ * ### Detects scoped dependencies selected through contextual bindings
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testContextualScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(ContextualFileLogger::class);
+ $container->singleton(ContextualNamedConsumer::class);
+
+ $container->bindContextual(
+ ContextualNamedConsumer::class,
+ ContextualLogger::class,
+ ContextualFileLogger::class
+ );
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ ContextualNamedConsumer::class,
+ ContextualFileLogger::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects scoped dependencies selected through union types
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testUnionScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(ContextualFileLogger::class);
+ $container->singleton(ContextualUnionConsumer::class);
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ ContextualUnionConsumer::class,
+ ContextualFileLogger::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects scoped dependencies selected through intersection types
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testIntersectionScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(ContextualFileLogger::class);
+ $container->singleton(ContextualIntersectionConsumer::class);
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ ContextualIntersectionConsumer::class,
+ ContextualFileLogger::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects scoped dependencies selected through DNF types
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testDnfScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(ContextualFileLogger::class);
+ $container->singleton(ContextualDnfConsumer::class);
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ ContextualDnfConsumer::class,
+ ContextualFileLogger::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects scoped dependencies injected through tags
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testTaggedScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(TaggedFileLogger::class);
+ $container->singleton(TaggedValidationConsumer::class);
+
+ $container->tag(
+ 'validation.services',
+ TaggedFileLogger::class
+ );
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ TaggedValidationConsumer::class,
+ TaggedFileLogger::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+ /**
+ * ### Detects scoped dependencies injected through collections
+ * @since 1.0.0
+ *
+ * @return void
+ */
+ public function testCollectionScopedDependency ():void {
+
+ $container = new Container;
+
+ $container->scoped(CollectionValidationFirst::class);
+ $container->singleton(CollectionValidationConsumer::class);
+
+ $issues = $this->issuesOfType(
+ $container->validate(),
+ IssueType::LIFETIME_MISMATCH
+ );
+
+ self::assertCount(1, $issues);
+
+ self::assertSame(
+ [
+ CollectionValidationConsumer::class,
+ CollectionValidationFirst::class
+ ],
+ $issues[0]->path
+ );
+
+ }
+
+}
\ No newline at end of file
diff --git a/tests/Unit/MethodInjectionTest.php b/tests/Unit/MethodInjectionTest.php
index f79f0aa..9be5c39 100644
--- a/tests/Unit/MethodInjectionTest.php
+++ b/tests/Unit/MethodInjectionTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/ParameterBindingTest.php b/tests/Unit/ParameterBindingTest.php
index 1b3833b..4e60c9e 100644
--- a/tests/Unit/ParameterBindingTest.php
+++ b/tests/Unit/ParameterBindingTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/TaggedInjectionTest.php b/tests/Unit/TaggedInjectionTest.php
index c08a879..078233c 100644
--- a/tests/Unit/TaggedInjectionTest.php
+++ b/tests/Unit/TaggedInjectionTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/
diff --git a/tests/Unit/TaggedServicesTest.php b/tests/Unit/TaggedServicesTest.php
index bd23a80..59f9557 100644
--- a/tests/Unit/TaggedServicesTest.php
+++ b/tests/Unit/TaggedServicesTest.php
@@ -7,7 +7,7 @@
* @copyright 2026-present The FireHub Project - All rights reserved
* @license https://opensource.org/license/Apache-2-0 Apache License, Version 2.0
*
- * @php-version >=8.5
+ * @php-version >=7.4
* @package Container\Tests
*/