Container::setFactory()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

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