Passed
Pull Request — master (#1077)
by Maxim
19:13 queued 06:20
created

Factory::resolveProxy()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 13
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 13
ccs 5
cts 5
cp 1
rs 10
cc 3
nc 2
nop 4
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Core\Internal;
6
7
use Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use ReflectionFunctionAbstract as ContextFunction;
10
use ReflectionParameter;
11
use Spiral\Core\Attribute;
12
use Spiral\Core\BinderInterface;
13
use Spiral\Core\Config;
14
use Spiral\Core\Container\InjectorInterface;
15
use Spiral\Core\Container\SingletonInterface;
16
use Spiral\Core\Exception\Container\AutowireException;
17
use Spiral\Core\Exception\Container\ContainerException;
18
use Spiral\Core\Exception\Container\InjectionException;
19
use Spiral\Core\Exception\Container\NotCallableException;
20
use Spiral\Core\Exception\Container\NotFoundException;
21
use Spiral\Core\Exception\Resolver\ValidationException;
22
use Spiral\Core\Exception\Resolver\WrongTypeException;
23
use Spiral\Core\Exception\Scope\BadScopeException;
24
use Spiral\Core\FactoryInterface;
25
use Spiral\Core\Internal\Common\DestructorTrait;
26
use Spiral\Core\Internal\Common\Registry;
27
use Spiral\Core\Internal\Factory\Ctx;
28
use Spiral\Core\InvokerInterface;
29
use Spiral\Core\ResolverInterface;
30
use Stringable;
31
use WeakReference;
32
33
/**
34
 * @internal
35
 */
36
final class Factory implements FactoryInterface
37
{
38
    use DestructorTrait;
39
40
    private State $state;
41
    private BinderInterface $binder;
42
    private InvokerInterface $invoker;
43
    private ContainerInterface $container;
44
    private ResolverInterface $resolver;
45
    private Tracer $tracer;
46
    private Scope $scope;
47
48 1311
    public function __construct(Registry $constructor)
49
    {
50 1311
        $constructor->set('factory', $this);
51
52 1311
        $this->state = $constructor->get('state', State::class);
53 1311
        $this->binder = $constructor->get('binder', BinderInterface::class);
54 1311
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
55 1311
        $this->container = $constructor->get('container', ContainerInterface::class);
56 1311
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
57 1311
        $this->tracer = $constructor->get('tracer', Tracer::class);
58 1311
        $this->scope = $constructor->get('scope', Scope::class);
59
    }
60
61
    /**
62
     * @param Stringable|string|null $context Related to parameter caused injection if any.
63
     *
64
     * @throws \Throwable
65
     */
66 1119
    public function make(string $alias, array $parameters = [], Stringable|string|null $context = null): mixed
67
    {
68 1119
        if ($parameters === [] && \array_key_exists($alias, $this->state->singletons)) {
69 505
            return $this->state->singletons[$alias];
70
        }
71
72 1119
        $binding = $this->state->bindings[$alias] ?? null;
73
74 1119
        if ($binding === null) {
75 925
            return $this->resolveWithoutBinding($alias, $parameters, $context);
76
        }
77
78
        try {
79 1044
            $this->tracer->push(
80 1044
                false,
81 1044
                action: 'resolve from binding',
0 ignored issues
show
Bug introduced by
'resolve from binding' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

81
                /** @scrutinizer ignore-type */ action: 'resolve from binding',
Loading history...
82 1044
                alias: $alias,
83 1044
                scope: $this->scope->getScopeName(),
0 ignored issues
show
Bug introduced by
$this->scope->getScopeName() of type null|string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

83
                /** @scrutinizer ignore-type */ scope: $this->scope->getScopeName(),
Loading history...
84 1044
                context: $context,
0 ignored issues
show
Bug introduced by
$context of type Stringable|null|string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

84
                /** @scrutinizer ignore-type */ context: $context,
Loading history...
85 1044
                binding: $binding,
86 1044
            );
87 1044
            $this->tracer->push(true);
88
89 1044
            unset($this->state->bindings[$alias]);
90 1044
            return match ($binding::class) {
91 1044
                Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $parameters),
92 1044
                Config\Proxy::class,
93 1044
                Config\DeprecationProxy::class => $this->resolveProxy($binding, $alias, $context, $parameters),
94 1044
                Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $parameters),
95 1044
                Config\DeferredFactory::class,
96 1044
                Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $parameters),
97 1044
                Config\Shared::class => $this->resolveShared($binding, $alias, $context, $parameters),
98 1044
                Config\Injectable::class => $this->resolveInjector(
99 1044
                    $binding,
100 1044
                    new Ctx(alias: $alias, class: $alias, context: $context),
101 1044
                    $parameters,
102 1044
                ),
103 1044
                Config\Scalar::class => $binding->value,
0 ignored issues
show
Bug introduced by
The property value does not seem to exist on Spiral\Core\Config\Injectable.
Loading history...
104 1044
                Config\WeakReference::class => $this
105 1044
                    ->resolveWeakReference($binding, $alias, $context, $parameters),
0 ignored issues
show
Bug introduced by
$binding of type Spiral\Core\Config\Injectable is incompatible with the type Spiral\Core\Config\WeakReference expected by parameter $binding of Spiral\Core\Internal\Fac...:resolveWeakReference(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

105
                    ->resolveWeakReference(/** @scrutinizer ignore-type */ $binding, $alias, $context, $parameters),
Loading history...
106 1044
                default => $binding,
107 1044
            };
108
        } finally {
109 1044
            $this->state->bindings[$alias] ??= $binding;
110 1044
            $this->tracer->pop(true);
111 1044
            $this->tracer->pop(false);
112
        }
113
    }
