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

Container   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 369
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 44
eloc 118
c 1
b 0
f 0
dl 0
loc 369
rs 8.8798

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setFactoryClass() 0 5 1
A build() 0 14 3
A doHas() 0 6 4
B doGet() 0 32 6
A configure() 0 13 2
A resolveFactory() 0 22 6
A resolveFactoryClass() 0 34 6
A createObjectFactory() 0 3 1
A setFactory() 0 20 4
A has() 0 3 1
A invokeFactory() 0 11 3
A set() 0 5 1
A __construct() 0 4 2
A setAlias() 0 17 3
A get() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Container often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Container, and based on these observations, apply Extract Interface, too.

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\InvalidArgumentException;
9
use Arp\Container\Exception\NotFoundException;
10
use Arp\Container\Factory\ObjectFactory;
11
use Arp\Container\Factory\ServiceFactoryInterface;
12
use Arp\Container\Provider\Exception\ServiceProviderException;
13
use Arp\Container\Provider\ServiceProviderInterface;
14
use Psr\Container\ContainerExceptionInterface;
15
use Psr\Container\ContainerInterface;
16
use Psr\Container\NotFoundExceptionInterface;
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 ContainerExceptionInterface
67
     * @throws NotFoundExceptionInterface
68
     *
69
     * @noinspection PhpMissingParamTypeInspection
70
     */
71
    public function get($name)
72
    {
73
        return $this->doGet($name);
74
    }
75
76
    /**
77
     * @param string     $name
78
     * @param array|null $arguments
79
     *
80
     * @return mixed
81
     *
82
     * @throws ContainerException
83
     * @throws NotFoundException
84
     */
85
    private function doGet(string $name, array $arguments = null)
86
    {
87
        if (isset($this->aliases[$name])) {
88
            return $this->doGet($this->aliases[$name]);
89
        }
90
91
        if (isset($this->services[$name])) {
92
            $service = $this->services[$name];
93
        } elseif (isset($this->requested[$name])) {
94
            throw new ContainerException(
95
                sprintf(
96
                    'A circular dependency has been detected for service \'%s\'. The dependency graph includes %s',
97
                    $name,
98
                    implode(',', array_keys($this->requested))
99
                )
100
            );
101
        } else {
102
            $factory = $this->resolveFactory($name);
103
            if (null !== $factory) {
104
                $this->requested[$name] = true;
105
                $service = $this->invokeFactory($factory, $name, $arguments);
106
                $this->set($name, $service);
107
                unset($this->requested[$name]);
108
            }
109
        }
110
111
        if (isset($service)) {
112
            return $service;
113
        }
114
115
        throw new NotFoundException(
116
            sprintf('Service \'%s\' could not be found registered with the container', $name)
117
        );
118
    }
119
120
    /**
121
     * Create a new service with the provided options. Services required via build will always have a new instance
122
     * of the service returned. Only services registered with factories can be built.
123
     *
124
     * @param string $name
125
     * @param array  $arguments
126
     *
127
     * @return mixed
128
     *
129
     * @throws ContainerException
130
     */
131
    public function build(string $name, array $arguments = [])
132
    {
133
        if (isset($this->aliases[$name])) {
134
            return $this->build($this->aliases[$name]);
135
        }
136
137
        $factory = $this->resolveFactory($name);
138
        if (null === $factory) {
139
            throw new ContainerException(
140
                sprintf('Unable to build service \'%s\': No valid factory could be found', $name)
141
            );
142
        }
143
144
        return $this->invokeFactory($factory, $name, $arguments);
145
    }
146
147
    /**
148
     * @param string $name
149
     *
150
     * @return bool
151
     *
152
     * @noinspection PhpMissingParamTypeInspection
153
     * @noinspection ReturnTypeCanBeDeclaredInspection
154
     */
155
    public function has($name)
156
    {
157
        return $this->doHas($name);
158
    }
159
160
    /**
161
     * @param string $name
162
     *
163
     * @return bool
164
     */
165
    private function doHas(string $name): bool
166
    {
167
        return isset($this->services[$name])
168
            || isset($this->factories[$name])
169
            || isset($this->aliases[$name])
170
            || isset($this->factoryClasses[$name]);
171
    }
172
173
    /**
174
     * Set a service on the container
175
     *
176
     * @param string $name
177
     * @param mixed  $service
178
     *
179
     * @return $this
180
     */
181
    public function set(string $name, $service): self
182
    {
183
        $this->services[$name] = $service;
184
185
        return $this;
186
    }
187
188
    /**
189
     * Register a factory for the container.
190
     *
191
     * @param string          $name    The name of the service to register.
192
     * @param string|callable $factory The factory callable responsible for creating the service.
193
     *
194
     * @return $this
195
     *
196
     * @throws InvalidArgumentException If the provided factory is not string or callable
197
     */
