Issues (248)

src/Spinner/Container/ServiceSpawner.php (1 issue)

Labels
Severity
1
<?php
2
3
declare(strict_types=1);
4
5
namespace AlecRabbit\Spinner\Container;
6
7
use AlecRabbit\Spinner\Container\Contract\ICircularDependencyDetector;
8
use AlecRabbit\Spinner\Container\Contract\IService;
9
use AlecRabbit\Spinner\Container\Contract\IServiceDefinition;
10
use AlecRabbit\Spinner\Container\Contract\IServiceFactory;
11
use AlecRabbit\Spinner\Container\Contract\IServiceSpawner;
12
use AlecRabbit\Spinner\Container\Exception\ClassDoesNotExist;
13
use AlecRabbit\Spinner\Container\Exception\SpawnFailed;
14
use AlecRabbit\Spinner\Container\Exception\UnableToCreateInstance;
15
use AlecRabbit\Spinner\Container\Exception\UnableToExtractType;
16
use Psr\Container\ContainerExceptionInterface;
17
use Psr\Container\ContainerInterface;
18
use Psr\Container\NotFoundExceptionInterface;
19
use ReflectionClass;
20
use ReflectionException;
21
use ReflectionNamedType;
22
use Throwable;
23
24
final readonly class ServiceSpawner implements IServiceSpawner
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_READONLY, expecting T_CLASS on line 24 at column 6
Loading history...
25
{
26
    public function __construct(
27
        private ContainerInterface $container,
28
        private ICircularDependencyDetector $circularDependencyDetector,
29
        private IServiceFactory $serviceObjectFactory,
30
    ) {
31
    }
32
33
    public function spawn(IServiceDefinition $serviceDefinition): IService
34
    {
35
        try {
36
            return $this->spawnService($serviceDefinition);
37
        } catch (Throwable $e) {
38
            $details =
39
                sprintf(
40
                    '[%s]: "%s".',
41
                    get_debug_type($e),
42
                    $e->getMessage(),
43
                );
44
45
            throw new SpawnFailed(
46
                sprintf(
47
                    'Failed to spawn service with id "%s". %s',
48
                    $serviceDefinition->getId(),
49
                    $details,
50
                ),
51
                previous: $e,
52
            );
53
        }
54
    }
55
56
    /**
57
     * @throws ReflectionException
58
     * @throws ContainerExceptionInterface
59
     * @throws NotFoundExceptionInterface
60
     */
61
    private function spawnService(IServiceDefinition $serviceDefinition): IService
62
    {
63
        $this->circularDependencyDetector->push($serviceDefinition->getId());
64
65
        $definition = $serviceDefinition->getDefinition();
66
67
        $value =
68
            match (true) {
69
                is_callable($definition) => $this->spawnByCallable($definition),
70
                is_string($definition) => $this->spawnByClassConstructor($definition),
71
                default => $definition, // return object as is
72
            };
73
74
        $this->circularDependencyDetector->pop();
75
76
        return $this->serviceObjectFactory->create(
77
            value: $value,
78
            serviceDefinition: $serviceDefinition,
79
        );
80
    }
81
82
    /**
83
     * @psalm-suppress MixedInferredReturnType
84
     * @psalm-suppress MixedReturnStatement
85
     */
86
    private function spawnByCallable(callable $definition): object
87
    {
88
        return $definition($this->container);
89
    }
90
91
    /**
92
     * @param class-string $definition
93
     *
94
     * @throws ContainerExceptionInterface
95
     * @throws NotFoundExceptionInterface
96
     * @throws ReflectionException
97
     */
98
    private function spawnByClassConstructor(string $definition): object
99
    {
100
        return match (true) {
101
            class_exists($definition) => $this->createInstanceByReflection($definition),
102
            default => throw new ClassDoesNotExist(
103
                sprintf('Class does not exist: %s', $definition)
104
            ),
105
        };
106
    }
107
108
    /**
109
     * @param class-string $class
110
     *
111
     * @throws ContainerExceptionInterface
112
     * @throws NotFoundExceptionInterface
113
     * @throws ReflectionException
114
     */
115
    private function createInstanceByReflection(string $class): object
116
    {
117
        $reflection = new ReflectionClass($class);
118
119
        $constructorParameters = $reflection->getConstructor()?->getParameters();
120
        if ($constructorParameters) {
121
            $parameters = [];
122
            foreach ($constructorParameters as $parameter) {
123
                $name = $parameter->getName();
124
                /** @var ReflectionNamedType|null $type */
125
                $type = $parameter->getType();
126
                if ($type === null) {
127
                    throw new UnableToExtractType('Unable to extract type for parameter name: $' . $name);
128
                }
129
                if ($this->needsService($type)) {
130
                    $parameters[$name] = $this->getServiceFromContainer($type->getName());
131
                }
132
            }
133
            /** @psalm-suppress MixedMethodCall */
134
            return new $class(...$parameters);
135
        }
136
137
        try {
138
            /** @psalm-suppress MixedMethodCall */
139
            return new $class();
140
        } catch (Throwable $e) {
141
            throw new UnableToCreateInstance('Unable to create instance of ' . $class, previous: $e);
142
        }
143
    }
144
145
    /**
146
     * @throws ContainerExceptionInterface
147
     */
148
    private function needsService(mixed $type): bool
149
    {
150
        return match (true) {
151
            // assumes that all non-builtin types are services
152
            $type instanceof ReflectionNamedType => !$type->isBuiltin(),
153
            default => throw new UnableToExtractType(
154
                sprintf(
155
                    'Only %s is supported.',
156
                    ReflectionNamedType::class,
157
                )
158
            ),
159
        };
160
    }
161
162
    /**
163
     * @psalm-suppress MixedInferredReturnType
164
     * @psalm-suppress MixedReturnStatement
165
     *
166
     * @throws ContainerExceptionInterface
167
     * @throws NotFoundExceptionInterface
168
     */
169
    private function getServiceFromContainer(string $id): object
170
    {
171
        return $this->container->get($id);
172
    }
173
}
174