114
115
    /**
116
     * @psalm-suppress UnusedParam
117
     * todo wat should we do with $arguments?
118
     */
119 468
    private function resolveInjector(Config\Injectable $binding, Ctx $ctx, array $arguments)
0 ignored issues
show
Unused Code introduced by
The parameter $arguments is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

119
    private function resolveInjector(Config\Injectable $binding, Ctx $ctx, /** @scrutinizer ignore-unused */ array $arguments)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
120
    {
121 468
        $context = $ctx->context;
122
        try {
123 468
            $reflection = $ctx->reflection ??= new \ReflectionClass($ctx->class);
124
        } catch (\ReflectionException $e) {
125
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
126
        }
127
128 468
        $injector = $binding->injector;
129
130
        try {
131 468
            $injectorInstance = \is_object($injector) ? $injector : $this->container->get($injector);
132
133 461
            if (!$injectorInstance instanceof InjectorInterface) {
134 2
                throw new InjectionException(
135 2
                    \sprintf(
136 2
                        "Class '%s' must be an instance of InjectorInterface for '%s'.",
137 2
                        $injectorInstance::class,
138 2
                        $reflection->getName()
139 2
                    )
140 2
                );
141
            }
142
143
            /** @var array<class-string<InjectorInterface>, \ReflectionMethod|false> $cache reflection for extended injectors */
144 459
            static $cache = [];
145 459
            $extended = $cache[$injectorInstance::class] ??= (
146 459
                static fn (\ReflectionType $type): bool =>
147 18
                $type::class === \ReflectionUnionType::class || (string)$type === 'mixed'
148 459
            )(
149 459
                ($refMethod = new \ReflectionMethod($injectorInstance, 'createInjection'))
150 459
                    ->getParameters()[1]->getType()
151 459
            ) ? $refMethod : false;
152
153 459
            $asIs = $extended && (\is_string($context) || $this->validateArguments($extended, [$reflection, $context]));
154 459
            $instance = $injectorInstance->createInjection($reflection, match (true) {
155 459
                $asIs => $context,
156 459
                $context instanceof ReflectionParameter => $context->getName(),
0 ignored issues
show
Bug introduced by
The method getName() does not exist on Stringable. It seems like you code against a sub-type of Stringable such as Spiral\Reactor\Partial\Method or Spiral\Reactor\AbstractDeclaration or Spiral\Reactor\FunctionDeclaration or Spiral\Reactor\Partial\PhpNamespace or SimpleXMLElement or SimpleXMLIterator or Spiral\Cookies\Cookie or ReflectionNamedType or PhpCsFixer\Console\Comma...beNameNotFoundException or ReflectionExtension or ReflectionProperty or ReflectionFunctionAbstract or ReflectionClassConstant or ReflectionClass or ReflectionZendExtension or ReflectionParameter or MonorepoBuilder202206\Ne...erators\CachingIterator or RectorPrefix202308\Nette\Iterators\CachingIterator or Nette\Iterators\CachingIterator. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

156
                $context instanceof ReflectionParameter => $context->/** @scrutinizer ignore-call */ getName(),
Loading history...
Bug introduced by
The method getName() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

156
                $context instanceof ReflectionParameter => $context->/** @scrutinizer ignore-call */ getName(),

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
157 459
                default => (string)$context,
158 459
            });
159
160 457
            if (!$reflection->isInstance($instance)) {
161 1
                throw new InjectionException(
162 1
                    \sprintf(
163 1
                        "Invalid injection response for '%s'.",
164 1
                        $reflection->getName()
165 1
                    )
166 1
                );
167
            }
168
169 456
            return $instance;
170
        } finally {
171 468
            $this->state->bindings[$ctx->class] ??= $binding;
172
        }
173
    }
