Test Failed
Pull Request — master (#1221)
by Aleksei
18:39
created

Actor::resolveInjector()   C

Complexity

Conditions 11
Paths 171

Size

Total Lines 58
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 39
dl 0
loc 58
rs 6.725
c 1
b 0
f 0
cc 11
nc 171
nop 4

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Core\Internal;
6
7
use Psr\Container\ContainerInterface;
8
use ReflectionFunctionAbstract as ContextFunction;
9
use Spiral\Core\BinderInterface;
10
use Spiral\Core\Config\Alias;
11
use Spiral\Core\Config\Binding;
12
use Spiral\Core\Attribute;
13
use Spiral\Core\Container\InjectorInterface;
14
use Spiral\Core\Container\SingletonInterface;
15
use Spiral\Core\Exception\Container\AutowireException;
16
use Spiral\Core\Exception\Container\ContainerException;
17
use Spiral\Core\Exception\Container\InjectionException;
18
use Spiral\Core\Exception\Container\NotCallableException;
19
use Spiral\Core\Exception\Container\NotFoundException;
20
use Spiral\Core\Exception\Container\RecursiveProxyException;
21
use Spiral\Core\Exception\Container\TracedContainerException;
22
use Spiral\Core\Exception\Resolver\ValidationException;
23
use Spiral\Core\Exception\Resolver\WrongTypeException;
24
use Spiral\Core\Exception\Scope\BadScopeException;
25
use Spiral\Core\FactoryInterface;
26
use Spiral\Core\Internal\Common\DestructorTrait;
27
use Spiral\Core\Internal\Common\Registry;
28
use Spiral\Core\Internal\Factory\Ctx;
29
use Spiral\Core\Internal\Proxy\RetryContext;
30
use Spiral\Core\InvokerInterface;
31
use Spiral\Core\Options;
32
use Spiral\Core\ResolverInterface;
33
use Spiral\Core\Config;
34
35
/**
36
 * @internal
37
 */
38
final class Actor
39
{
40
    use DestructorTrait;
41
42
    private State $state;
43
    private BinderInterface $binder;
44
    private InvokerInterface $invoker;
45
    private ContainerInterface $container;
46
    private ResolverInterface $resolver;
47
    private FactoryInterface $factory;
48
    private Scope $scope;
49
    private Options $options;
50
51
    public function __construct(Registry $constructor)
52
    {
53
        $constructor->set('hub', $this);
54
55
        $this->state = $constructor->get('state', State::class);
56
        $this->binder = $constructor->get('binder', BinderInterface::class);
57
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
58
        $this->container = $constructor->get('container', ContainerInterface::class);
59
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
60
        $this->factory = $constructor->get('factory', FactoryInterface::class);
61
        $this->scope = $constructor->get('scope', Scope::class);
62
        $this->options = $constructor->getOptions();
63
    }
64
65
    /**
66
     * Get class name of the resolving object.
67
     * With it, you can quickly get cached singleton or detect that there are injector or binding.
68
     * The method does not detect that the class is instantiable.
69
     *
70
     * @param non-empty-string $alias
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
71
     *
72
     * @param self|null $actor Will be set to the hub where the result was found.
73
     *
74
     * @return class-string|null Returns {@see null} if exactly one returning class cannot be resolved.
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string|null at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string|null.
Loading history...
75
     * @psalm-suppress all
76
     */
77
    public function resolveType(
78
        string $alias,
79
        ?Binding &$binding = null,
80
        ?object &$singleton = null,
81
        ?object &$injector = null,
82
        ?self &$actor = null,
83
        bool $followAlias = true,
84
    ): ?string {
85
        // Aliases to prevent circular dependencies
86
        $as = [];
87
        $actor = $this;
88
        do {
89
            $bindings = &$actor->state->bindings;
90
            $singletons = &$actor->state->singletons;
91
            $injectors = &$actor->state->injectors;
92
            if (\array_key_exists($alias, $singletons)) {
93
                $singleton = $singletons[$alias];
94
                $binding = $bindings[$alias] ?? null;
95
                $injector = $injectors[$alias] ?? null;
96
                return \is_object($singleton::class) ? $singleton::class : null;
97
            }
98
99
            if (\array_key_exists($alias, $bindings)) {
100
                $binding = $bindings[$alias];
101
                if ($followAlias && $binding::class === Alias::class) {
102
                    if ($binding->alias === $alias) {
103
                        break;
104
                    }
105
106
                    $alias = $binding->alias;
107
                    \array_key_exists($alias, $as) and throw new ContainerException(
108
                        \sprintf('Circular dependency detected for alias `%s`.', $alias),
109
                    );
110
                    $as[$alias] = true;
111
                    continue;
112
                }
113
114
                return $binding->getReturnClass();
115
            }
116
117
            if (\array_key_exists($alias, $injectors)) {
118
                $injector = $injectors[$alias];
119
                $binding = $bindings[$alias] ?? null;
120
                return $alias;
121
            }
122
123
            // Go to parent scope
124
            $parent = $actor->scope->getParentActor();
125
            if ($parent === null) {
126
                break;
127
            }
128
129
            $actor = $parent;
130
        } while (true);
131
132
        return \class_exists($alias) ? $alias : null;
133
    }
134
135
    public function resolveBinding(
136
        object $binding,
137
        string $alias,
138
        \Stringable|string|null $context,
139
        array $arguments,
140
        Tracer $tracer,
141
    ): mixed {
142
        return match ($binding::class) {
143
            Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $arguments, $tracer),
144
            Config\Proxy::class,
145
            Config\DeprecationProxy::class => $this->resolveProxy($binding, $alias, $context),
146
            Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $arguments, $tracer),
147
            Config\DeferredFactory::class,
148
            Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $arguments, $tracer),
149
            Config\Shared::class => $this->resolveShared($binding, $alias, $context, $arguments, $tracer),
150
            Config\Injectable::class => $this->resolveInjector(
151
                $binding,
152
                new Ctx(alias: $alias, class: $alias, context: $context),
153
                $arguments,
154
                $tracer,
155
            ),
156
            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...
157
            Config\WeakReference::class => $this
158
                ->resolveWeakReference($binding, $alias, $context, $arguments, $tracer),
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\Actor::resolveWeakReference(). ( Ignorable by Annotation )

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

158
                ->resolveWeakReference(/** @scrutinizer ignore-type */ $binding, $alias, $context, $arguments, $tracer),
Loading history...
159
            default => $binding,
160
        };
