Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
ReflectionContainer
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
9 / 9
36
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 reflectClass
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 instantiateClass
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 resolveArguments
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
7
 resolveParameter
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 resolveParameterClassName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 resolveDefaultValue
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5/**
6 * This file is part of php-fast-forward/container.
7 *
8 * This source file is subject to the license bundled
9 * with this source code in the file LICENSE.
10 *
11 * @copyright Copyright (c) 2025-2026 Felipe SayĆ£o Lobato Abreu <github@mentordosnerds.com>
12 * @license   https://opensource.org/licenses/MIT MIT License
13 *
14 * @see       https://github.com/php-fast-forward/container
15 * @see       https://github.com/php-fast-forward
16 * @see       https://datatracker.ietf.org/doc/html/rfc2119
17 */
18
19namespace FastForward\Container\Autowire;
20
21use ReflectionClass;
22use ReflectionMethod;
23use ReflectionNamedType;
24use ReflectionParameter;
25use Throwable;
26use FastForward\Container\ContainerInterface;
27use FastForward\Container\Exception\AutowiringException;
28use FastForward\Container\Exception\ContainerException;
29use FastForward\Container\Exception\NotFoundException;
30use Psr\Container\ContainerInterface as PsrContainerInterface;
31use Psr\Container\NotFoundExceptionInterface;
32
33/**
34 * Resolves constructor dependencies through the composed container.
35 *
36 * AutowireContainer connects this resolver to the same aggregate used for nested
37 * lookups, allowing recursive autowiring alongside explicitly registered services.
38 *
39 * @internal
40 */
41 class ReflectionContainer implements ContainerInterface
42{
43    /**
44     * @var array<string, object> successfully constructed services
45     */
46    private array $resolved = [];
47
48    /**
49     * @var array<string, true> the active construction path
50     */
51    private array $resolving = [];
52
53    /**
54     * @param PsrContainerInterface $wrapperContainer the complete container composition
55     */
56    public function __construct(
57        private  PsrContainerInterface $wrapperContainer
58    ) {}
59
60    /**
61     * Checks class metadata without constructing an object.
62     *
63     * @param string $id the entry identifier
64     *
65     * @return bool whether the class can be instantiated
66     *
67     * @throws ContainerException if class loading or reflection fails
68     */
69    public function has(string $id): bool
70    {
71        return $this->reflectClass($id) instanceof ReflectionClass;
72    }
73
74    /**
75     * Constructs and caches a concrete class, detecting recursive dependencies.
76     *
77     * @param string $id the class identifier
78     *
79     * @return object the resolved service
80     *
81     * @throws NotFoundException if the class is not instantiable
82     * @throws AutowiringException if required arguments cannot be inferred
83     * @throws ContainerException if construction fails or a dependency cycle exists
84     */
85    public function get(string $id): object
86    {
87        if (isset($this->resolved[$id])) {
88            return $this->resolved[$id];
89        }
90
91        $class = $this->reflectClass($id);
92
93        if (! $class instanceof ReflectionClass) {
94            throw NotFoundException::forServiceID($id);
95        }
96
97        if (isset($this->resolving[$id])) {
98            throw ContainerException::forCircularDependency([...array_keys($this->resolving), $id]);
99        }
100
101        $this->resolving[$id] = true;
102
103        try {
104            return $this->resolved[$id] = $this->instantiateClass($id, $class);
105        } finally {
106            unset($this->resolving[$id]);
107        }
108    }
109
110    /**
111     * Looks up instantiable class metadata within the PSR-11 error boundary.
112     *
113     * @param string $id the class identifier
114     *
115     * @return ReflectionClass<object>|null the class metadata, if instantiable
116     *
117     * @throws ContainerException if class loading or reflection fails
118     */
119    private function reflectClass(string $id): ?ReflectionClass
120    {
121        try {
122            if (! class_exists($id)) {
123                return null;
124            }
125
126            $class = new ReflectionClass($id);
127
128            return $class->isInstantiable() ? $class : null;
129        } catch (Throwable $throwable) {
130            throw ContainerException::forInvalidService($id, $throwable);
131        }
132    }
133
134    /**
135     * Invokes the constructor while retaining storage for reference arguments.
136     *
137     * @param string $id the requested service identifier
138     * @param ReflectionClass<object> $class the instantiable class
139     *
140     * @return object the constructed service
141     *
142     * @throws AutowiringException if required arguments cannot be inferred
143     * @throws ContainerException if dependency resolution or construction fails
144     */
145    private function instantiateClass(string $id, ReflectionClass $class): object
146    {
147        $constructor = $class->getConstructor();
148        $values = $this->resolveArguments($constructor);
149        $arguments = $values;
150
151        foreach ($constructor?->getParameters() ?? [] as $parameter) {
152            $name = $parameter->getName();
153
154            if ($parameter->isPassedByReference() && \array_key_exists($name, $values)) {
155                $arguments[$name] = &$values[$name];
156            }
157        }
158
159        try {
160            return $class->newInstanceArgs($arguments);
161        } catch (Throwable $throwable) {
162            throw ContainerException::forInvalidService($id, $throwable);
163        }
164    }
165
166    /**
167     * Resolves constructor values, omitting variadics and unavailable optional arguments.
168     *
169     * @param ReflectionMethod|null $constructor the constructor, if declared
170     *
171     * @return array<string, mixed> resolved values keyed by parameter name
172     *
173     * @throws AutowiringException if required arguments cannot be inferred
174     * @throws ContainerException if resolving a dependency or default fails
175     */
176    private function resolveArguments(?ReflectionMethod $constructor): array
177    {
178        if (! $constructor instanceof ReflectionMethod) {
179            return [];
180        }
181
182        $arguments = [];
183
184        foreach ($constructor->getParameters() as $parameter) {
185            if ($parameter->isVariadic()) {
186                continue;
187            }
188
189            try {
190                $arguments[$parameter->getName()] = $this->resolveParameter(
191                    $parameter,
192                    $constructor->getDeclaringClass()
193                );
194            } catch (AutowiringException $exception) {
195                if (! $parameter->isOptional() || $parameter->isDefaultValueAvailable()) {
196                    throw $exception;
197                }
198            }
199        }
200
201        return $arguments;
202    }
203
204    /**
205     * Resolves named object types before considering an optional default.
206     *
207     * @param ReflectionParameter $parameter the constructor parameter
208     * @param ReflectionClass<object> $declaringClass the class that declares the constructor
209     *
210     * @return mixed the dependency or declared default
211     *
212     * @throws AutowiringException if no dependency or default can be supplied
213     * @throws ContainerException if evaluating a declared default fails
214     */
215    private function resolveParameter(ReflectionParameter $parameter, ReflectionClass $declaringClass): mixed
216    {
217        $id = $this->resolveParameterClassName($parameter, $declaringClass);
218        $previous = null;
219
220        if (null !== $id) {
221            try {
222                return $this->wrapperContainer->get($id);
223            } catch (NotFoundExceptionInterface|AutowiringException $exception) {
224                $previous = $exception;
225            }
226        }
227
228        return $this->resolveDefaultValue($parameter, $declaringClass, $previous);
229    }
230
231    /**
232     * Maps a named object type to the service identifier in its declaring scope.
233     *
234     * @param ReflectionParameter $parameter the constructor parameter
235     * @param ReflectionClass<object> $declaringClass the class that declares the constructor
236     *
237     * @return string|null the class name, or null for unsupported parameter types
238     */
239    private function resolveParameterClassName(ReflectionParameter $parameter, ReflectionClass $declaringClass): ?string
240    {
241        $type = $parameter->getType();
242
243        if (! $type instanceof ReflectionNamedType || $type->isBuiltin()) {
244            return null;
245        }
246
247        return match ($type->getName()) {
248            'self' => $declaringClass->getName(),
249            'parent' => get_parent_class($declaringClass->getName()) ?: null,
250            default => $type->getName(),
251        };
252    }
253
254    /**
255     * Reads a declared default or reports why an explicit factory is required.
256     *
257     * @param ReflectionParameter $parameter the constructor parameter
258     * @param ReflectionClass<object> $declaringClass the class that declares the constructor
259     * @param Throwable|null $previous the structural dependency resolution failure
260     *
261     * @return mixed the declared default
262     *
263     * @throws AutowiringException if the parameter has no declared default
264     * @throws ContainerException if evaluating the default fails
265     */
266    private function resolveDefaultValue(
267        ReflectionParameter $parameter,
268        ReflectionClass $declaringClass,
269        ?Throwable $previous
270    ): mixed {
271        if (! $parameter->isDefaultValueAvailable()) {
272            $type = $parameter->getType();
273
274            throw AutowiringException::forUnresolvableParameter(
275                $declaringClass->getName(),
276                $parameter->getName(),
277                null === $type ? null : (string) $type,
278                $previous,
279            );
280        }
281
282        try {
283            return $parameter->getDefaultValue();
284        } catch (Throwable $throwable) {
285            throw ContainerException::forInvalidService($declaringClass->getName(), $throwable);
286        }
287    }
288}