174
175 798
    private function resolveAlias(
176
        Config\Alias $binding,
177
        string $alias,
178
        Stringable|string|null $context,
179
        array $arguments,
180
    ): mixed {
181 798
        $result = $binding->alias === $alias
182 402
            ? $this->autowire(
183 402
                new Ctx(alias: $alias, class: $binding->alias, context: $context, singleton: $binding->singleton),
184 402
                $arguments,
185 402
            )
186 798
            //Binding is pointing to something else
187 790
            : $this->make($binding->alias, $arguments, $context);
188
189 795
        if ($binding->singleton && $arguments === []) {
190 522
            $this->state->singletons[$alias] = $result;
191
        }
192
193 795
        return $result;
194
    }
195
196 4
    private function resolveProxy(
197
        Config\Proxy $binding,
198
        string $alias,
199
        Stringable|string|null $context,
200
        array $arguments,
201
    ): mixed {
202 4
        $result = Proxy::create(new \ReflectionClass($binding->getInterface()), $context, new Attribute\Proxy());
203
204 4
        if ($binding->singleton && $arguments === []) {
205 3
            $this->state->singletons[$alias] = $result;
206
        }
207
208 4
        return $result;
209
    }
210
211 862
    private function resolveShared(
212
        Config\Shared $binding,
213
        string $alias,
214
        Stringable|string|null $context,
215
        array $arguments,
216
    ): object {
217 862
        $avoidCache = $arguments !== [];
218 862
        return $avoidCache
219 1
            ? $this->createInstance(
220 1
                new Ctx(alias: $alias, class: $binding->value::class, context: $context),
221 1
                $arguments,
222 1
            )
223 862
            : $binding->value;
224
    }
225
226 7
    private function resolveAutowire(
227
        Config\Autowire $binding,
228
        string $alias,
229
        Stringable|string|null $context,
230
        array $arguments,
231
    ): mixed {
232 7
        $instance = $binding->autowire->resolve($this, $arguments);
233
234 7
        $ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
235 7
        return $this->validateNewInstance($instance, $ctx, $arguments);
236
    }
237
238 476
    private function resolveFactory(
239
        Config\Factory|Config\DeferredFactory $binding,
240
        string $alias,
241
        Stringable|string|null $context,
242
        array $arguments,
243
    ): mixed {
244 476
        $ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
245
        try {
246 476
            $instance = $binding::class === Config\Factory::class && $binding->getParametersCount() === 0
0 ignored issues
show
Bug introduced by
The method getParametersCount() does not exist on Spiral\Core\Config\DeferredFactory. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

246
            $instance = $binding::class === Config\Factory::class && $binding->/** @scrutinizer ignore-call */ getParametersCount() === 0

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
247 16
                ? ($binding->factory)()
248 462
                : $this->invoker->invoke($binding->factory, $arguments);
249 8
        } catch (NotCallableException $e) {
250
            throw new ContainerException(
251
                $this->tracer->combineTraceMessage(\sprintf('Invalid binding for `%s`.', $ctx->alias)),
252
                $e->getCode(),
253
                $e,
254
            );
255
        }
256
257 472
        return \is_object($instance) ? $this->validateNewInstance($instance, $ctx, $arguments) : $instance;
258
    }