161
    }
162
163
    /**
164
     * Automatically create class.
165
     * Object will be cached if the $arguments list is empty.
166
     *
167
     * @psalm-assert class-string $class
168
     *
169
     * @throws AutowireException
170
     * @throws \Throwable
171
     */
172
    public function autowire(Ctx $ctx, array $arguments, ?Actor $fallbackActor, Tracer $tracer): object
173
    {
174
        \class_exists($ctx->class)
175
        or (\interface_exists($ctx->class)
176
            && (isset($this->state->injectors[$ctx->class]) || $this->binder->hasInjector($ctx->class)))
177
        or throw NotFoundException::createWithTrace(
178
            $ctx->alias === $ctx->class
179
                ? "Can't autowire `$ctx->class`: class or injector not found."
180
                : "Can't resolve `$ctx->alias`: class or injector `$ctx->class` not found.",
181
            $tracer->getTraces(),
182
        );
183
184
        // automatically create instance
185
        return $this->createInstance($ctx, $arguments, $fallbackActor, $tracer);
186
    }
187
188
    /**
189
     * @psalm-suppress UnusedParam
190
     * todo wat should we do with $arguments?
191
     */
192
    private function resolveInjector(Config\Injectable $binding, Ctx $ctx, array $arguments, Tracer $tracer)
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

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

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...
193
    {
194
        $context = $ctx->context;
195
        try {
196
            $reflection = $ctx->reflection ??= new \ReflectionClass($ctx->class);
197
        } catch (\ReflectionException $e) {
198
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
199
        }
200
201
        $injector = $binding->injector;
202
203
        try {
204
            $injectorInstance = \is_object($injector) ? $injector : $this->container->get($injector);
205
206
            if (!$injectorInstance instanceof InjectorInterface) {
207
                throw new InjectionException(
208
                    \sprintf(
209
                        "Class '%s' must be an instance of InjectorInterface for '%s'.",
210
                        $injectorInstance::class,
211
                        $reflection->getName(),
212
                    ),
213
                );
214
            }
215
216
            /** @var array<class-string<InjectorInterface>, \ReflectionMethod|false> $cache reflection for extended injectors */
217
            static $cache = [];
218
            $extended = $cache[$injectorInstance::class] ??= (
219
            static fn(\ReflectionType $type): bool =>
220
                $type::class === \ReflectionUnionType::class || (string) $type === 'mixed'
221
            )(
222
                ($refMethod = new \ReflectionMethod($injectorInstance, 'createInjection'))
223
                    ->getParameters()[1]->getType()
224
            ) ? $refMethod : false;
225
226
            $asIs = $extended && (\is_string($context) || $this->validateArguments($extended, [$reflection, $context]));
227
            $instance = $injectorInstance->createInjection($reflection, match (true) {
228
                $asIs => $context,
229
                $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 Nette\Iterators\CachingIterator or RectorPrefix202503\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

229
                $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

229
                $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...
230
                default => (string) $context,
231
            });
232
233
            if (!$reflection->isInstance($instance)) {
234
                throw new InjectionException(
235
                    \sprintf(
236
                        "Invalid injection response for '%s'.",
237
                        $reflection->getName(),
238
                    ),
239
                );
240
            }
241
242
            return $instance;
243
        } catch (TracedContainerException $e) {
244
            throw isset($injectorInstance) ? $e : $e::extendTracedException(\sprintf(
245
                'Can\'t resolve `%s`.',
246
                $tracer->getRootAlias(),
247
            ), $tracer->getTraces(), $e);
248
        } finally {
249
            $this->state->bindings[$ctx->class] ??= $binding;
250
        }
251
    }
