Passed
Push — master ( 2fcc18...44459b )
by butschster
11:39 queued 15s
created

Factory::validateArguments()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 9
ccs 3
cts 5
cp 0.6
rs 10
cc 2
nc 2
nop 2
crap 2.2559
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 1291
    public function __construct(Registry $constructor)
52
    {
53 1291
        $constructor->set('factory', $this);
54
55 1291
        $this->state = $constructor->get('state', State::class);
56 1291
        $this->binder = $constructor->get('binder', BinderInterface::class);
57 1291
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
58 1291
        $this->container = $constructor->get('container', ContainerInterface::class);
59 1291
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
60 1291
        $this->tracer = $constructor->get('tracer', Tracer::class);
61 1291
        $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 1104
    public function make(string $alias, array $parameters = [], Stringable|string|null $context = null): mixed
70
    {
71 1104
        if ($parameters === [] && \array_key_exists($alias, $this->state->singletons)) {
72 503
            return $this->state->singletons[$alias];
73
        }
74
75 1104
        $binding = $this->state->bindings[$alias] ?? null;
76
77 1104
        if ($binding === null) {
78 916
            return $this->resolveWithoutBinding($alias, $parameters, $context);
79
        }
80
81
        try {
82 1029
            $this->tracer->push(
83 1029
                false,
84 1029
                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 1029
                alias: $alias,
86 1029
                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 1029
                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 1029
                binding: $binding,
89 1029
            );
90 1029
            $this->tracer->push(true);
91
92 1029
            unset($this->state->bindings[$alias]);
93 1029
            return match ($binding::class) {
94 1029
                \Spiral\Core\Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $parameters),
95 1029
                \Spiral\Core\Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $parameters),
96 1029
                DeferredFactory::class,
97 1029
                \Spiral\Core\Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $parameters),
98 1029
                \Spiral\Core\Config\Shared::class => $this->resolveShared($binding, $alias, $context, $parameters),
99 1029
                Injectable::class => $this->resolveInjector(
100 1029
                    $binding,
101 1029
                    new Ctx(alias: $alias, class: $alias, context: $context),
102 1029
                    $parameters,
103 1029
                ),
104 1029
                \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 1029
                \Spiral\Core\Config\WeakReference::class => $this
106 1029
                    ->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 1029
                default => $binding,
108 1029
            };
109
        } finally {
110 1029
            $this->state->bindings[$alias] ??= $binding;
111 1029
            $this->tracer->pop(true);
112 1029
            $this->tracer->pop(false);
113
        }
114
    }
115
116
    /**
117
     * @psalm-suppress UnusedParam
118
     * todo wat should we do with $arguments?
119
     */
120 474
    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 474
        $context = $ctx->context;
123
        try {
124 474
            $reflection = $ctx->reflection ??= new \ReflectionClass($ctx->class);
125
        } catch (\ReflectionException $e) {
126
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
127
        }
128
129 474
        $injector = $binding->injector;
130
131
        try {
132 474
            $injectorInstance = \is_object($injector) ? $injector : $this->container->get($injector);
133
134 467
            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 465
            static $cache = [];
146 465
            $extended = $cache[$injectorInstance::class] ??= (
147 465
                static fn (\ReflectionType $type): bool =>
148 16
                $type::class === \ReflectionUnionType::class || (string)$type === 'mixed'
149 465
            )(
150 465
                ($refMethod = new \ReflectionMethod($injectorInstance, 'createInjection'))
151 465
                    ->getParameters()[1]->getType()
152 465
            ) ? $refMethod : false;
153
154 465
            $asIs = $extended && (\is_string($context) || $this->validateArguments($extended, [$reflection, $context]));
155 465
            $instance = $injectorInstance->createInjection($reflection, match (true) {
156 465
                $asIs => $context,
157 465
                $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 465
                default => (string)$context,
159 465
            });
160
161 463
            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 462
            return $instance;
171
        } finally {
172 474
            $this->state->bindings[$ctx->class] ??= $binding;
173
        }
174
    }
