Actor::createInstance()   F
last analyzed

Complexity

Conditions 14
Paths 551

Size

Total Lines 106
Code Lines 70

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 68
CRAP Score 14.0045

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 70
dl 0
loc 106
ccs 68
cts 70
cp 0.9714
rs 2.8334
c 2
b 0
f 0
cc 14
nc 551
nop 4
crap 14.0045

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\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use ReflectionFunctionAbstract as ContextFunction;
10
use Spiral\Core\BinderInterface;
11
use Spiral\Core\Config\Alias;
12
use Spiral\Core\Config\Binding;
13
use Spiral\Core\Attribute;
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\Container\RecursiveProxyException;
22
use Spiral\Core\Exception\Container\TracedContainerException;
23
use Spiral\Core\Exception\Resolver\ValidationException;
24
use Spiral\Core\Exception\Resolver\WrongTypeException;
25
use Spiral\Core\Exception\Scope\BadScopeException;
26
use Spiral\Core\FactoryInterface;
27
use Spiral\Core\Internal\Common\DestructorTrait;
28
use Spiral\Core\Internal\Common\Registry;
29
use Spiral\Core\Internal\Factory\Ctx;
30
use Spiral\Core\Internal\Proxy\RetryContext;
31
use Spiral\Core\InvokerInterface;
32
use Spiral\Core\Options;
33
use Spiral\Core\ResolverInterface;
34
use Spiral\Core\Config;
35
36
/**
37
 * @internal
38
 */
39
final class Actor
40
{
41
    use DestructorTrait;
42
43
    private State $state;
44
    private BinderInterface $binder;
45
    private InvokerInterface $invoker;
46
    private ContainerInterface $container;
47
    private ResolverInterface $resolver;
48
    private FactoryInterface $factory;
49
    private Scope $scope;
50
    private Options $options;
51
52 1455
    public function __construct(Registry $constructor)
53
    {
54 1455
        $constructor->set('hub', $this);
55
56 1455
        $this->state = $constructor->get('state', State::class);
57 1455
        $this->binder = $constructor->get('binder', BinderInterface::class);
58 1455
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
59 1455
        $this->container = $constructor->get('container', ContainerInterface::class);
60 1455
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
61 1455
        $this->factory = $constructor->get('factory', FactoryInterface::class);
62 1455
        $this->scope = $constructor->get('scope', Scope::class);
63 1455
        $this->options = $constructor->getOptions();
64
    }
65
66 1120
    public function disableBinding(string $alias): void
67
    {
68 1120
        unset($this->state->bindings[$alias]);
69
    }
70
71 1120
    public function enableBinding(string $alias, Binding $binding): void
72
    {
73 1120
        $this->state->bindings[$alias] ??= $binding;
74
    }
75
76
    /**
77
     * Get class name of the resolving object.
78
     * With it, you can quickly get cached singleton or detect that there are injector or binding.
79
     * The method does not detect that the class is instantiable.
80
     *
81
     * @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...
82
     *
83
     * @param self|null $actor Will be set to the hub where the result was found.
84
     *
85
     * @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...
86
     * @psalm-suppress all
87
     */
88 1253
    public function resolveType(
89
        string $alias,
90
        ?Binding &$binding = null,
91
        ?object &$singleton = null,
92
        ?object &$injector = null,
93
        ?self &$actor = null,
94
        bool $followAlias = true,
95
    ): ?string {
96
        // Aliases to prevent circular dependencies
97 1253
        $as = [];
98 1253
        $actor = $this;
99
        do {
100 1253
            $bindings = &$actor->state->bindings;
101 1253
            $singletons = &$actor->state->singletons;
102 1253
            $injectors = &$actor->state->injectors;
103 1253
            $binding = $bindings[$alias] ?? null;
104 1253
            if (\array_key_exists($alias, $singletons)) {
105 474
                $singleton = $singletons[$alias];
106 474
                $injector = $injectors[$alias] ?? null;
107 474
                return \is_object($singleton::class) ? $singleton::class : null;
108
            }
109
110 1253
            if ($binding !== null) {
111 1122
                if ($followAlias && $binding::class === Alias::class) {
112 81
                    if ($binding->alias === $alias) {
113 79
                        break;
114
                    }
115
116 2
                    $alias = $binding->alias;
117 2
                    \array_key_exists($alias, $as) and throw new ContainerException(
118 2
                        \sprintf('Circular dependency detected for alias `%s`.', $alias),
119 2
                    );
120 2
                    $as[$alias] = true;
121 2
                    continue;
122
                }
123
124 1121
                return $binding->getReturnClass();
125
            }
126
127 1105
            if (\array_key_exists($alias, $injectors)) {
128
                $injector = $injectors[$alias];
129
                $binding = $bindings[$alias] ?? null;
130
                return $alias;
131
            }
132
133
            // Go to parent scope
134 1105
            $parent = $actor->scope->getParentActor();
135 1105
            if ($parent === null) {
136 1096
                break;
137
            }
138
139 255
            $actor = $parent;
140 256
        } while (true);
141
142 1096
        return \class_exists($alias) ? $alias : null;
143
    }
144
145 1120
    public function resolveBinding(
146
        object $binding,
147
        string $alias,
148
        \Stringable|string|null $context,
149
        array $arguments,
150
        Tracer $tracer,
151
    ): mixed {
152 1120
        return match ($binding::class) {
153 858
            Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $arguments, $tracer),
154 1120
            Config\Proxy::class,
155 393
            Config\DeprecationProxy::class => $this->resolveProxy($binding, $alias, $context),
156 9
            Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $arguments, $tracer),
157 1120
            Config\DeferredFactory::class,
158 603
            Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $arguments, $tracer),
