Passed
Push — master ( de3d61...be839c )
by Alec
13:42 queued 13s
created

ServiceSpawner::createInstanceByReflection()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 24
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 16
c 1
b 0
f 0
nc 6
nop 1
dl 0
loc 24
rs 9.1111
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlecRabbit\Spinner\Container;
6
7
use AlecRabbit\Spinner\Container\Contract\IServiceSpawner;
8
use AlecRabbit\Spinner\Container\Exception\ClassDoesNotExistException;
9
use AlecRabbit\Spinner\Container\Exception\SpawnFailedException;
10
use AlecRabbit\Spinner\Container\Exception\UnableToCreateInstanceException;
11
use AlecRabbit\Spinner\Container\Exception\UnableToExtractTypeException;
12
use Psr\Container\ContainerExceptionInterface;
13
use Psr\Container\ContainerInterface;
14
use Psr\Container\NotFoundExceptionInterface;
15
use ReflectionClass;
16
use ReflectionException;
17
use ReflectionIntersectionType;
0 ignored issues
show
Bug introduced by
The type ReflectionIntersectionType 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...
18
use ReflectionNamedType;
19
use ReflectionUnionType;
20
use Throwable;
21
22
final class ServiceSpawner implements IServiceSpawner
23
{
24
    public function __construct(
25
        protected ContainerInterface $container,
26
    ) {
27
    }
28
29
    public function spawn(string|callable|object $definition): object
30
    {
31
        try {
32
            return match (true) {
33
                is_callable($definition) => $this->spawnByCallable($definition),
0 ignored issues
show
Bug introduced by
It seems like $definition can also be of type object; however, parameter $definition of AlecRabbit\Spinner\Conta...wner::spawnByCallable() does only seem to accept callable, maybe add an additional type check? ( Ignorable by Annotation )

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

33
                is_callable($definition) => $this->spawnByCallable(/** @scrutinizer ignore-type */ $definition),
Loading history...
34
                is_string($definition) => $this->spawnByClassConstructor($definition),
0 ignored issues
show
Bug introduced by
It seems like $definition can also be of type callable and object; however, parameter $definition of AlecRabbit\Spinner\Conta...awnByClassConstructor() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

34
                is_string($definition) => $this->spawnByClassConstructor(/** @scrutinizer ignore-type */ $definition),
Loading history...
35
                default => $definition, // return object as is
36
            };
37
        } catch (Throwable $e) {
38
            throw new SpawnFailedException(
39
                sprintf(
40
                    'Could not spawn object with callable.%s',
41
                    sprintf(
42
                        ' [%s]: "%s".',
43
                        get_debug_type($e),
44
                        $e->getMessage(),
45
                    ),
46
                ),
47
                previous: $e,
48
            );
49
        }
50
    }
51
52
    private function spawnByCallable(callable $definition): object
53
    {
54
        /** @psalm-suppress MixedReturnStatement */
55
        return $definition($this->container);
56
    }
57
58
    /**
59
     * @param class-string $definition
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
60
     *
61
     * @throws ContainerExceptionInterface
62
     * @throws NotFoundExceptionInterface
63
     * @throws ReflectionException
64
     */
65
    private function spawnByClassConstructor(string $definition): object
66
    {
67
        return match (true) {
68
            class_exists($definition) => $this->createInstanceByReflection($definition),
69
            default => throw new ClassDoesNotExistException('Class does not exist: ' . $definition),
70
        };
71
    }
72
73
    /**
74
     * @param class-string $class
0 ignored issues
show
Documentation Bug introduced by
The doc comment class-string at position 0 could not be parsed: Unknown type name 'class-string' at position 0 in class-string.
Loading history...
75
     *
76
     * @throws ContainerExceptionInterface
77
     * @throws NotFoundExceptionInterface
78
     * @throws ReflectionException
79
     */
80
    private function createInstanceByReflection(string $class): object
81
    {
82
        $reflection = new ReflectionClass($class);
83
84
        $constructorParameters = $reflection->getConstructor()?->getParameters();
85
        if ($constructorParameters) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $constructorParameters of type ReflectionParameter[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
86
            $parameters = [];
87
            foreach ($constructorParameters as $parameter) {
88
                $name = $parameter->getName();
89
                $type = $parameter->getType();
90
                if ($type === null) {
91
                    throw new UnableToExtractTypeException('Unable to extract type for parameter name: $' . $name);
92
                }
93
                if ($this->needsService($type)) {
94
                    $parameters[$name] = $this->getServiceFromContainer($type->getName());
0 ignored issues
show
Bug introduced by
The method getName() does not exist on ReflectionType. It seems like you code against a sub-type of ReflectionType such as ReflectionNamedType. ( Ignorable by Annotation )

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

94
                    $parameters[$name] = $this->getServiceFromContainer($type->/** @scrutinizer ignore-call */ getName());
Loading history...
95
                }
96
            }
97
            return new $class(...$parameters);
98
        }
99
100
        try {
101
            return new $class();
102
        } catch (Throwable $e) {
103
            throw new UnableToCreateInstanceException('Unable to create instance of ' . $class, previous: $e);
104
        }
105
    }
106
107
    /**
108
     * @throws ContainerExceptionInterface
109
     */
110
    private function needsService(ReflectionIntersectionType|ReflectionNamedType|ReflectionUnionType $type): bool
111
    {
112
        return match (true) {
113
            // assumes that all non-builtin types are services
114
            $type instanceof ReflectionNamedType => !$type->isBuiltin(),
115
            default => throw new UnableToExtractTypeException(
116
                sprintf(
117
                    'Only %s is supported.',
118
                    ReflectionNamedType::class,
119
                )
120
            ),
121
        };
122
    }
123
124
    /**
125
     * @throws ContainerExceptionInterface
126
     * @throws NotFoundExceptionInterface
127
     */
128
    private function getServiceFromContainer(string $id): object
129
    {
130
        return $this->container->get($id);
131
    }
132
}
133