Passed
Pull Request — master (#1076)
by Maxim
10:13
created

Factory::validateNewInstance()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7

Importance

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

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

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

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

86
                /** @scrutinizer ignore-type */ scope: $this->scope->getScopeName(),
Loading history...
87 1041
                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

87
                /** @scrutinizer ignore-type */ context: $context,
Loading history...
88 1041
                binding: $binding,
89 1041
            );
90 1041
            $this->tracer->push(true);
91
92 1041
            unset($this->state->bindings[$alias]);
93 1041
            return match ($binding::class) {
94 1041
                \Spiral\Core\Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $parameters),
95 1041
                \Spiral\Core\Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $parameters),
96 1041
                DeferredFactory::class,
97 1041
                \Spiral\Core\Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $parameters),
98 1041
                \Spiral\Core\Config\Shared::class => $this->resolveShared($binding, $alias, $context, $parameters),
99 1041
                Injectable::class => $this->resolveInjector(
100 1041
                    $binding,
101 1041
                    new Ctx(alias: $alias, class: $alias, context: $context),
102 1041
                    $parameters,
103 1041
                ),
104 1041
                \Spiral\Core\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...
105 1041
                \Spiral\Core\Config\WeakReference::class => $this
106 1041
                    ->resolveWeakReference($binding, $alias, $context, $parameters),
0 ignored issues
show
Bug introduced by
$binding of type Spiral\Core\Config\Injectable is incompatible with the type Spiral\Core\Config\WeakReference expected by parameter $binding of Spiral\Core\Internal\Fac...:resolveWeakReference(). ( Ignorable by Annotation )

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

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

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

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

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

Loading history...
121
    {
122 468
        $context = $ctx->context;
123
        try {
124 468
            $reflection = $ctx->reflection ??= new \ReflectionClass($ctx->class);
125
        } catch (\ReflectionException $e) {
126
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
127
        }
128
129 468
        $injector = $binding->injector;
130
131
        try {
132 468
            $injectorInstance = \is_object($injector) ? $injector : $this->container->get($injector);
133
134 461
            if (!$injectorInstance instanceof InjectorInterface) {
135 2
                throw new InjectionException(
136 2
                    \sprintf(
137 2
                        "Class '%s' must be an instance of InjectorInterface for '%s'.",
138 2
                        $injectorInstance::class,
139 2
                        $reflection->getName()
140 2
                    )
141 2
                );
142
            }
143
144
            /** @var array<class-string<InjectorInterface>, \ReflectionMethod|false> $cache reflection for extended injectors */
145 459
            static $cache = [];
146 459
            $extended = $cache[$injectorInstance::class] ??= (
147 459
                static fn (\ReflectionType $type): bool =>
148 18
                $type::class === \ReflectionUnionType::class || (string)$type === 'mixed'
149 459
            )(
150 459
                ($refMethod = new \ReflectionMethod($injectorInstance, 'createInjection'))
151 459
                    ->getParameters()[1]->getType()
152 459
            ) ? $refMethod : false;
153
154 459
            $asIs = $extended && (\is_string($context) || $this->validateArguments($extended, [$reflection, $context]));
155 459
            $instance = $injectorInstance->createInjection($reflection, match (true) {
156 459
                $asIs => $context,
157 459
                $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

157
                $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 RectorPrefix202308\Nette\Iterators\CachingIterator or Nette\Iterators\CachingIterator. ( Ignorable by Annotation )

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

157
                $context instanceof ReflectionParameter => $context->/** @scrutinizer ignore-call */ getName(),
Loading history...
158 459
                default => (string)$context,
159 459
            });
160
161 457
            if (!$reflection->isInstance($instance)) {
162 1
                throw new InjectionException(
163 1
                    \sprintf(
164 1
                        "Invalid injection response for '%s'.",
165 1
                        $reflection->getName()
166 1
                    )
167 1
                );
168
            }
169
170 456
            return $instance;
171
        } finally {
172 468
            $this->state->bindings[$ctx->class] ??= $binding;
173
        }
174
    }