159 912
            Config\Shared::class => $this->resolveShared($binding, $alias, $context, $arguments, $tracer),
160 478
            Config\Injectable::class => $this->resolveInjector(
161 478
                $binding,
162 478
                new Ctx(alias: $alias, class: $alias, context: $context),
163 478
                $arguments,
164 478
                $tracer,
165 478
            ),
166 2
            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...
167 749
            Config\WeakReference::class => $this
168 749
                ->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

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

202
    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...
203
    {
204 530
        $context = $ctx->context;
205
        try {
206 530
            $reflection = $ctx->reflection ??= new \ReflectionClass($ctx->class);
207
        } catch (\ReflectionException $e) {
208
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
209
        }
210
211 530
        $injector = $binding->injector;
212
213
        try {
214 530
            $injectorInstance = \is_object($injector) ? $injector : $this->container->get($injector);
215
216 528
            if (!$injectorInstance instanceof InjectorInterface) {
217 2
                throw new InjectionException(
218 2
                    \sprintf(
219 2
                        "Class '%s' must be an instance of InjectorInterface for '%s'.",
220 2
                        $injectorInstance::class,
221 2
                        $reflection->getName(),
222 2
                    ),
223 2
                );
224
            }
225
226
            /** @var array<class-string<InjectorInterface>, \ReflectionMethod|false> $cache reflection for extended injectors */
227 526
            static $cache = [];
228 526
            $extended = $cache[$injectorInstance::class] ??= (
229 526
            static fn(\ReflectionType $type): bool =>
230 18
                $type::class === \ReflectionUnionType::class || (string) $type === 'mixed'
231 526
            )(
232 526
                ($refMethod = new \ReflectionMethod($injectorInstance, 'createInjection'))
233 526
                    ->getParameters()[1]->getType()
234 526
            ) ? $refMethod : false;
235
236 526
            $asIs = $extended && (\is_string($context) || $this->validateArguments($extended, [$reflection, $context]));
237 526
            $instance = $injectorInstance->createInjection($reflection, match (true) {
238 526
                $asIs => $context,
239 514
                $context instanceof \ReflectionParameter => $context->getName(),
0 ignored issues
show
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

239
                $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...
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 RectorPrefix202506\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

239
                $context instanceof \ReflectionParameter => $context->/** @scrutinizer ignore-call */ getName(),
Loading history...
240 526
                default => (string) $context,
241 526
            });
242
243 524
            if (!$reflection->isInstance($instance)) {
244 1
                throw new InjectionException(
245 1
                    \sprintf(
246 1
                        "Invalid injection response for '%s'.",
247 1
                        $reflection->getName(),
248 1
                    ),
249 1
                );
250
            }
251
252 523
            return $instance;
253 8
        } catch (TracedContainerException $e) {
254 2
            throw isset($injectorInstance) ? $e : $e::createWithTrace(\sprintf(
255 2
                'Can\'t resolve `%s`.',
256 2
                $tracer->getRootAlias(),
257 2
            ), $tracer->getTraces(), $e);
258
        } finally {
259 530
            $this->state->bindings[$ctx->class] ??= $binding;
260
        }
261
    }
