Test Failed
Pull Request — master (#1041)
by Aleksei
06:38
created

Factory::validateArguments()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 5
c 0
b 0
f 0
dl 0
loc 9
ccs 0
cts 0
cp 0
rs 10
cc 2
nc 2
nop 2
crap 6
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 1285
    private Scope $scope;
50
51 1285
    public function __construct(Registry $constructor)
52
    {
53 1285
        $constructor->set('factory', $this);
54 1285
55 1285
        $this->state = $constructor->get('state', State::class);
56 1285
        $this->binder = $constructor->get('binder', BinderInterface::class);
57 1285
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
58 1285
        $this->container = $constructor->get('container', ContainerInterface::class);
59 1285
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
60
        $this->tracer = $constructor->get('tracer', Tracer::class);
61
        $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 1098
     * @throws \Throwable
68
     */
69 1098
    public function make(string $alias, array $parameters = [], Stringable|string|null $context = null): mixed
70 500
    {
71
        if ($parameters === [] && \array_key_exists($alias, $this->state->singletons)) {
72
            return $this->state->singletons[$alias];
73 1098
        }
74
75 1098
        $binding = $this->state->bindings[$alias] ?? null;
76 912
77
        if ($binding === null) {
78
            return $this->resolveWithoutBinding($alias, $parameters, $context);
79
        }
80 1023
81 1023
        try {
82 1023
            $this->tracer->push(
83 1023
                false,
84 1023
                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 1023
                alias: $alias,
86 1023
                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 1023
                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 1023
                binding: $binding,
89
            );
90 1023
            $this->tracer->push(true);
91 1023
92 1023
            unset($this->state->bindings[$alias]);
93 1023
            return match ($binding::class) {
94 1023
                \Spiral\Core\Config\Alias::class => $this->resolveAlias($binding, $alias, $context, $parameters),
95 1023
                \Spiral\Core\Config\Autowire::class => $this->resolveAutowire($binding, $alias, $context, $parameters),
96 1023
                DeferredFactory::class,
97 1023
                \Spiral\Core\Config\Factory::class => $this->resolveFactory($binding, $alias, $context, $parameters),
98 1023
                \Spiral\Core\Config\Shared::class => $this->resolveShared($binding, $alias, $context, $parameters),
99 1023
                Injectable::class => $this->resolveInjector(
100 1023
                    $binding,
101 1023
                    new Ctx(alias: $alias, class: $alias, context: $context),
102 1023
                    $parameters,
103
                ),
104 1023
                \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 1023
                \Spiral\Core\Config\WeakReference::class => $this
106 1023
                    ->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
                default => $binding,
108
            };
109
        } finally {
110 468
            $this->state->bindings[$alias] ??= $binding;
111
            $this->tracer->pop(true);
112
            $this->tracer->pop(false);
113
        }
114
    }
115
116 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

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

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

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

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

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

253
                $this->tracer->push(false, alias: $alias, source: WeakReference::class, /** @scrutinizer ignore-type */ context: $context);
Loading history...
254
255
                $object = $this->createInstance(
256
                    new Ctx(alias: $alias, class: $alias, context: $context),
257
                    $arguments,
258
                );
259
                if ($avoidCache) {
260
                    return $object;
261
                }
262
                $binding->reference = WeakReference::create($object);
263
            } catch (\Throwable) {
264
                throw new ContainerException(
265 3
                    $this->tracer->combineTraceMessage(
266
                        \sprintf(
267
                            'Can\'t resolve `%s`: can\'t instantiate `%s` from WeakReference binding.',
268
                            $this->tracer->getRootAlias(),
269 701
                            $alias,
270
                        )
271
                    )
272 912
                );
273
            } finally {
274 912
                $this->tracer->pop();
275
            }
276 912
        }
277
278 14
        return $binding->reference->get();
279 14
    }
280 14
281 14
    private function resolveWithoutBinding(
282 14
        string $alias,
283 3
        array $parameters = [],
284 3
        Stringable|string|null $context = null
285 3
    ): mixed {
286
        $parent = $this->scope->getParent();
287 1
288 1
        if ($parent !== null) {
289 1
            try {
290 1
                $this->tracer->push(false, ...[
291 1
                    'current scope' => $this->scope->getScopeName(),
292 1
                    'jump to parent scope' => $this->scope->getParentScope()->getScopeName(),
293 1
                ]);
294 1
                return $parent->make($alias, $parameters, $context);
295 1
            } catch (BadScopeException $e) {
296
                if ($this->scope->getScopeName() !== $e->getScope()) {
297 14
                    throw $e;
298
                }
299
            } catch (ContainerExceptionInterface $e) {
300
                $className = match (true) {
301 908
                    $e instanceof NotFoundException => NotFoundException::class,
302
                    default => ContainerException::class,
303
                };
304 908
                throw new $className($this->tracer->combineTraceMessage(\sprintf(
305 908
                    'Can\'t resolve `%s`.',
306 908
                    $alias,
307 908
                )), previous: $e);
308
            } finally {
309 908
                $this->tracer->pop(false);
310
            }
311
        }
312
313
        $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

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

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

416
                $reflection->/** @scrutinizer ignore-call */ 
417
                             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...
417 816
                $reflection->isAbstract() => 'Abstract class',
418 816
                default => 'Class',
419 816
            };
420 21
            throw new ContainerException(
421 6
                $this->tracer->combineTraceMessage(\sprintf('%s `%s` can not be constructed.', $itIs, $class)),
422 6
            );
423 6
        }
424 6
425 6
        $constructor = $reflection->getConstructor();
426 6
427 6
        if ($constructor !== null) {
428 6
            try {
429 6
                $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

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

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

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