175
176 794
    private function resolveAlias(
177
        \Spiral\Core\Config\Alias $binding,
178
        string $alias,
179
        Stringable|string|null $context,
180
        array $arguments,
181
    ): mixed {
182 794
        $result = $binding->alias === $alias
183 402
            ? $this->autowire(
184 402
                new Ctx(alias: $alias, class: $binding->alias, context: $context, singleton: $binding->singleton),
185 402
                $arguments,
186 402
            )
187 794
            //Binding is pointing to something else
188 786
            : $this->make($binding->alias, $arguments, $context);
189
190 791
        if ($binding->singleton && $arguments === []) {
191 518
            $this->state->singletons[$alias] = $result;
192
        }
193
194 791
        return $result;
195
    }
196
197 862
    private function resolveShared(
198
        \Spiral\Core\Config\Shared $binding,
199
        string $alias,
200
        Stringable|string|null $context,
201
        array $arguments,
202
    ): object {
203 862
        $avoidCache = $arguments !== [];
204 862
        return $avoidCache
205 1
            ? $this->createInstance(
206 1
                new Ctx(alias: $alias, class: $binding->value::class, context: $context),
207 1
                $arguments,
208 1
            )
209 862
            : $binding->value;
210
    }
211
212 7
    private function resolveAutowire(
213
        \Spiral\Core\Config\Autowire $binding,
214
        string $alias,
215
        Stringable|string|null $context,
216
        array $arguments,
217
    ): mixed {
218 7
        $instance = $binding->autowire->resolve($this, $arguments);
219
220 7
        $ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
221 7
        return $this->validateNewInstance($instance, $ctx, $arguments);
222
    }
223
224 478
    private function resolveFactory(
225
        \Spiral\Core\Config\Factory|DeferredFactory $binding,
226
        string $alias,
227
        Stringable|string|null $context,
228
        array $arguments,
229
    ): mixed {
230 478
        $ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
231
        try {
232 478
            $instance = $binding::class === \Spiral\Core\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

232
            $instance = $binding::class === \Spiral\Core\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...
233 18
                ? ($binding->factory)()
234 462
                : $this->invoker->invoke($binding->factory, $arguments);
235 8
        } catch (NotCallableException $e) {
236
            throw new ContainerException(
237
                $this->tracer->combineTraceMessage(\sprintf('Invalid binding for `%s`.', $ctx->alias)),
238
                $e->getCode(),
239
                $e,
240
            );
241
        }
242
243 474
        return \is_object($instance) ? $this->validateNewInstance($instance, $ctx, $arguments) : $instance;
244
    }
245
246 704
    private function resolveWeakReference(
247
        \Spiral\Core\Config\WeakReference $binding,
248
        string $alias,
249
        Stringable|string|null $context,
250
        array $arguments,
251
    ): ?object {
252 704
        $avoidCache = $arguments !== [];
253
254 704
        if (($avoidCache || $binding->reference->get() === null) && \class_exists($alias)) {
255
            try {
256 3
                $this->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

256
                $this->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

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

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

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

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

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

Loading history...
299 3
            } catch (BadScopeException $e) {
300 3
                if ($this->scope->getScopeName() !== $e->getScope()) {
301 3
                    throw $e;
302
                }
303 1
            } catch (ContainerExceptionInterface $e) {
304 1
                $className = match (true) {
305 1
                    $e instanceof NotFoundException => NotFoundException::class,
306 1
                    default => ContainerException::class,
307 1
                };
308 1
                throw new $className($this->tracer->combineTraceMessage(\sprintf(
309 1
                    'Can\'t resolve `%s`.',
310 1
                    $alias,
311 1
                )), previous: $e);
312
            } finally {
313 20
                $this->tracer->pop(false);
314
            }
315
        }
316
317 916
        $this->tracer->push(false, action: 'autowire', alias: $alias, 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

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

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

317
        $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'autowire', alias: $alias, context: $context);
Loading history...
318
        try {
319
            //No direct instructions how to construct class, make is automatically
320 916
            return $this->autowire(
321 916
                new Ctx(alias: $alias, class: $alias, context: $context),
322 916
                $parameters,
323 916
            );
324
        } finally {
325 916
            $this->tracer->pop(false);
326
        }
327
    }
328
329
    /**
330
     * Automatically create class.
331
     * Object will be cached if the $arguments list is empty.
332
     *
333
     * @psalm-assert class-string $class
334
     *
335
     * @throws AutowireException
336
     * @throws \Throwable
337
     */