262
263 858
    private function resolveAlias(
264
        Config\Alias $binding,
265
        string $alias,
266
        \Stringable|string|null $context,
267
        array $arguments,
268
        Tracer $tracer,
269
    ): mixed {
270 858
        if ($binding->alias === $alias) {
271 504
            $instance = $this->autowire(
272 504
                new Ctx(alias: $alias, class: $binding->alias, context: $context, singleton: $binding->singleton && $arguments === []),
273 504
                $arguments,
274 504
                $this,
275 504
                $tracer,
276 504
            );
277
        } else {
278
            try {
279
                //Binding is pointing to something else
280 850
                $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

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

371
            $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...
372 32
                ? ($binding->factory)()
373 573
                : $this->invoker->invoke($binding->factory, $arguments);
374 15
        } catch (NotCallableException $e) {
375
            throw TracedContainerException::createWithTrace(
376
                \sprintf('Invalid callable binding for `%s`.', $ctx->alias),
377
                $tracer->getTraces(),
378
                $e,
379
            );
380 15
        } catch (TracedContainerException $e) {
381 2
            throw $e::createWithTrace(
382 2
                \sprintf("Can't resolve `%s`: factory invocation failed.", $tracer->getRootAlias()),
383 2
                $tracer->getTraces(),
384 2
                $e,
385 2
            );
386 13
        } catch (ContainerExceptionInterface $e) {
387 5
            throw $e;
388 8
        } catch (\Throwable $e) {
389 8
            throw NotFoundException::createWithTrace(
390 8
                \sprintf("Can't resolve `%s` due to factory invocation error: %s", $tracer->getRootAlias(), $e->getMessage()),
391 8
                $tracer->getTraces(),
392 8
                $e,
393 8
            );
394
        }
395
396 596
        if (\is_object($instance)) {
397 591
            $this->validateConstraint($instance, $ctx);
398 590
            return $this->registerInstance($ctx, $instance);
399
        }
400
401 5
        return $instance;
402
    }
403
404 749
    private function resolveWeakReference(
405
        Config\WeakReference $binding,
406
        string $alias,
407
        \Stringable|string|null $context,
408
        array $arguments,
409
        Tracer $tracer,
410
    ): ?object {
411 749
        $avoidCache = $arguments !== [];
412
413 749
        if (($avoidCache || $binding->reference->get() === null) && \class_exists($alias)) {
414
            try {
415 3
                $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

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

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

520
                $reflection->/** @scrutinizer ignore-call */ 
521
                             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...
521 2
                $reflection->isAbstract() => 'Abstract class',
522 1
                default => 'Class',
523 8
            };
524 8
            throw TracedContainerException::createWithTrace(
525 8
                \sprintf('%s `%s` can not be constructed.', $itIs, $class),
526 8
                $tracer->getTraces(),
527 8
            );
528
        }
529
530 1081
        $constructor = $reflection->getConstructor();
531
532 1081
        if ($constructor !== null) {
533
            try {
534 954
                $newScope = $this !== $actor;
535 954
                $debug = [
536 954
                    'action' => 'resolve arguments',
537 954
                    'alias' => $ctx->class,
538 954
                    'signature' => $constructor,
539 954
                ];
540 954
                $newScope and $debug += [
541 954
                    'jump to scope' => $actor->scope->getScopeName(),
542 954
                    'from scope' => $this->scope->getScopeName(),
543 954
                ];
544 954
                $tracer->push($newScope, ...$debug);
545 954
                $tracer->push(true);
546 954
                $args = $actor->resolver->resolveArguments($constructor, $arguments, $actor->options->validateArguments);
547 23
            } catch (\Throwable $e) {
548 23
                throw TracedContainerException::createWithTrace(
549 23
                    \sprintf(
550 23
                        "Can't resolve `%s`.",
551 23
                        $tracer->getRootAlias(),
552 23
                    ), $tracer->getTraces(), $e
553 23
                );
554
            } finally {
555 954
                $tracer->pop($newScope);
556 954
                $tracer->pop(false);
557
            }
558
            try {
559
                // Using constructor with resolved arguments
560 933
                $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

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

612
        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...
613
            return true;
614
        }
615
616 1042
        return $ctx->reflection->getAttributes(Attribute\Singleton::class) !== [];
617
    }
618
619 1075
    private function getFinalizer(Ctx $ctx, object $instance): ?callable
620
    {
621
        /**
622
         * @psalm-suppress UnnecessaryVarAnnotation
623
         * @var Attribute\Finalize|null $attribute
624
         */
625 1075
        $attribute = ($ctx->reflection->getAttributes(Attribute\Finalize::class)[0] ?? null)?->newInstance();
626 1075
        if ($attribute === null) {
627 1074
            return null;
628
        }
629
630 4
        return [$instance, $attribute->method];
631
    }
632
633
    /**
634
     * Find and run inflector
635
     */
636 1075
    private function runInflector(object $instance): object
637
    {
638 1075
        $scope = $this->scope;
639
640 1075
        while ($scope !== null) {
641 1075
            foreach ($this->state->inflectors as $class => $inflectors) {
642 5
                if ($instance instanceof $class) {
643 5
                    foreach ($inflectors as $inflector) {
644 5
                        $instance = $inflector->getParametersCount() > 1
645 1
                            ? $this->invoker->invoke($inflector->inflector, [$instance])
646 4
                            : ($inflector->inflector)($instance);
647
                    }
648
                }
649
            }
650
651 1075
            $scope = $scope->getParentScope();
652
        }
653
654 1075
        return $instance;
655
    }
656
657 12
    private function validateArguments(ContextFunction $reflection, array $arguments = []): bool
658
    {
659
        try {
660 12
            $this->resolver->validateArguments($reflection, $arguments);
661
        } catch (\Throwable) {
662
            return false;
663
        }
664
665 12
        return true;
666
    }
667
}
668