Test Failed
Pull Request — master (#1221)
by Aleksei
12:29
created

Actor::validateConstraint()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 12
rs 10
c 0
b 0
f 0
cc 4
nc 4
nop 2
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
    public function disableBinding(string $alias): void
66
    {
67
        unset($this->state->bindings[$alias]);
68
    }
69
70
    public function enableBinding(string $alias, Binding $binding): void
71
    {
72
        $this->state->bindings[$alias] ??= $binding;
73
    }
74
75
    /**
76
     * Get class name of the resolving object.
77
     * With it, you can quickly get cached singleton or detect that there are injector or binding.
78
     * The method does not detect that the class is instantiable.
79
     *
80
     * @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...
81
     *
82
     * @param self|null $actor Will be set to the hub where the result was found.
83
     *
84
     * @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...
85
     * @psalm-suppress all
86
     */
87
    public function resolveType(
88
        string $alias,
89
        ?Binding &$binding = null,
90
        ?object &$singleton = null,
91
        ?object &$injector = null,
92
        ?self &$actor = null,
93
        bool $followAlias = true,
94
    ): ?string {
95
        // Aliases to prevent circular dependencies
96
        $as = [];
97
        $actor = $this;
98
        do {
99
            $bindings = &$actor->state->bindings;
100
            $singletons = &$actor->state->singletons;
101
            $injectors = &$actor->state->injectors;
102
            $binding = $bindings[$alias] ?? null;
103
            if (\array_key_exists($alias, $singletons)) {
104
                $singleton = $singletons[$alias];
105
                $injector = $injectors[$alias] ?? null;
106
                return \is_object($singleton::class) ? $singleton::class : null;
107
            }
108
109
            if ($binding !== null) {
110
                if ($followAlias && $binding::class === Alias::class) {
111
                    if ($binding->alias === $alias) {
112
                        break;
113
                    }
114
115
                    $alias = $binding->alias;
116
                    \array_key_exists($alias, $as) and throw new ContainerException(
117
                        \sprintf('Circular dependency detected for alias `%s`.', $alias),
118
                    );
119
                    $as[$alias] = true;
120
                    continue;
121
                }
122
123
                return $binding->getReturnClass();
124
            }
125
126
            if (\array_key_exists($alias, $injectors)) {
127
                $injector = $injectors[$alias];
128
                $binding = $bindings[$alias] ?? null;
129
                return $alias;
130
            }
131
132
            // Go to parent scope
133
            $parent = $actor->scope->getParentActor();
134
            if ($parent === null) {
135
                break;
136
            }
137
138
            $actor = $parent;
139
        } while (true);
140
141
        return \class_exists($alias) ? $alias : null;
142
    }
143
144
    public function resolveBinding(
145
        object $binding,
146
        string $alias,
147
        \Stringable|string|null $context,
148
        array $arguments,
149
        Tracer $tracer,
150
    ): mixed {
151
        return match ($binding::class) {
152
            Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $arguments, $tracer),
153
            Config\Proxy::class,
154
            Config\DeprecationProxy::class => $this->resolveProxy($binding, $alias, $context),
155
            Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $arguments, $tracer),
156
            Config\DeferredFactory::class,
157
            Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $arguments, $tracer),
158
            Config\Shared::class => $this->resolveShared($binding, $alias, $context, $arguments, $tracer),
159
            Config\Injectable::class => $this->resolveInjector(
160
                $binding,
161
                new Ctx(alias: $alias, class: $alias, context: $context),
162
                $arguments,
163
                $tracer,
164
            ),
165
            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...
166
            Config\WeakReference::class => $this
167
                ->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

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

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

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

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

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

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

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

412
                $tracer->push(false, /** @scrutinizer ignore-type */ alias: $alias, source: \WeakReference::class, context: $context);
Loading history...
413
414
                $object = $this->createInstance(
415
                    new Ctx(alias: $alias, class: $alias, context: $context, singleton: false),
416
                    $arguments,
417
                    $this,
418
                    $tracer,
419
                );
420
                if ($avoidCache) {
421
                    return $object;
422
                }
423
                $binding->reference = \WeakReference::create($object);
424
            } catch (\Throwable) {
425
                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

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

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

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

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