252
253
    private function resolveAlias(
254
        Config\Alias $binding,
255
        string $alias,
256
        \Stringable|string|null $context,
257
        array $arguments,
258
        Tracer $tracer,
259
    ): mixed {
260
        if ($binding->alias === $alias) {
261
            $instance = $this->autowire(
262
                new Ctx(alias: $alias, class: $binding->alias, context: $context, singleton: $binding->singleton && $arguments === []),
263
                $arguments,
264
                $this,
265
                $tracer,
266
            );
267
        } else {
268
            try {
269
                //Binding is pointing to something else
270
                $instance = $this->factory->make($binding->alias, $arguments, $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

270
                /** @scrutinizer ignore-call */ 
271
                $instance = $this->factory->make($binding->alias, $arguments, $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...
271
            } catch (TracedContainerException $e) {
272
                throw $e::extendTracedException(
273
                    $alias === $tracer->getRootAlias()
274
                        ? "Can't resolve `{$alias}`."
275
                        : "Can't resolve `$alias` with alias `{$binding->alias}`.",
276
                    $tracer->getTraces(),
277
                    $e,
278
                );
279
            }
280
281
            $binding->singleton and $arguments === [] and $this->state->singletons[$alias] = $instance;
282
        }
283
284
285
        return $instance;
286
    }
287
288
    private function resolveProxy(Config\Proxy $binding, string $alias, \Stringable|string|null $context): mixed
289
    {
290
        if ($context instanceof RetryContext) {
291
            return $binding->fallbackFactory === null
292
                ? throw new RecursiveProxyException(
293
                    $alias,
294
                    $this->scope->getScopeName(),
295
                )
296
                : ($binding->fallbackFactory)($this->container, $context->context);
297
        }
298
299
        $result = Proxy::create(new \ReflectionClass($binding->getReturnClass()), $context, new Attribute\Proxy());
300
301
        if ($binding->singleton) {
302
            $this->state->singletons[$alias] = $result;
303
        }
304
305
        return $result;
306
    }
307
308
    private function resolveShared(
309
        Config\Shared $binding,
310
        string $alias,
311
        \Stringable|string|null $context,
312
        array $arguments,
313
        Tracer $tracer,
314
    ): object {
315
        if ($arguments !== []) {
316
            // Avoid singleton cache
317
            return $this->createInstance(
318
                new Ctx(alias: $alias, class: $binding->value::class, context: $context, singleton: false),
319
                $arguments,
320
                $this,
321
                $tracer,
322
            );
323
        }
324
325
        if ($binding->singleton) {
326
            $this->state->singletons[$alias] = $binding->value;
327
        }
328
329
        return $binding->value;
330
    }
331
332
    private function resolveAutowire(
333
        Config\Autowire $binding,
334
        string $alias,
335
        \Stringable|string|null $context,
336
        array $arguments,
337
        Tracer $tracer,
338
    ): mixed {
339
        $target = $binding->autowire->alias;
340
        $ctx = new Ctx(alias: $alias, class: $target, context: $context, singleton: $binding->singleton && $arguments === [] ?: null);
341
342
        if ($alias === $target) {
343
            $instance = $this->autowire($ctx, \array_merge($binding->autowire->parameters, $arguments), $this, $tracer);
344
        } else {
345
            $instance = $binding->autowire->resolve($this->factory, $arguments);
346
            $this->validateConstraint($instance, $ctx);
347
        }
348
349
        return $this->registerInstance($ctx, $instance);
350
    }
351
352
    private function resolveFactory(
353
        Config\Factory|Config\DeferredFactory $binding,
354
        string $alias,
355
        \Stringable|string|null $context,
356
        array $arguments,
357
        Tracer $tracer,
358
    ): mixed {
359
        $ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton && $arguments === [] ?: null);
360
        try {
361
            $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

361
            $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...
362
                ? ($binding->factory)()
363
                : $this->invoker->invoke($binding->factory, $arguments);
364
        } catch (NotCallableException $e) {
365
            throw TracedContainerException::createWithTrace(
366
                \sprintf('Invalid callable binding for `%s`.', $ctx->alias),
367
                $tracer->getTraces(),
368
                $e,
369
            );
370
        } catch (TracedContainerException $e) {
371
            throw $e::extendTracedException(
372
                \sprintf("Can't resolve `%s`: factory invocation failed.", $tracer->getRootAlias()),
373
                $tracer->getTraces(),
374
                $e,
375
            );
376
        }
377
378
        if (\is_object($instance)) {
379
            $this->validateConstraint($instance, $ctx);
380
            return $this->registerInstance($ctx, $instance);
381
        }
382
383
        return $instance;
384
    }
385
386
    private function resolveWeakReference(
387
        Config\WeakReference $binding,
388
        string $alias,
389
        \Stringable|string|null $context,
390
        array $arguments,
391
        Tracer $tracer,
392
    ): ?object {
393
        $avoidCache = $arguments !== [];
394
395
        if (($avoidCache || $binding->reference->get() === null) && \class_exists($alias)) {
396
            try {
397
                $tracer->push(false, alias: $alias, source: \WeakReference::class, context: $context);
0 ignored issues
show
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

397
                $tracer->push(false, /** @scrutinizer ignore-type */ alias: $alias, source: \WeakReference::class, 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

397
                $tracer->push(false, alias: $alias, source: \WeakReference::class, /** @scrutinizer ignore-type */ context: $context);
Loading history...
398
399
                $object = $this->createInstance(
400
                    new Ctx(alias: $alias, class: $alias, context: $context, singleton: false),
401
                    $arguments,
402
                    $this,
403
                    $tracer,
404
                );
405
                if ($avoidCache) {
406
                    return $object;
407
                }
408
                $binding->reference = \WeakReference::create($object);
409
            } catch (\Throwable) {
410
                throw ContainerException::createWithTrace(\sprintf(
0 ignored issues
show
Bug introduced by
The method createWithTrace() does not exist on Spiral\Core\Exception\Container\ContainerException. It seems like you code against a sub-type of Spiral\Core\Exception\Container\ContainerException such as Spiral\Core\Exception\Co...racedContainerException. ( Ignorable by Annotation )

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

410
                throw ContainerException::/** @scrutinizer ignore-call */ createWithTrace(\sprintf(
Loading history...
411
                    'Can\'t resolve `%s`: can\'t instantiate `%s` from WeakReference binding.',
412
                    $tracer->getRootAlias(),
413
                    $alias,
414
                ), $tracer->getTraces());
415
            } finally {
416
                $tracer->pop();
417
            }
418
        }
419
420
        return $binding->reference->get();
421
    }
422
423
    /**
424
     * @throws BadScopeException
425
     * @throws \Throwable
426
     */
427
    private function validateConstraint(
428
        object $instance,
429
        Ctx $ctx,
430
    ): void {
431
        if ($this->options->checkScope) {
432
            // Check scope name
433
            $ctx->reflection ??= new \ReflectionClass($instance);
434
            $scopeName = ($ctx->reflection->getAttributes(Attribute\Scope::class)[0] ?? null)?->newInstance()->name;
435
            if ($scopeName !== null) {
436
                $scope = $this->scope;
437
                while ($scope->getScopeName() !== $scopeName) {
438
                    $scope = $scope->getParentScope() ?? throw new BadScopeException($scopeName, $instance::class);
439
                }
440
            }
441
        }
442
    }
443
444
    /**
445
     * Create instance of desired class.
446
     *
447
     * @template TObject of object
448
     *
449
     * @param Ctx<TObject> $ctx
450
     * @param array $arguments Constructor arguments.
451
     *
452
     * @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...
453
     *
454
     * @throws ContainerException
455
     * @throws \Throwable
456
     */
457
    private function createInstance(
458
        Ctx $ctx,
459
        array $arguments,
460
        ?Actor $fallbackActor,
461
        Tracer $tracer,
462
    ): object {
463
        $class = $ctx->class;
464
        try {
465
            $ctx->reflection = $reflection = new \ReflectionClass($class);
466
        } catch (\ReflectionException $e) {
467
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
468
        }
469
470
        // Check Scope attribute
471
        $actor = $fallbackActor ?? $this;
472
        if ($this->options->checkScope) { # todo
473
            $ar = ($reflection->getAttributes(Attribute\Scope::class)[0] ?? null);
474
            if ($ar !== null) {
475
                /** @var Attribute\Scope $attr */
476
                $attr = $ar->newInstance();
477
                $scope = $this->scope;
478
                $actor = $this;
479
                // Go through all parent scopes
480
                $needed = $actor;
481
                while ($attr->name !== $scope->getScopeName()) {
482
                    $needed = $scope->getParentActor();
483
                    if ($needed === null) {
484
                        throw new BadScopeException($attr->name, $class);
485
                    }
486
487
                    $scope = $scope->getParentScope();
488
                }
489
490
                // Scope found
491
                $actor = $needed;
492
            }
493
        } # todo
494
495
        // We have to construct class using external injector when we know the exact context
496
        if ($arguments === [] && $this->binder->hasInjector($class)) {
497
            return $actor->resolveInjector($actor->state->bindings[$ctx->class], $ctx, $arguments, $tracer);
498
        }
499
500
        if (!$reflection->isInstantiable()) {
501
            $itIs = match (true) {
502
                $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

502
                $reflection->/** @scrutinizer ignore-call */ 
503
                             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...
503
                $reflection->isAbstract() => 'Abstract class',
504
                default => 'Class',
505
            };
506
            throw TracedContainerException::createWithTrace(
507
                \sprintf('%s `%s` can not be constructed.', $itIs, $class),
508
                $tracer->getTraces(),
509
            );
510
        }
511
512
        $constructor = $reflection->getConstructor();
513
514
        if ($constructor !== null) {
515
            try {
516
                $newScope = $this !== $actor;
517
                $debug = [
518
                    'action' => 'resolve arguments',
519
                    'alias' => $ctx->class,
520
                    'signature' => $constructor,
521
                ];
522
                $newScope and $debug += [
523
                    'jump to scope' => $actor->scope->getScopeName(),
524
                    'from scope' => $this->scope->getScopeName(),
525
                ];
526
                $tracer->push($newScope, ...$debug);
527
                $tracer->push(true);
528
                $args = $actor->resolver->resolveArguments($constructor, $arguments, $actor->options->validateArguments);
529
            } catch (ValidationException $e) {
530
                throw TracedContainerException::createWithTrace(\sprintf(
531
                    'Can\'t resolve `%s`. %s',
532
                    $tracer->getRootAlias(),
533
                    $e->getMessage(),
534
                ), $tracer->getTraces());
535
            } catch (TracedContainerException $e) {
536
                throw $e::extendTracedException(\sprintf(
537
                    'Can\'t resolve `%s`.',
538
                    $tracer->getRootAlias(),
539
                ), $tracer->getTraces(), $e);
540
            } finally {
541
                $tracer->pop($newScope);
542
                $tracer->pop(false);
543
            }
544
            try {
545
                // Using constructor with resolved arguments
546
                $tracer->push(false, call: "$class::__construct", arguments: $args);
0 ignored issues
show
Bug introduced by
$class.'::__construct' 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

546
                $tracer->push(false, /** @scrutinizer ignore-type */ call: "$class::__construct", arguments: $args);
Loading history...
547
                $tracer->push(true);
548
                $instance = new $class(...$args);
549
            } catch (\TypeError $e) {
550
                throw new WrongTypeException($constructor, $e);
551
            } catch (TracedContainerException $e) {
552
                throw $e::extendTracedException(\sprintf(
553
                    'Can\'t resolve `%s`: failed constructing `%s`.',
554
                    $tracer->getRootAlias(),
555
                    $class,
556
                ), $tracer->getTraces(), $e);
557
            } finally {
558
                $tracer->pop(true);
559
                $tracer->pop(false);
560
            }
561
        } else {
562
            // No constructor specified
563
            $instance = $reflection->newInstance();
564
        }
565
566
        return $actor->registerInstance($ctx, $instance);
567
    }
568
569
    /**
570
     * Register instance in container, might perform methods like auto-singletons, log populations, etc.
571
     */
572
    private function registerInstance(Ctx $ctx, object $instance): object
573
    {
574
        $ctx->reflection ??= new \ReflectionClass($instance);
575
576
        $instance = $this->runInflector($instance);
577
578
        // Declarative singletons
579
        $this->isSingleton($ctx) and $this->state->singletons[$ctx->alias] = $instance;
580
581
        // Register finalizer
582
        $finalizer = $this->getFinalizer($ctx, $instance);
583
        $finalizer === null or $this->state->finalizers[] = $finalizer;
584
585
        return $instance;
586
    }
587
588
    /**
589
     * Check the class was configured as a singleton.
590
     */
591
    private function isSingleton(Ctx $ctx): bool
592
    {
593
        if (is_bool($ctx->singleton)) {
594
            return $ctx->singleton;
595
        }
596
597
        /** @psalm-suppress RedundantCondition https://github.com/vimeo/psalm/issues/9489 */
598
        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

598
        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...
599
            return true;
600
        }
601
602
        return $ctx->reflection->getAttributes(Attribute\Singleton::class) !== [];
603
    }
604
605
    private function getFinalizer(Ctx $ctx, object $instance): ?callable
606
    {
607
        /**
608
         * @psalm-suppress UnnecessaryVarAnnotation
609
         * @var Attribute\Finalize|null $attribute
610
         */
611
        $attribute = ($ctx->reflection->getAttributes(Attribute\Finalize::class)[0] ?? null)?->newInstance();
612
        if ($attribute === null) {
613
            return null;
614
        }
615
616
        return [$instance, $attribute->method];
617
    }
618
619
    /**
620
     * Find and run inflector
621
     */
622
    private function runInflector(object $instance): object
623
    {
624
        $scope = $this->scope;
625
626
        while ($scope !== null) {
627
            foreach ($this->state->inflectors as $class => $inflectors) {
628
                if ($instance instanceof $class) {
629
                    foreach ($inflectors as $inflector) {
630
                        $instance = $inflector->getParametersCount() > 1
631
                            ? $this->invoker->invoke($inflector->inflector, [$instance])
632
                            : ($inflector->inflector)($instance);
633
                    }
634
                }
635
            }
636
637
            $scope = $scope->getParentScope();
638
        }
639
640
        return $instance;
641
    }
642
643
    private function validateArguments(ContextFunction $reflection, array $arguments = []): bool
644
    {
645
        try {
646
            $this->resolver->validateArguments($reflection, $arguments);
647
        } catch (\Throwable) {
648
            return false;
649
        }
650
651
        return true;
652
    }
653
}
654