Test Failed
Pull Request — master (#870)
by Aleksei
08:56
created

Factory::createInstance()   C

Complexity

Conditions 12
Paths 59

Size

Total Lines 103
Code Lines 62

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 12.0427

Importance

Changes 4
Bugs 2 Features 0
Metric Value
eloc 62
dl 0
loc 103
ccs 14
cts 15
cp 0.9333
rs 6.4024
c 4
b 2
f 0
cc 12
nc 59
nop 2
crap 12.0427

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Spiral\Core\Internal;
6
7
use Psr\Container\ContainerExceptionInterface;
8
use Psr\Container\ContainerInterface;
9
use Spiral\Core\Attribute\Finalize;
10
use Spiral\Core\Attribute\Scope as ScopeAttribute;
11
use Spiral\Core\Attribute\Singleton;
12
use Spiral\Core\BinderInterface;
13
use Spiral\Core\Container\Autowire;
14
use Spiral\Core\Container\InjectorInterface;
15
use Spiral\Core\Container\SingletonInterface;
16
use Spiral\Core\Exception\Container\AutowireException;
17
use Spiral\Core\Exception\Container\ContainerException;
18
use Spiral\Core\Exception\Container\InjectionException;
19
use Spiral\Core\Exception\Container\NotCallableException;
20
use Spiral\Core\Exception\Container\NotFoundException;
21
use Spiral\Core\Exception\Resolver\ValidationException;
22
use Spiral\Core\Exception\Resolver\WrongTypeException;
23
use Spiral\Core\Exception\Scope\BadScopeException;
24
use Spiral\Core\FactoryInterface;
25
use Spiral\Core\Internal\Common\DestructorTrait;
26
use Spiral\Core\Internal\Common\Registry;
27
use Spiral\Core\Internal\Factory\Ctx;
28
use Spiral\Core\InvokerInterface;
29
use Spiral\Core\ResolverInterface;
30
use WeakReference;
31
32
/**
33
 * @internal
34
 */
35
final class Factory implements FactoryInterface
36
{
37
    use DestructorTrait;
38 1004
39
    private State $state;
40 1004
    private BinderInterface $binder;
41
    private InvokerInterface $invoker;
42 1004
    private ContainerInterface $container;
43 1004
    private ResolverInterface $resolver;
44 1004
    private Tracer $tracer;
45 1004
    private Scope $scope;
46 1004
47 1004
    public function __construct(Registry $constructor)
48
    {
49
        $constructor->set('factory', $this);
50
51
        $this->state = $constructor->get('state', State::class);
52
        $this->binder = $constructor->get('binder', BinderInterface::class);
53
        $this->invoker = $constructor->get('invoker', InvokerInterface::class);
54
        $this->container = $constructor->get('container', ContainerInterface::class);
55 843
        $this->resolver = $constructor->get('resolver', ResolverInterface::class);
56
        $this->tracer = $constructor->get('tracer', Tracer::class);
57 843
        $this->scope = $constructor->get('scope', Scope::class);
58 687
    }
59
60
    /**
61 687
     * @param string|null $context Related to parameter caused injection if any.
62
     *
63 687
     * @throws \Throwable
64
     */
65
    public function make(string $alias, array $parameters = [], string $context = null): mixed
66
    {
67 765
        if (!isset($this->state->bindings[$alias])) {
68 765
            return $this->resolveWithoutBinding($alias, $parameters, $context);
69
        }
70 765
71 765
        $avoidCache = $parameters !== [];
72 765
        $binding = $this->state->bindings[$alias];
73 765
        try {
74 765
            $this->tracer->push(
75 765
                false,
76 765
                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

76
                /** @scrutinizer ignore-type */ action: 'resolve from binding',
Loading history...
77 765
                alias: $alias,
78
                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

78
                /** @scrutinizer ignore-type */ scope: $this->scope->getScopeName(),
Loading history...
79 765
                context: $context,
80 731
                binding: $binding,
81 525
            );
82
            $this->tracer->push(true);
83 3
84 3
            if (\is_object($binding)) {
85 3
                if ($binding::class === WeakReference::class) {
86 1
                    return $this->resolveWeakReference($binding, $alias, $context, $parameters);
87
                }
88 2
89
                // When binding is instance, assuming singleton
90
                return $avoidCache
91
                    ? $this->createInstance(
92
                        new Ctx(alias: $alias, class: $binding::class, parameter: $context),
93
                        $parameters,
94
                    )
95
                    : $binding;
96 3
            }
97
98
            $ctx = new Ctx(alias: $alias, class: $alias, parameter: $context);
99 525
            if (\is_string($binding)) {
100
                $ctx->class = $binding;
101
                return $binding === $alias
102 658
                    ? $this->autowire($ctx, $parameters)
103 3
                    //Binding is pointing to something else
104 658
                    : $this->make($binding, $parameters, $context);
105
            }
106
107 604
            if ($binding[1] === true) {
108 562
                $ctx->singleton = true;
109 1
            }
110 562
            unset($this->state->bindings[$alias]);
111 562
            try {
112
                return $binding[0] === $alias
113
                    ? $this->autowire($ctx, $parameters)
114 390
                    : $this->evaluateBinding($ctx, $binding[0], $parameters);
115
            } finally {
116 390
                $this->state->bindings[$alias] ??= $binding;
117 326
            }
118 383
        } finally {
119
            $this->tracer->pop(true);
120
            $this->tracer->pop(false);
121 390
        }
122
    }
123
124 765
    private function resolveWeakReference(
125 765
        WeakReference $binding,
126
        string $alias,
127
        ?string $context,
128 385
        array $parameters
129
    ): ?object {
130
        $avoidCache = $parameters !== [];
131 376
132
        if (($avoidCache || $binding->get() === null) && \class_exists($alias)) {
133
            try {
134 385
                $this->tracer->push(false, alias: $alias, source: WeakReference::class, context: $context);
0 ignored issues
show
Bug introduced by
$context 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

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

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

134
                $this->tracer->push(false, /** @scrutinizer ignore-type */ alias: $alias, source: WeakReference::class, context: $context);
Loading history...
135
                /** @psalm-suppress NoValue */
136
                $object = $this->createInstance(
137
                    new Ctx(alias: $alias, class: $alias, parameter: $context),
138
                    $parameters,
139
                );
140
                if ($avoidCache) {
141
                    return $object;
142
                }
143
                $binding = $this->state->bindings[$alias] = WeakReference::create($object);
144
            } catch (\Throwable) {
145
                throw new ContainerException(
146 689
                    $this->tracer->combineTraceMessage(
147
                        \sprintf(
148 689
                            'Can\'t resolve `%s`: can\'t instantiate `%s` from WeakReference binding.',
149 688
                            $this->tracer->getRootAlias(),
150 688
                            $alias,
151 688
                        )
152
                    )
153
                );
154 452
            } finally {
155 452
                $this->tracer->pop();
156 452
            }
157 452
        }
158 452
159
        return $binding->get();
160
    }
161
162 677
    private function resolveWithoutBinding(string $alias, array $parameters = [], string $context = null): mixed
163
    {
164
        $parent = $this->scope->getParent();
165 650
166 641
        if ($parent !== null) {
167 650
            try {
168
                $this->tracer->push(false, ...[
169
                    'current scope' => $this->scope->getScopeName(),
170
                    'jump to parent scope' => $this->scope->getParentScope()->getScopeName(),
171
                ]);
172
                return $parent->make($alias, $parameters, $context);
173
            } catch (BadScopeException $e) {
174
                if ($this->scope->getScopeName() !== $e->getScope()) {
175
                    throw $e;
176 383
                }
177
            } catch (ContainerExceptionInterface $e) {
178
                $className = match (true) {
179
                    $e instanceof NotFoundException => NotFoundException::class,
180
                    default => ContainerException::class,
181
                };
182 383
                throw new $className($this->tracer->combineTraceMessage(\sprintf(
183
                    'Can\'t resolve `%s`.',
184 358
                    $alias,
185
                )), previous: $e);
186
            } finally {
187 349
                $this->tracer->pop(false);
188 3
            }
189
        }
190
191
        $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

191
        $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'autowire', alias: $alias, context: $context);
Loading history...
Bug introduced by
$context 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

191
        $this->tracer->push(false, action: 'autowire', alias: $alias, /** @scrutinizer ignore-type */ context: $context);
Loading history...
192 346
        try {
193 8
            //No direct instructions how to construct class, make is automatically
194 2
            return $this->autowire(
195 2
                new Ctx(alias: $alias, class: $alias, parameter: $context),
196 2
                $parameters,
197 2
            );
198 2
        } finally {
199
            $this->tracer->pop(false);
200
        }
201
    }
202
203
    /**
204
     * Automatically create class.
205
     * Object will be cached if the $arguments list is empty.
206
     *
207
     * @psalm-assert class-string $class
208
     *
209
     * @throws AutowireException
210
     * @throws \Throwable
211
     */
212
    private function autowire(Ctx $ctx, array $arguments): object
213
    {
214
        if (!(\class_exists($ctx->class) || (
215 681
            \interface_exists($ctx->class)
216
                &&
217
                (isset($this->state->injectors[$ctx->class]) || $this->binder->hasInjector($ctx->class))
218 681
        ))
219
        ) {
220
            throw new NotFoundException($this->tracer->combineTraceMessage(\sprintf(
221
                'Can\'t resolve `%s`: undefined class or binding `%s`.',
222
                $this->tracer->getRootAlias(),
223
                $ctx->class,
224 681
            )));
225 356
        }
226
227
        // automatically create instance
228 356
        $instance = $this->createInstance($ctx, $arguments);
229
230 348
        // apply registration functions to created instance
231 2
        return $arguments === []
232 2
            ? $this->registerInstance($ctx, $instance)
233 2
            : $instance;
234 2
    }
235 2
236 2
    /**
237 2
     * @param mixed $target Value that was bound by user.
238
     *
239
     * @throws ContainerException
240
     * @throws \Throwable
241
     */
242
    private function evaluateBinding(
243
        Ctx $ctx,
244 346
        mixed $target,
245 344
        array $arguments,
246 1
    ): mixed {
247 1
        if (\is_string($target)) {
248 1
            // Reference
249 1
            $instance = $this->make($target, $arguments, $ctx->parameter);
250 1
        } else {
251 1
            if ($target instanceof Autowire) {
252
                $instance = $target->resolve($this, $arguments);
253
            } else {
254 343
                try {
255
                    $instance = $this->invoker->invoke($target, $arguments);
256 356
                } catch (NotCallableException $e) {
257
                    throw new ContainerException(
258
                        $this->tracer->combineTraceMessage(\sprintf('Invalid binding for `%s`.', $ctx->alias)),
259
                        $e->getCode(),
260 669
                        $e,
261 4
                    );
262 4
                }
263 4
            }
264 4
265 4
            // Check scope name
266 4
            if (\is_object($instance)) {
267 4
                $ctx->reflection = new \ReflectionClass($instance);
268 4
                $scopeName = ($ctx->reflection->getAttributes(ScopeAttribute::class)[0] ?? null)?->newInstance()->name;
269
                if ($scopeName !== null && $scopeName !== $this->scope->getScopeName()) {
270
                    throw new BadScopeException($scopeName, $instance::class);
271 668
                }
272
            }
273 668
        }
274
275 609
        return \is_object($instance) && $arguments === []
276 609
            ? $this->registerInstance($ctx, $instance)
277 609
            : $instance;
278 20
    }
279 6
280 6
    /**
281 6
     * Create instance of desired class.
282 6
     *
283 6
     * @template TObject of object
284 6
     *
285 6
     * @param Ctx<TObject> $ctx
286 6
     * @param array $parameters Constructor parameters.
287 6
     *
288
     * @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...
289 609
     *
290 609
     * @throws ContainerException
291
     * @throws \Throwable
292
     */
293
    private function createInstance(
294 589
        Ctx $ctx,
295 589
        array $parameters,
296 589
    ): object {
297 1
        $class = $ctx->class;
298
        try {
299
            $ctx->reflection = $reflection = new \ReflectionClass($class);
300 589
        } catch (\ReflectionException $e) {
301 589
            throw new ContainerException($e->getMessage(), $e->getCode(), $e);
302
        }
303
304
        // Check scope name
305 524
        $scope = ($reflection->getAttributes(ScopeAttribute::class)[0] ?? null)?->newInstance()->name;
306
        if ($scope !== null && $scope !== $this->scope->getScopeName()) {
307
            throw new BadScopeException($scope, $class);
308 647
        }
309
310
        //We have to construct class using external injector when we know exact context
311
        if ($parameters === [] && $this->binder->hasInjector($class)) {
312
            $injector = $this->state->injectors[$reflection->getName()];
313
314
            try {
315
                $injectorInstance = $this->container->get($injector);
316
317
                if (!$injectorInstance instanceof InjectorInterface) {
318
                    throw new InjectionException(
319
                        \sprintf(
320
                            "Class '%s' must be an instance of InjectorInterface for '%s'.",
321 641
                            $injectorInstance::class,
322
                            $reflection->getName()
323
                        )
324 641
                    );
325 385
                }
326 385
327 385
                /**
328
                 * @var InjectorInterface<TObject> $injectorInstance
329
                 * @psalm-suppress RedundantCondition
330
                 */
331 641
                $instance = $injectorInstance->createInjection($reflection, $ctx->parameter);
332
                if (!$reflection->isInstance($instance)) {
333
                    throw new InjectionException(
334
                        \sprintf(
335
                            "Invalid injection response for '%s'.",
336
                            $reflection->getName()
337
                        )
338
                    );
339
                }
340
341
                return $instance;
342
            } finally {
343
                $this->state->injectors[$reflection->getName()] = $injector;
344
            }
345
        }
346
347
        if (!$reflection->isInstantiable()) {
348
            $itIs = match (true) {
349
                $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

349
                $reflection->/** @scrutinizer ignore-call */ 
350
                             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...
350
                $reflection->isAbstract() => 'Abstract class',
351
                default => 'Class',
352
            };
353
            throw new ContainerException(
354
                $this->tracer->combineTraceMessage(\sprintf('%s `%s` can not be constructed.', $itIs, $class)),
355
            );
356
        }
357
358
        $constructor = $reflection->getConstructor();
359
360
        if ($constructor !== null) {
361
            try {
362
                $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

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

362
                $this->tracer->push(false, /** @scrutinizer ignore-type */ action: 'resolve arguments', signature: $constructor);
Loading history...
363
                $this->tracer->push(true);
364
                $arguments = $this->resolver->resolveArguments($constructor, $parameters);
365
            } catch (ValidationException $e) {
366
                throw new ContainerException(
367
                    $this->tracer->combineTraceMessage(
368
                        \sprintf(
369
                            'Can\'t resolve `%s`. %s',
370
                            $this->tracer->getRootAlias(),
371
                            $e->getMessage()
372
                        )
373
                    ),
374
                );
375
            } finally {
376
                $this->tracer->pop(true);
377
                $this->tracer->pop(false);
378
            }
379
            try {
380
                // Using constructor with resolved arguments
381
                $this->tracer->push(false, call: "$class::__construct", arguments: $arguments);
382
                $this->tracer->push(true);
383
                $instance = new $class(...$arguments);
384
            } catch (\TypeError $e) {
385
                throw new WrongTypeException($constructor, $e);
386
            } finally {
387
                $this->tracer->pop(true);
388
                $this->tracer->pop(false);
389
            }
390
        } else {
391
            // No constructor specified
392
            $instance = $reflection->newInstance();
393
        }
394
395
        return $instance;
396
    }
397
398
    /**
399
     * Register instance in container, might perform methods like auto-singletons, log populations
400
     * and etc.
401
     *
402
     * @template TObject of object
403
     *
404
     * @param TObject $instance Created object.
405
     * @param \ReflectionClass<TObject> $reflection
406
     *
407
     * @return TObject
408
     */
409
    private function registerInstance(Ctx $ctx, object $instance): object
410
    {
411
        $ctx->reflection ??= new \ReflectionClass($instance);
412
413
        //Declarative singletons
414
        if ($this->isSingleton($ctx)) {
415
            $this->state->bindings[$ctx->alias] = $instance;
416
        }
417
418
        // Register finalizer
419
        $finalizer = $this->getFinalizer($ctx, $instance);
420
        if ($finalizer !== null) {
421
            $this->state->finalizers[] = $finalizer;
422
        }
423
424
        return $instance;
425
    }
426
427
    /**
428
     * Check the class was configured as a singleton.
429
     */
430
    private function isSingleton(Ctx $ctx): bool
431
    {
432
        if ($ctx->singleton === true) {
433
            return true;
434
        }
435
436
        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

436
        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...
437
            return true;
438
        }
439
440
        return $ctx->reflection->getAttributes(Singleton::class) !== [];
441
    }
442
443
    private function getFinalizer(Ctx $ctx, object $instance): ?callable
444
    {
445
        /** @var Finalize|null $attribute */
446
        $attribute = ($ctx->reflection->getAttributes(Finalize::class)[0] ?? null)?->newInstance();
447
        if ($attribute === null) {
448
            return null;
449
        }
450
451
        return [$instance, $attribute->method];
452
    }
453
}
454