198
    public function setFactory(string $name, $factory): self
199
    {
200
        if (is_string($factory)) {
201
            return $this->setFactoryClass($name, $factory);
202
        }
203
204
        if (!is_callable($factory)) {
205
            throw new InvalidArgumentException(
206
                sprintf(
207
                    'The \'factory\' argument must be of type \'string\' or \'callable\';'
208
                    . '\'%s\' provided for service \'%s\'',
209
                    is_object($factory) ? get_class($factory) : gettype($factory),
210
                    $name
211
                )
212
            );
213
        }
214
215
        $this->factories[$name] = $factory;
216
217
        return $this;
218
    }
219
220
    /**
221
     * Set the class name of a factory that will create service $name.
222
     *
223
     * @param string      $name         The name of the service to set the factory for.
224
     * @param string      $factoryClass The fully qualified class name of the factory.
225
     * @param string|null $method       The name of the factory method to call.
226
     *
227
     * @return $this
228
     */
229
    public function setFactoryClass(string $name, string $factoryClass, string $method = null): self
230
    {
231
        $this->factoryClasses[$name] = [$factoryClass, $method];
232
233
        return $this;
234
    }
235
236
    /**
237
     * Set an alias for a given service
238
     *
239
     * @param string $alias The name of the alias to set
240
     * @param string $name  The name of the service that
241
     *
242
     * @return $this
243
     *
244
     * @throws ContainerException
245
     */
246
    public function setAlias(string $alias, string $name): self
247
    {
248
        if (!isset($this->services[$name])) {
249
            throw new ContainerException(
250
                sprintf('Unable to configure alias \'%s\' for unknown service \'%s\'', $alias, $name)
251
            );
252
        }
253
254
        if ($alias === $name) {
255
            throw new ContainerException(
256
                sprintf('Unable to configure alias \'%s\' with identical service name \'%s\'', $alias, $name)
257
            );
258
        }
259
260
        $this->aliases[$alias] = $name;
261
262
        return $this;
263
    }
264
265
    /**
266
     * @param callable   $factory
267
     * @param string     $name
268
     * @param array|null $options
269
     *
270
     * @return mixed
271
     *
272
     * @throws ContainerExceptionInterface
273
     */
274
    private function invokeFactory(callable $factory, string $name, array $options = null)
275
    {
276
        try {
277
            return $factory($this, $name, $options);
278
        } catch (ContainerExceptionInterface $e) {
279
            throw $e;
280
        } catch (\Throwable $e) {
281
            throw new ContainerException(
282
                sprintf('The service \'%s\' could not be created: %s', $name, $e->getMessage()),
283
                $e->getCode(),
284
                $e
285
            );
286
        }
287
    }
288
289
    /**
290
     * @param string $name
291
     *
292
     * @return callable|null
293
     *
294
     * @throws ContainerException
295
     */
296
    private function resolveFactory(string $name): ?callable
297
    {
298
        $factory = null;
299
        if (isset($this->factories[$name])) {
300
            $factory = $this->factories[$name];
301
        } elseif (isset($this->factoryClasses[$name][0])) {
302
            $factory = $this->resolveFactoryClass(
303
                $name,
304
                $this->factoryClasses[$name][0],
305
                $this->factoryClasses[$name][0] ?? null
306
            );
307
        } elseif (class_exists($name, true)) {
308
            $factory = $this->createObjectFactory();
309
        }
310
311
        if (null !== $factory && !is_callable($factory)) {
312
            throw new ContainerException(
313
                sprintf('Unable to create service \'%s\': The registered factory is not callable', $name)
314
            );
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
        $factory = [$this->get($factoryClassName), $methodName ?? '__invoke'];
360
        if (!is_callable($factory)) {
361
            throw new ContainerException(
362
                sprintf(
363
                    'Factory \'%s\' registered for service \'%s\', must be callable',
364
                    $factoryClassName,
365
                    $name
366
                )
367
            );
368
        }
369
370
        return $factory;
371
    }
372
373
    /**
374
     * @param ServiceProviderInterface $serviceProvider
375
     *
376
     * @throws ContainerException
377
     */
378
    public function configure(ServiceProviderInterface $serviceProvider): void
379
    {
380
        try {
381
            $serviceProvider->registerServices($this);
382
        } catch (ServiceProviderException $e) {
383
            throw new ContainerException(
384
                sprintf(
385
                    'Failed to register services using provider \'%s\': %s',
386
                    get_class($serviceProvider),
387
                    $e->getMessage()
388
                ),
389
                $e->getCode(),
390
                $e
391
            );
392
        }
393
    }
394
}
395