175
176 790
    private function resolveAlias(
177
        \Spiral\Core\Config\Alias $binding,
178
        string $alias,
179
        Stringable|string|null $context,
180
        array $arguments,
181
    ): mixed {
182 790
        $result = $binding->alias === $alias
183 410
            ? $this->autowire(
184 410
                new Ctx(alias: $alias, class: $binding->alias, context: $context, singleton: $binding->singleton),
185 410
                $arguments,
186 410
            )
187 790
            //Binding is pointing to something else
188 782
            : $this->make($binding->alias, $arguments, $context);
189
190 787
        if ($binding->singleton && $arguments === []) {
191 517
            $this->state->singletons[$alias] = $result;
192
        }
193
194 787
        return $result;
195
    }
196
197 870
    private function resolveShared(
198
        \Spiral\Core\Config\Shared $binding,
199
        string $alias,
200
        Stringable|string|null $context,
201
        array $arguments,
202
    ): object {
203 870
        $avoidCache = $arguments !== [];
204 870
        return $avoidCache
205 1
            ? $this->createInstance(
206 1
                new Ctx(alias: $alias, class: $binding->value::class, context: $context),
207 1
                $arguments,
208 1
            )
209 870
            : $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 482
    private function resolveFactory(
225
        \Spiral\Core\Config\Factory|DeferredFactory $binding,
226
        string $alias,
227
        Stringable|string|null $context,
228
        array $arguments,
229
    ): mixed {
230 482
        $ctx = new Ctx(alias: $alias, class: $alias, context: $context, singleton: $binding->singleton);
231
        try {
232 482
            $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 14
                ? ($binding->factory)()
234 470
                : $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 478
        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 916
    private function resolveWithoutBinding(
285
        string $alias,
286
        array $parameters = [],
287
        Stringable|string|null $context = null
288
    ): mixed {
289 916
        $parent = $this->scope->getParent();
290
291 916
        if ($parent !== null) {
292
            try {
293 14
                $this->tracer->push(false, ...[
294 14
                    'current scope' => $this->scope->getScopeName(),
295 14
                    'jump to parent scope' => $this->scope->getParentScope()->getScopeName(),
296 14
                ]);
297 14
                return $parent->make($alias, $parameters, $context);
298 3
            } catch (BadScopeException $e) {
299 3
                if ($this->scope->getScopeName() !== $e->getScope()) {
300 3
                    throw $e;
301
                }
302 1
            } catch (ContainerExceptionInterface $e) {
303 1
                $className = match (true) {
304 1
                    $e instanceof NotFoundException => NotFoundException::class,
305 1
                    default => ContainerException::class,
306 1
                };
307 1
                throw new $className($this->tracer->combineTraceMessage(\sprintf(
308 1
                    'Can\'t resolve `%s`.',
309 1
                    $alias,
310 1
                )), previous: $e);
311
            } finally {
312 14
                $this->tracer->pop(false);
313
            }
314
        }
315
316 912
        $this->tracer->push(false, action: 'autowire', alias: $alias, context: $context);
0 ignored issues
show
Bug introduced by
'autowire' of type string is incompatible with the type boolean expected by parameter $nextLevel of Spiral\Core\Internal\Tracer::push(). ( Ignorable by Annotation )

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

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

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

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

419
                $reflection->/** @scrutinizer ignore-call */ 
420
                             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...
420 8
                $reflection->isAbstract() => 'Abstract class',
421 8
                default => 'Class',
422 8
            };
423 8
            throw new ContainerException(
424 8
                $this->tracer->combineTraceMessage(\sprintf('%s `%s` can not be constructed.', $itIs, $class)),
425 8
            );
426
        }
427
428 896
        $constructor = $reflection->getConstructor();
429
430 896
        if ($constructor !== null) {
431
            try {
432 819
                $this->tracer->push(false, action: 'resolve arguments', signature: $constructor);
0 ignored issues
show
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

432
                $this->tracer->push(false, action: 'resolve arguments', /** @scrutinizer ignore-type */ signature: $constructor);
Loading history...
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

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

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