Passed
Push — master ( 208710...261e75 )
by Alex
35s queued 10s
created

Container::resolveFactoryClass()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 12
c 0
b 0
f 0
dl 0
loc 23
rs 9.5555
cc 5
nc 5
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
18
/**
19
 * @author  Alex Patterson <[email protected]>
20
 * @package Arp\Container
21
 */
22
final class Container implements ContainerInterface
23
{
24
    /**
25
     * @var string[]
26
     */
27
    private array $aliases = [];
28
29
    /**
30
     * @var mixed[]
31
     */
32
    private array $services = [];
33
34
    /**
35
     * @var callable[]
36
     */
37
    private array $factories = [];
38
39
    /**
40
     * @var string[]
41
     */
42
    private array $factoryClasses = [];
43
44
    /**
45
     * @var array
46
     */
47
    private array $requested = [];
48
49
    /**
50
     * @param ServiceProviderInterface|null $serviceProvider
51
     *
52
     * @throws ContainerException
53
     */
54
    public function __construct(ServiceProviderInterface $serviceProvider = null)
55
    {
56
        if (null !== $serviceProvider) {
57
            $this->configure($serviceProvider);
58
        }
59
    }
60
61
    /**
62
     * @param string $name Identifier of the entry to look for
63
     *
64
     * @return mixed
65
     *
66
     * @throws CircularDependencyException
67
     * @throws ContainerException
68
     * @throws NotFoundException
69
     *
70
     * @noinspection PhpMissingParamTypeInspection
71
     */
72
    public function get($name)
73
    {
74
        return $this->doGet($name);
75
    }
76
77
    /**
78
     * @param string     $name
79
     * @param array|null $arguments
80
     *
81
     * @return mixed
82
     *
83
     * @throws ContainerException
84
     * @throws NotFoundException
85
     * @throws CircularDependencyException
86
     */
87
    private function doGet(string $name, array $arguments = null)
88
    {
89
        if (isset($this->aliases[$name])) {
90
            return $this->doGet($this->aliases[$name]);
91
        }
92
93
        if (isset($this->services[$name])) {
94
            $service = $this->services[$name];
95
        } elseif (isset($this->requested[$name])) {
96
            throw new CircularDependencyException(
97
                sprintf(
98
                    'A circular dependency has been detected for service \'%s\'. The dependency graph includes %s',
99
                    $name,
100
                    implode(',', array_keys($this->requested))
101
                )
102
            );
103
        } else {
104
            $factory = $this->resolveFactory($name);
105
            if (null !== $factory) {
106
                $this->requested[$name] = true;
107
                $service = $this->invokeFactory($factory, $name, $arguments);
108
                $this->set($name, $service);
109
                unset($this->requested[$name]);
110
            }
111
        }
112
113
        if (isset($service)) {
114
            return $service;
115
        }
116
117
        throw new NotFoundException(
118
            sprintf('Service \'%s\' could not be found registered with the container', $name)
119
        );
120
    }
121
122
    /**
123
     * Create a new service with the provided options. Services required via build will always have a new instance
124
     * of the service returned. Only services registered with factories can be built.
125
     *
126
     * @param string $name
127
     * @param array  $arguments
128
     *
129
     * @return mixed
130
     *
131
     * @throws ContainerException
132
     * @throws NotFoundException
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 NotFoundException(
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 InvalidArgumentException
248
     */
249
    public function setAlias(string $alias, string $name): self
250
    {
251
        if (!isset($this->services[$name]) && !isset($this->factories[$name]) && !isset($this->factoryClasses[$name])) {
252
            throw new InvalidArgumentException(
253
                sprintf('Unable to configure alias \'%s\' for unknown service \'%s\'', $alias, $name)
254
            );
255
        }
256
257
        if ($alias === $name) {
258
            throw new InvalidArgumentException(
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(sprintf('Factory registered for service \'%s\', must be callable', $name));
316
        }
317
318
        return $factory;
319
    }
320
321
    /**
322
     * @param string      $name
323
     * @param string      $factoryClassName
324
     * @param string|null $methodName
325
     *
326
     * @return array
327
     *
328
     * @throws ContainerException
329
     */
330
    private function resolveFactoryClass(string $name, string $factoryClassName, ?string $methodName): array
331
    {
332
        if ($factoryClassName === $name) {
333
            throw new ContainerException(
334
                sprintf('A circular configuration dependency was detected for service \'%s\'', $name)
335
            );
336
        }
337
338
        if (class_exists($factoryClassName, true) && !$this->has($factoryClassName)) {
339
            $this->setFactory($factoryClassName, $this->createObjectFactory());
340
        }
341
342
        if (!$this->has($factoryClassName)) {
343
            throw new ContainerException(
344
                sprintf(
345
                    'The factory service \'%s\', registered for service \'%s\', is not a valid service or class name',
346
                    $factoryClassName,
347
                    $name
348
                )
349
            );
350
        }
351
352
        return [$this->get($factoryClassName), $methodName ?? '__invoke'];
353
    }
354
355
    /**
356
     * @param ServiceProviderInterface $serviceProvider
357
     *
358
     * @throws ContainerException
359
     */
360
    public function configure(ServiceProviderInterface $serviceProvider): void
361
    {
362
        try {
363
            $serviceProvider->registerServices($this);
364
        } catch (ServiceProviderException $e) {
365
            throw new ContainerException(
366
                sprintf(
367
                    'Failed to register services using provider \'%s\': %s',
368
                    get_class($serviceProvider),
369
                    $e->getMessage()
370
                ),
371
                $e->getCode(),
372
                $e
373
            );
374
        }
375
    }
376
377
    /**
378
     * @return ServiceFactoryInterface
379
     */
380
    private function createObjectFactory(): ServiceFactoryInterface
381
    {
382
        return new ObjectFactory();
383
    }
384
}
385