Passed
Pull Request — master (#4)
by Alex
01:54
created

Container::resolveFactoryClass()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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