259
260 704
    private function resolveWeakReference(
261
        Config\WeakReference $binding,
262
        string $alias,
263
        Stringable|string|null $context,
264
        array $arguments,
265
    ): ?object {
266 704
        $avoidCache = $arguments !== [];
267
268 704
        if (($avoidCache || $binding->reference->get() === null) && \class_exists($alias)) {
269
            try {
270 3
                $this->tracer->push(false, alias: $alias, source: WeakReference::class, context: $context);
0 ignored issues
show
Bug introduced by
$context of type Stringable|null|string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
                $this->tracer->push(false, alias: $alias, source: WeakReference::class, /** @scrutinizer ignore-type */ context: $context);
Loading history...
Bug introduced by
$alias of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

270
                $this->tracer->push(false, /** @scrutinizer ignore-type */ alias: $alias, source: WeakReference::class, context: $context);
Loading history...
271
272 3
                $object = $this->createInstance(
273 3
                    new Ctx(alias: $alias, class: $alias, context: $context),
274 3
                    $arguments,
275 3
                );
276 3
                if ($avoidCache) {
277 1
                    return $object;
278
                }
279 2
                $binding->reference = WeakReference::create($object);
280
            } catch (\Throwable) {
281
                throw new ContainerException(
282
                    $this->tracer->combineTraceMessage(
283
                        \sprintf(
284
                            'Can\'t resolve `%s`: can\'t instantiate `%s` from WeakReference binding.',
285
                            $this->tracer->getRootAlias(),
286
                            $alias,
287
                        )
288
                    )
289
                );
290
            } finally {
291 3
                $this->tracer->pop();
292
            }
293
        }
294
295 704
        return $binding->reference->get();
296
    }
297
298 925
    private function resolveWithoutBinding(
299
        string $alias,
300
        array $parameters = [],
301
        Stringable|string|null $context = null
302
    ): mixed {
303 925
        $parent = $this->scope->getParentFactory();
304
305 925
        if ($parent !== null) {
306
            try {
307 24
                $this->tracer->push(false, ...[
308 24
                    'current scope' => $this->scope->getScopeName(),
309 24
                    'jump to parent scope' => $this->scope->getParentScope()->getScopeName(),
310 24
                ]);
311
                /** @psalm-suppress TooManyArguments */
312 24
                return $parent->make($alias, $parameters, $context);
0 ignored issues
show
Unused Code introduced by
The call to Spiral\Core\FactoryInterface::make() has too many arguments starting with $context. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

312
                return $parent->/** @scrutinizer ignore-call */ make($alias, $parameters, $context);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
313 3
            } catch (BadScopeException $e) {
314 3
                if ($this->scope->getScopeName() !== $e->getScope()) {
315 3
                    throw $e;
316
                }
317 1
            } catch (ContainerExceptionInterface $e) {
318 1
                $className = match (true) {
319 1
                    $e instanceof NotFoundException => NotFoundException::class,
320 1
                    default => ContainerException::class,
321 1
                };
322 1
                throw new $className($this->tracer->combineTraceMessage(\sprintf(
323 1
                    'Can\'t resolve `%s`.',
324 1
                    $alias,
325 1
                )), previous: $e);
326
            } finally {
327 24
                $this->tracer->pop(false);
328
            }
329
        }
330
331 920
        $this->tracer->push(false, action: 'autowire', alias: $alias, context: $context);
0 ignored issues
show
Bug introduced by
'autowire' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

331
        $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'autowire', alias: $alias, context: $context);
Loading history...
Bug introduced by
$context of type Stringable|null|string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

331
        $this->tracer->push(false, action: 'autowire', alias: $alias, /** @scrutinizer ignore-type */ context: $context);
Loading history...
332
        try {
333
            //No direct instructions how to construct class, make is automatically
334 920
            return $this->autowire(
335 920
                new Ctx(alias: $alias, class: $alias, context: $context),
336 920
                $parameters,
337 920
            );
338
        } finally {
339 920
            $this->tracer->pop(false);
340
        }
341
    }
342
343
    /**
344
     * Automatically create class.
345
     * Object will be cached if the $arguments list is empty.
346
     *
347
     * @psalm-assert class-string $class
348
     *
349
     * @throws AutowireException
350
     * @throws \Throwable
351
     */
352 923
    private function autowire(Ctx $ctx, array $arguments): object
