Passed
Push — master ( 593de6...208710 )
by Alex
37s queued 10s
created

Container::doGet()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 19
rs 9.9
c 0
b 0
f 0
cc 4
nc 4
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Arp\Container;
6
7
use Arp\Container\Exception\ContainerException;
8
use Arp\Container\Exception\NotFoundException;
9
use Arp\Container\Factory\ObjectFactory;
10
use Arp\Container\Factory\ServiceFactoryInterface;
11
use Arp\Container\Provider\Exception\ServiceProviderException;
12
use Arp\Container\Provider\ServiceProviderInterface;
13
use Psr\Container\ContainerExceptionInterface;
14
use Psr\Container\ContainerInterface;
15
use Psr\Container\NotFoundExceptionInterface;
16
17
/**
18
 * @author  Alex Patterson <[email protected]>
19
 * @package Arp\Container
20
 */
21
final class Container implements ContainerInterface
22
{
23
    /**
24
     * @var string[]
25
     */
26
    private array $aliases = [];
27
28
    /**
29
     * @var mixed[]
30
     */
31
    private array $services = [];
32
33
    /**
34
     * @var callable[]
35
     */
36
    private array $factories = [];
37
38
    /**
39
     * @var string[]
40
     */
41
    private array $factoryClasses = [];
42
43
    /**
44
     * @param ServiceProviderInterface|null $serviceProvider
45
     *
46
     * @throws ContainerException
47
     */
48
    public function __construct(ServiceProviderInterface $serviceProvider = null)
49
    {
50
        if (null !== $serviceProvider) {
51
            $this->configure($serviceProvider);
52
        }
53
    }
54
55
    /**
56
     * @param string $name Identifier of the entry to look for
57
     *
58
     * @return mixed
59
     *
60
     * @throws ContainerExceptionInterface
61
     * @throws NotFoundExceptionInterface
62
     *
63
     * @noinspection PhpMissingParamTypeInspection
64
     */
65
    public function get($name)
66
    {
67
        return $this->doGet($name);
68
    }
69
70
    /**
71
     * @param string     $name
72
     * @param array|null $arguments
73
     *
74
     * @return mixed
75
     *
76
     * @throws ContainerException
77
     * @throws NotFoundException
78
     */
79
    private function doGet(string $name, array $arguments = null)
80
    {
81
        if (isset($this->services[$name])) {
82
            return $this->services[$name];
83
        }
84
85
        if (isset($this->aliases[$name])) {
86
            return $this->get($this->aliases[$name]);
87
        }
88
89
        $factory = $this->resolveFactory($name);
90
        if (null !== $factory) {
91
            $service = $this->invokeFactory($factory, $name, $arguments);
92
            $this->set($name, $service);
93
            return $service;
94
        }
95
96
        throw new NotFoundException(
97
            sprintf('Service \'%s\' could not be found registered with the container', $name)
98
        );
99
    }
100
101
    /**
102
     * @param string $name
103
     *
104
     * @return bool
105
     *
106
     * @noinspection PhpMissingParamTypeInspection
107
     * @noinspection ReturnTypeCanBeDeclaredInspection
108
     */
109
    public function has($name)
110
    {
111
        return $this->doHas($name);
112
    }
113
114
    /**
115
     * @param string $name
116
     *
117
     * @return bool
118
     */
119
    private function doHas(string $name): bool
120
    {
121
        return isset($this->services[$name])
122
            || isset($this->factories[$name])
123
            || isset($this->aliases[$name])
124
            || isset($this->factoryClasses[$name]);
125
    }
126
127
    /**
128
     * Set a service on the container
129
     *
130
     * @param string $name
131
     * @param mixed  $service
132
     *
133
     * @return $this
134
     */
135
    public function set(string $name, $service): self
136
    {
137
        $this->services[$name] = $service;
138
139
        return $this;
140
    }
141
142
    /**
143
     * Register a callable factory for the container.
144
     *
145
     * @param string   $name    The name of the service to register.
146
     * @param callable $factory The factory callable responsible for creating the service.
147
     *
148
     * @return $this
149
     */
150
    public function setFactory(string $name, callable $factory): self
151
    {
152
        $this->factories[$name] = $factory;
153
154
        return $this;
155
    }
156
157
    /**
158
     * Set the class name of a factory that will create service $name.
159
     *
160
     * @param string      $name         The name of the service to set the factory for.
161
     * @param string      $factoryClass The fully qualified class name of the factory.
162
     * @param string|null $method       The name of the factory method to call.
163
     *
164
     * @return $this
165
     */
166
    public function setFactoryClass(string $name, string $factoryClass, string $method = null): self
167
    {
168
        $this->factoryClasses[$name] = [$factoryClass, $method];
169
170
        return $this;
171
    }
172
173
    /**
174
     * Set an alias for a given service
175
     *
176
     * @param string $alias The name of the alias to set
177
     * @param string $name  The name of the service that
178
     *
179
     * @return $this
180
     *
181
     * @throws ContainerException
182
     */
183
    public function setAlias(string $alias, string $name): self
184
    {
185
        if (!isset($this->services[$name])) {
186
            throw new ContainerException(
187
                sprintf('Unable to configure alias \'%s\' for unknown service \'%s\'', $alias, $name)
188
            );
189
        }
190
191
        if ($alias === $name) {
192
            throw new ContainerException(
193
                sprintf('Unable to configure alias \'%s\' with identical service name \'%s\'', $alias, $name)
194
            );
195
        }
196
197
        $this->aliases[$alias] = $name;
198
199
        return $this;
200
    }
201
202
    /**
203
     * Create a new service with the provided options
204
     *
205
     * @param string $name
206
     * @param array  $arguments
207
     *
208
     * @return mixed
209
     *
210
     * @throws ContainerException
211
     */
212
    public function build(string $name, array $arguments = [])
213
    {
214
        if (isset($this->aliases[$name])) {
215
            return $this->build($this->aliases[$name]);
216
        }
217
218
        $factory = $this->resolveFactory($name);
219
220
        if (null === $factory) {
221
            throw new ContainerException(
222
                sprintf('Unable to build service \'%s\': No valid factory could be found', $name)
223
            );
224
        }
225
226
        return $this->invokeFactory($factory, $name, $arguments);
227
    }
228
229
    /**
230
     * @param callable   $factory
231
     * @param string     $name
232
     * @param array|null $options
233
     *
234
     * @return mixed
235
     *
236
     * @throws ContainerExceptionInterface
237
     */
238
    private function invokeFactory(callable $factory, string $name, array $options = null)
239
    {
240
        try {
241
            return $factory($this, $name, $options);
242
        } catch (ContainerExceptionInterface $e) {
243
            throw $e;
244
        } catch (\Throwable $e) {
245
            throw new ContainerException(
246
                sprintf('The service \'%s\' could not be created: %s', $name, $e->getMessage()),
247
                $e->getCode(),
248
                $e
249
            );
250
        }
251
    }
252
253
    /**
254
     * @param string $name
255
     *
256
     * @return callable|null
257
     *
258
     * @throws ContainerException
259
     */
260
    private function resolveFactory(string $name): ?callable
261
    {
262
        $factory = null;
263
        if (isset($this->factories[$name])) {
264
            $factory = $this->factories[$name];
265
        } elseif (isset($this->factoryClasses[$name][0])) {
266
            $factory = $this->resolveFactoryClass(
267
                $name,
268
                $this->factoryClasses[$name][0],
269
                $this->factoryClasses[$name][0] ?? null
270
            );
271
        } elseif (class_exists($name, true)) {
272
            $factory = $this->createObjectFactory();
273
        }
274
275
        if (null !== $factory && !is_callable($factory)) {
276
            throw new ContainerException(
277
                sprintf('Unable to create service \'%s\': The registered factory is not callable', $name)
278
            );
279
        }
280
281
        return $factory;
282
    }
283
284
    /**
285
     * @return ServiceFactoryInterface
286
     */
287
    private function createObjectFactory(): ServiceFactoryInterface
288
    {
289
        return new ObjectFactory();
290
    }
291
292
    /**
293
     * @param string      $name
294
     * @param string      $factoryClassName
295
     * @param string|null $methodName
296
     *
297
     * @return array
298
     *
299
     * @throws ContainerException
300
     */
301
    private function resolveFactoryClass(string $name, string $factoryClassName, ?string $methodName): array
302
    {
303
        if ($factoryClassName === $name) {
304
            throw new ContainerException(
305
                sprintf(
306
                    'A circular dependency was detected for service \'%s\' and the registered factory \'%s\'',
307
                    $name,
308
                    $factoryClassName
309
                )
310
            );
311
        }
312
313
        if (class_exists($factoryClassName, true) && !$this->has($factoryClassName)) {
314
            $this->setFactory($factoryClassName, $this->createObjectFactory());
315
        }
316
317
        if (!$this->has($factoryClassName)) {
318
            throw new ContainerException(
319
                sprintf(
320
                    'The factory service \'%s\', registered for service \'%s\', is not a valid service or class name',
321
                    $factoryClassName,
322
                    $name
323
                )
324
            );
325
        }
326
327
        $factory = [$this->get($factoryClassName), $methodName ?? '__invoke'];
328
        if (!is_callable($factory)) {
329
            throw new ContainerException(
330
                sprintf(
331
                    'Factory \'%s\' registered for service \'%s\', must be callable',
332
                    $factoryClassName,
333
                    $name
334
                )
335
            );
336
        }
337
338
        return $factory;
339
    }
340
341
    /**
342
     * @param ServiceProviderInterface $serviceProvider
343
     *
344
     * @throws ContainerException
345
     */
346
    public function configure(ServiceProviderInterface $serviceProvider): void
347
    {
348
        try {
349
            $serviceProvider->registerServices($this);
350
        } catch (ServiceProviderException $e) {
351
            throw new ContainerException(
352
                sprintf(
353
                    'Failed to register services using provider \'%s\': %s',
354
                    get_class($serviceProvider),
355
                    $e->getMessage()
356
                ),
357
                $e->getCode(),
358
                $e
359
            );
360
        }
361
    }
362
}
363