338 919
    private function autowire(Ctx $ctx, array $arguments): object
339
    {
340 919
        if (!(\class_exists($ctx->class) || (
341 918
            \interface_exists($ctx->class)
342 918
                &&
343 918
                (isset($this->state->injectors[$ctx->class]) || $this->binder->hasInjector($ctx->class))
344
        ))
345
        ) {
346 646
            throw new NotFoundException($this->tracer->combineTraceMessage(\sprintf(
347 646
                'Can\'t resolve `%s`: undefined class or binding `%s`.',
348 646
                $this->tracer->getRootAlias(),
349 646
                $ctx->class,
350 646
            )));
351
        }
352
353
        // automatically create instance
354 906
        $instance = $this->createInstance($ctx, $arguments);
355
356
        // apply registration functions to created instance
357 881
        return $arguments === []
358 869
            ? $this->registerInstance($ctx, $instance)
359 881
            : $instance;
360
    }
361
362
    /**
363
     * @throws BadScopeException
364
     * @throws \Throwable
365
     */
366 474
    private function validateNewInstance(
367
        object $instance,
368
        Ctx $ctx,
369
        array $arguments,
370
    ): object {
371
        // Check scope name
372 474
        $ctx->reflection = new \ReflectionClass($instance);
373 474
        $scopeName = ($ctx->reflection->getAttributes(ScopeAttribute::class)[0] ?? null)?->newInstance()->name;
374 474
        if ($scopeName !== null && $scopeName !== $this->scope->getScopeName()) {
375 2
            $parent = $this->scope->getParentScope();
376 2
            while ($parent !== null) {
377 2
                if ($correctScope = $parent->getScopeName() === $scopeName) {
378 1
                    break;
379
                }
380 1
                $parent = $parent->getParentScope();
381
            }
382 2
            ($correctScope ?? false) === true or throw new BadScopeException($scopeName, $instance::class);
383
        }
384
385 473
        return $arguments === []
386 467
            ? $this->registerInstance($ctx, $instance)
387 473
            : $instance;
388
    }
389
390
    /**
391
     * Create instance of desired class.
392
     *
393
     * @template TObject of object
394
     *
395
     * @param Ctx<TObject> $ctx
396
     * @param array $arguments Constructor arguments.
397
     *
398
     * @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...
399
     *
400
     * @throws ContainerException
401
     * @throws \Throwable
402
     */
403 910
    private function createInstance(
404
        Ctx $ctx,
405
        array $arguments,
406
    ): object {
407 910
        $class = $ctx->class;
408
        try {
409 910
            $ctx->reflection = $reflection = new \ReflectionClass($class);
410
        } catch (\ReflectionException $e) {
411
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
412
        }
413
414
        // Check scope name
415 910
        $scope = ($reflection->getAttributes(ScopeAttribute::class)[0] ?? null)?->newInstance()->name;
416 910
        if ($scope !== null && $scope !== $this->scope->getScopeName()) {
417 4
            throw new BadScopeException($scope, $class);
418
        }
419
420
        // We have to construct class using external injector when we know the exact context
421 909
        if ($arguments === [] && $this->binder->hasInjector($class)) {
422 454
            return $this->resolveInjector($this->state->bindings[$ctx->class], $ctx, $arguments);
423
        }
424
425 901
        if (!$reflection->isInstantiable()) {
426 8
            $itIs = match (true) {
427 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

427
                $reflection->/** @scrutinizer ignore-call */ 
428
                             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...
428 8
                $reflection->isAbstract() => 'Abstract class',
429 8
                default => 'Class',
430 8
            };
431 8
            throw new ContainerException(
432 8
                $this->tracer->combineTraceMessage(\sprintf('%s `%s` can not be constructed.', $itIs, $class)),
433 8
            );
434
        }
435
436 900
        $constructor = $reflection->getConstructor();
437
438 900
        if ($constructor !== null) {
439
            try {
440 815
                $this->tracer->push(false, action: 'resolve arguments', signature: $constructor);
0 ignored issues
show
Bug introduced by
'resolve arguments' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

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

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

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

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

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