353
    {
354 923
        if (!(\class_exists($ctx->class) || (
355 922
            \interface_exists($ctx->class)
356 922
                &&
357 922
                (isset($this->state->injectors[$ctx->class]) || $this->binder->hasInjector($ctx->class))
358
        ))
359
        ) {
360 646
            throw new NotFoundException($this->tracer->combineTraceMessage(\sprintf(
361 646
                'Can\'t resolve `%s`: undefined class or binding `%s`.',
362 646
                $this->tracer->getRootAlias(),
363 646
                $ctx->class,
364 646
            )));
365
        }
366
367
        // automatically create instance
368 910
        $instance = $this->createInstance($ctx, $arguments);
369
370
        // apply registration functions to created instance
371 885
        return $arguments === []
372 873
            ? $this->registerInstance($ctx, $instance)
373 885
            : $instance;
374
    }
375
376
    /**
377
     * @throws BadScopeException
378
     * @throws \Throwable
379
     */
380 472
    private function validateNewInstance(
381
        object $instance,
382
        Ctx $ctx,
383
        array $arguments,
384
    ): object {
385
        // Check scope name
386 472
        $ctx->reflection = new \ReflectionClass($instance);
387 472
        $scopeName = ($ctx->reflection->getAttributes(Attribute\Scope::class)[0] ?? null)?->newInstance()->name;
388 472
        if ($scopeName !== null && $scopeName !== $this->scope->getScopeName()) {
389
            throw new BadScopeException($scopeName, $instance::class);
390
        }
391
392 472
        return $arguments === []
393 466
            ? $this->registerInstance($ctx, $instance)
394 472
            : $instance;
395
    }
396
397
    /**
398
     * Create instance of desired class.
399
     *
400
     * @template TObject of object
401
     *
402
     * @param Ctx<TObject> $ctx
403
     * @param array $arguments Constructor arguments.
404
     *
405
     * @return TObject
0 ignored issues
show
Bug introduced by
The type Spiral\Core\Internal\TObject was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
406
     *
407
     * @throws ContainerException
408
     * @throws \Throwable
409
     */
410 914
    private function createInstance(
411
        Ctx $ctx,
412
        array $arguments,
413
    ): object {
414 914
        $class = $ctx->class;
415
        try {
416 914
            $ctx->reflection = $reflection = new \ReflectionClass($class);
417
        } catch (\ReflectionException $e) {
418
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
419
        }
420
421
        // Check scope name
422 914
        $scope = ($reflection->getAttributes(Attribute\Scope::class)[0] ?? null)?->newInstance()->name;
423 914
        if ($scope !== null && $scope !== $this->scope->getScopeName()) {
424 4
            throw new BadScopeException($scope, $class);
425
        }
426
427
        // We have to construct class using external injector when we know the exact context
428 913
        if ($arguments === [] && $this->binder->hasInjector($class)) {
429 454
            return $this->resolveInjector($this->state->bindings[$ctx->class], $ctx, $arguments);
430
        }
431
432 905
        if (!$reflection->isInstantiable()) {
433 8
            $itIs = match (true) {
434 8
                $reflection->isEnum() => 'Enum',
0 ignored issues
show
Bug introduced by
The method isEnum() does not exist on ReflectionClass. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

434
                $reflection->/** @scrutinizer ignore-call */ 
435
                             isEnum() => 'Enum',

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
435 8
                $reflection->isAbstract() => 'Abstract class',
436 8
                default => 'Class',
437 8
            };
438 8
            throw new ContainerException(
439 8
                $this->tracer->combineTraceMessage(\sprintf('%s `%s` can not be constructed.', $itIs, $class)),
440 8
            );
441
        }
442
443 904
        $constructor = $reflection->getConstructor();
444
445 904
        if ($constructor !== null) {
446
            try {
447 815
                $this->tracer->push(false, action: 'resolve arguments', signature: $constructor);
0 ignored issues
show
Bug introduced by
'resolve arguments' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

447
                $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'resolve arguments', signature: $constructor);
Loading history...
Bug introduced by
$constructor of type ReflectionMethod is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

447
                $this->tracer->push(false, action: 'resolve arguments', /** @scrutinizer ignore-type */ signature: $constructor);
Loading history...
448 815
                $this->tracer->push(true);
449 815
                $args = $this->resolver->resolveArguments($constructor, $arguments);
450 21
            } catch (ValidationException $e) {
451 6
                throw new ContainerException(
452 6
                    $this->tracer->combineTraceMessage(
453 6
                        \sprintf(
454 6
                            'Can\'t resolve `%s`. %s',
455 6
                            $this->tracer->getRootAlias(),
456 6
                            $e->getMessage()
457 6
                        )
458 6
                    ),
459 6
                );
460
            } finally {
461 815
                $this->tracer->pop(true);
462 815
                $this->tracer->pop(false);
463
            }
464
            try {
465
                // Using constructor with resolved arguments
466 795
                $this->tracer->push(false, call: "$class::__construct", arguments: $args);
467 795
                $this->tracer->push(true);
468 795
                $instance = new $class(...$args);
469 1
            } catch (\TypeError $e) {
470
                throw new WrongTypeException($constructor, $e);
471
            } finally {
472 795
                $this->tracer->pop(true);
473 795
                $this->tracer->pop(false);
474
            }
475
        } else {
476
            // No constructor specified
477 719
            $instance = $reflection->newInstance();
478
        }
