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

Container::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 2
c 1
b 0
f 0
dl 0
loc 4
rs 10
cc 2
nc 2
nop 1
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 NotFoundException
132
     */
133
    public function build(string $name, array $arguments = [])
134
    {
135
        if (isset($this->aliases[$name])) {
136
            return $this->build($this->aliases[$name]);
137
        }
138
139
        $factory = $this->resolveFactory($name);
140
        if (null === $factory) {
141
            throw new NotFoundException(
142
                sprintf('Unable to build service \'%s\': No valid factory could be found', $name)
143
            );
144
        }
145
146
        return $this->invokeFactory($factory, $name, $arguments);
147
    }
148
149
    /**
150
     * @param string $name
151
     *
152
     * @return bool
153
     *
154
     * @noinspection PhpMissingParamTypeInspection
155
     * @noinspection ReturnTypeCanBeDeclaredInspection
156
     */
157
    public function has($name)
158
    {
159
        return $this->doHas($name);
160
    }
161
162
    /**
163
     * @param string $name
164
     *
165
     * @return bool
166
     */
167
    private function doHas(string $name): bool
168
    {
169
        return isset($this->services[$name])
170
            || isset($this->factories[$name])
171
            || isset($this->aliases[$name])
172
            || isset($this->factoryClasses[$name]);
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
     * @return ServiceFactoryInterface
322
     */
323
    private function createObjectFactory(): ServiceFactoryInterface
324
    {
325
        return new ObjectFactory();
326
    }
327
328
    /**
329
     * @param string      $name
330
     * @param string      $factoryClassName
331
     * @param string|null $methodName
332
     *
333
     * @return array
334
     *
335
     * @throws ContainerException
336
     */
337
    private function resolveFactoryClass(string $name, string $factoryClassName, ?string $methodName): array
338
    {
339
        if ($factoryClassName === $name) {
340
            throw new ContainerException(
341
                sprintf('A circular configuration dependency was detected for service \'%s\'', $name)
342
            );
343
        }
344
345
        if (class_exists($factoryClassName, true) && !$this->has($factoryClassName)) {
346
            $this->setFactory($factoryClassName, $this->createObjectFactory());
347
        }
348
349
        if (!$this->has($factoryClassName)) {
350
            throw new ContainerException(
351
                sprintf(
352
                    'The factory service \'%s\', registered for service \'%s\', is not a valid service or class name',
353
                    $factoryClassName,
354
                    $name
355
                )
356
            );
357
        }
358
359
        return [$this->get($factoryClassName), $methodName ?? '__invoke'];
360
    }
361
362
    /**
363
     * @param ServiceProviderInterface $serviceProvider
364
     *
365
     * @throws ContainerException
366
     */
367
    public function configure(ServiceProviderInterface $serviceProvider): void
368
    {
369
        try {
370
            $serviceProvider->registerServices($this);
371
        } catch (ServiceProviderException $e) {
372
            throw new ContainerException(
373
                sprintf(
374
                    'Failed to register services using provider \'%s\': %s',
375
                    get_class($serviceProvider),
376
                    $e->getMessage()
377
                ),
378
                $e->getCode(),
379
                $e
380
            );
381
        }
382
    }
383
}
384