479
480 883
        return $instance;
481
    }
482
483
    /**
484
     * Register instance in container, might perform methods like auto-singletons, log populations, etc.
485
     */
486 883
    private function registerInstance(Ctx $ctx, object $instance): object
487
    {
488 883
        $ctx->reflection ??= new \ReflectionClass($instance);
489
490 883
        $instance = $this->runInflector($instance);
491
492
        //Declarative singletons
493 883
        if ($this->isSingleton($ctx)) {
494 542
            $this->state->singletons[$ctx->alias] = $instance;
495
        }
496
497
        // Register finalizer
498 883
        $finalizer = $this->getFinalizer($ctx, $instance);
499 883
        if ($finalizer !== null) {
500 4
            $this->state->finalizers[] = $finalizer;
501
        }
502
503 883
        return $instance;
504
    }
505
506
    /**
507
     * Check the class was configured as a singleton.
508
     */
509 883
    private function isSingleton(Ctx $ctx): bool
510
    {
511 883
        if ($ctx->singleton === true) {
512 469
            return true;
513
        }
514
515
        /** @psalm-suppress RedundantCondition https://github.com/vimeo/psalm/issues/9489 */
516 874
        if ($ctx->reflection->implementsInterface(SingletonInterface::class)) {
0 ignored issues
show
Bug introduced by
The method implementsInterface() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

516
        if ($ctx->reflection->/** @scrutinizer ignore-call */ implementsInterface(SingletonInterface::class)) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
517
            return true;
518
        }
519
520 874
        return $ctx->reflection->getAttributes(Attribute\Singleton::class) !== [];
521
    }
522
523 883
    private function getFinalizer(Ctx $ctx, object $instance): ?callable
524
    {
525
        /**
526
         * @psalm-suppress UnnecessaryVarAnnotation
527
         * @var Attribute\Finalize|null $attribute
528
         */
529 883
        $attribute = ($ctx->reflection->getAttributes(Attribute\Finalize::class)[0] ?? null)?->newInstance();
530 883
        if ($attribute === null) {
531 882
            return null;
532
        }
533
534 4
        return [$instance, $attribute->method];
535
    }
536
537
    /**
538
     * Find and run inflector
539
     */
540 883
    private function runInflector(object $instance): object
541
    {
542 883
        $scope = $this->scope;
543
544 883
        while ($scope !== null) {
545 883
            foreach ($this->state->inflectors as $class => $inflectors) {
546 5
                if ($instance instanceof $class) {
547 5
                    foreach ($inflectors as $inflector) {
548 5
                        $instance = $inflector->getParametersCount() > 1
549 1
                            ? $this->invoker->invoke($inflector->inflector, [$instance])
550 4
                            : ($inflector->inflector)($instance);
551
                    }
552
                }
553
            }
554
555 883
            $scope = $scope->getParentScope();
556
        }
557
558 883
        return $instance;
559
    }
560
561 5
    private function validateArguments(ContextFunction $reflection, array $arguments = []): bool
562
    {
563
        try {
564 5
            $this->resolver->validateArguments($reflection, $arguments);
565
        } catch (\Throwable) {
566
            return false;
567
        }
568
569 5
        return true;
570
    }
571
}
572