Passed
Push — master ( 0e61d9...3c9296 )
by Raffael
02:20
created

Container   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 451
Duplicated Lines 0 %

Test Coverage

Coverage 81.88%

Importance

Changes 0
Metric Value
dl 0
loc 451
ccs 113
cts 138
cp 0.8188
rs 7.4757
c 0
b 0
f 0
wmc 53

18 Methods

Rating   Name   Duplication   Size   Complexity  
C autoWireMethod() 0 29 8
B parseParam() 0 24 6
A wireReference() 0 14 3
A addStaticService() 0 11 2
A __construct() 0 5 1
A setParentService() 0 5 1
A getParent() 0 3 1
A get() 0 6 2
B getRealInstance() 0 24 4
A storeService() 0 12 3
A has() 0 3 1
B autoWireClass() 0 24 4
A add() 0 9 2
C resolve() 0 27 8
A findService() 0 14 3
A getProxyInstance() 0 10 1
A createInstance() 0 7 2
A getConfig() 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
/**
6
 * Micro\Container
7
 *
8
 * @copyright   Copryright (c) 2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro\Container;
13
14
use Closure;
15
use Psr\Container\ContainerInterface;
16
use ReflectionClass;
17
use ReflectionMethod;
18
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
19
20
class Container implements ContainerInterface
21
{
22
    /**
23
     * Config.
24
     *
25
     * @var Config
26
     */
27
    protected $config;
28
29
    /**
30
     * Service registry.
31
     *
32
     * @var array
33
     */
34
    protected $service = [];
35
36
    /**
37
     * Registered but not initialized service registry.
38
     *
39
     * @var array
40
     */
41
    protected $registry = [];
42
43
    /**
44
     * Parent container.
45
     *
46
     * @var ContainerInterface
47
     */
48
    protected $parent;
49
50
    /**
51
     * Children container.
52
     *
53
     * @var ContainerInterface[]
54
     */
55
    protected $children = [];
56
57
    /**
58
     * Parent service.
59
     *
60
     * @var mixed
61
     */
62
    protected $parent_service;
63
64
    /**
65
     * Create container.
66
     *
67
     * @param iterable           $config
68
     * @param ContainerInterface $parent
69
     */
70 37
    public function __construct(Iterable $config = [], ?ContainerInterface $parent = null)
71
    {
72 37
        $this->config = new Config($config, $this);
73 37
        $this->parent = $parent;
74 37
        $this->service[ContainerInterface::class] = $this;
75 37
    }
76
77
    /**
78
     * Get service.
79
     *
80
     * @param string $name
81
     *
82
     * @return mixed
83
     */
84 28
    public function get($name)
85
    {
86
        try {
87 28
            return $this->resolve($name);
88 11
        } catch (Exception\ServiceNotFound $e) {
89 8
            return $this->autoWireClass($name);
90
        }
91
    }
92
93
    /**
94
     * Get parent container.
95
     *
96
     * @return ContainerInterface
97
     */
98 28
    public function getParent(): ?ContainerInterface
99
    {
100 28
        return $this->parent;
101
    }
102
103
    /**
104
     * Add service.
105
     *
106
     * @param string $name
107
     * @param mixed  $service
108
     *
109
     * @return Container
110
     */
111 3
    public function add(string $name, $service): self
112
    {
113 3
        if ($this->has($name)) {
114 1
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
115
        }
116
117 3
        $this->registry[$name] = $service;
118
119 3
        return $this;
120
    }
121
122
    /**
123
     * Check if service is registered.
124
     *
125
     * @param mixed $name
126
     *
127
     * @return bool
128
     */
129 29
    public function has($name): bool
130
    {
131 29
        return isset($this->service[$name]);
132
    }
133
134
    /**
135
     * Set parent service on container
136
     * (Used internally, there is no point to call this method directly).
137
     *
138
     * @param mixed $service
139
     *
140
     * @return ContainerInterface
141
     */
142 1
    public function setParentService($service): ContainerInterface
143
    {
144 1
        $this->parent_service = $service;
145
146 1
        return $this;
147
    }
148
149
    /**
150
     * Get config.
151
     *
152
     * @return Config
153
     */
154 1
    public function getConfig(): Config
155
    {
156 1
        return $this->config;
157
    }
158
159
    /**
160
     * Resolve service.
161
     *
162
     * @param string $name
163
     *
164
     * @return mixed
165
     */
166 28
    public function resolve(string $name)
167
    {
168 28
        if ($this->has($name)) {
169 2
            return $this->service[$name];
170
        }
171
172 28
        if (isset($this->registry[$name])) {
173 3
            return $this->addStaticService($name);
174
        }
175
176 25
        if ($this->config->has($name)) {
177 22
            return $this->autoWireClass($name);
178
        }
179
180 8
        if (null !== $this->parent_service) {
181
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
182
183
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
184
                return $this->parent_service;
185
            }
186
        }
187
188 8
        if (null !== $this->parent) {
189
            return $this->parent->lookupService($name);
0 ignored issues
show
Bug introduced by
The method lookupService() does not exist on Psr\Container\ContainerInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

189
            return $this->parent->/** @scrutinizer ignore-call */ lookupService($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
190
        }
191
192 8
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
193
    }
194
195
    /**
196
     * Check for static injections.
197
     *
198
     * @param string $name
199
     *
200
     * @return mixed
201
     */
202 3
    protected function addStaticService(string $name)
203
    {
204 3
        if ($this->registry[$name] instanceof Closure) {
205 1
            $this->service[$name] = $this->registry[$name]->call($this);
206
        } else {
207 2
            $this->service[$name] = $this->registry[$name];
208
        }
209
210 3
        unset($this->registry[$name]);
211
212 3
        return $this->service[$name];
213
    }
214
215
    /**
216
     * Auto wire.
217
     *
218
     * @param string $name
219
     * @param array  $config
220
     * @param array  $parents
221
     *
222
     * @return mixed
223
     */
224 25
    protected function autoWireClass(string $name)
225
    {
226 25
        $config = $this->config->get($name);
227 24
        $class = $config['use'];
228
229 24
        if (preg_match('#^\{(.*)\}$#', $class, $match)) {
230
            return $this->wireReference($name, $match[1], $config);
231
        }
232
233
        try {
234 24
            $reflection = new ReflectionClass($class);
235 1
        } catch (\Exception $e) {
236 1
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
237
        }
238
239 23
        $constructor = $reflection->getConstructor();
240
241 23
        if (null === $constructor) {
242 3
            return $this->storeService($name, $config, new $class());
243
        }
244
245 20
        $args = $this->autoWireMethod($name, $constructor, $config);
246
247 19
        return $this->createInstance($name, $reflection, $args, $config);
248
    }
249
250
    /**
251
     * Wire named referenced service.
252
     *
253
     * @param string $name
254
     * @param string $refrence
255
     * @param array  $config
256
     *
257
     * @return mixed
258
     */
259
    protected function wireReference(string $name, string $reference, array $config)
260
    {
261
        $service = $this->get($reference);
262
263
        if (isset($config['selects'])) {
264
            $reflection = new ReflectionClass(get_class($service));
265
266
            foreach ($config['selects'] as $select) {
267
                $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
268
                $service = call_user_func_array([&$service, $select['method']], $args);
269
            }
270
        }
271
272
        return $this->storeService($name, $config, $service);
273
    }
274
275
    /**
276
     * Store service.
277
     *
278
     * @param param string $name
279
     * @param array        $config
280
     * @param mixed        $service
281
     *
282
     * @return mixed
283
     */
284 21
    protected function storeService(string $name, array $config, $service)
285
    {
286 21
        if (true === $config['singleton']) {
287 2
            return $service;
288
        }
289 20
        $this->service[$name] = $service;
290
291 20
        if (isset($this->children[$name])) {
292 1
            $this->children[$name]->setParentService($service);
293
        }
294
295 20
        return $service;
296
    }
297
298
    /**
299
     * Get instance (virtual or real instance).
300
     *
301
     * @param string          $name
302
     * @param ReflectionClass $class
303
     * @param array           $arguments
304
     * @param array           $config
305
     *
306
     * @return mixed
307
     */
308 19
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
309
    {
310 19
        if (true === $config['lazy']) {
311 2
            return $this->getProxyInstance($name, $class, $arguments, $config);
312
        }
313
314 17
        return $this->getRealInstance($name, $class, $arguments, $config);
315
    }
316
317
    /**
318
     * Create proxy instance.
319
     *
320
     * @param string          $name
321
     * @param ReflectionClass $class
322
     * @param array           $arguments
323
     * @param array           $config
324
     *
325
     * @return mixed
326
     */
327 2
    protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config)
328
    {
329 2
        $factory = new LazyLoadingValueHolderFactory();
330 2
        $that = $this;
331
332 2
        return $factory->createProxy(
333 2
            $class->getName(),
334 2
            function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) {
335 1
                $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config);
336 1
                $initializer = null;
337 2
            }
338
        );
339
    }
340
341
    /**
342
     * Create real instance.
343
     *
344
     * @param string          $name
345
     * @param ReflectionClass $class
346
     * @param array           $arguments
347
     * @param array           $config
348
     *
349
     * @return mixed
350
     */
351 18
    protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config)
352
    {
353 18
        $instance = $class->newInstanceArgs($arguments);
354 18
        $this->storeService($name, $config, $instance);
355 18
        $config = $this->config->get($name);
356
357 18
        foreach ($config['calls'] as $call) {
358 3
            if (!isset($call['method'])) {
359 1
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
360
            }
361
362 2
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
363
364
            try {
365 2
                $method = $class->getMethod($call['method']);
366
            } catch (\ReflectionException $e) {
367
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
368
            }
369
370 2
            $arguments = $this->autoWireMethod($name, $method, $call);
371 2
            call_user_func_array([&$instance, $call['method']], $arguments);
372
        }
373
374 17
        return $instance;
375
    }
376
377
    /**
378
     * Autowire method.
379
     *
380
     * @param string           $name
381
     * @param ReflectionMethod $method
382
     * @param array            $config
383
     *
384
     * @return array
385
     */
386 20
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
387
    {
388 20
        $params = $method->getParameters();
389 20
        $args = [];
390
391 20
        foreach ($params as $param) {
392 20
            $type = $param->getClass();
393 20
            $param_name = $param->getName();
394
395 20
            if (isset($config['arguments'][$param_name])) {
396 20
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
397 8
            } elseif (null !== $type) {
398 5
                $type_class = $type->getName();
399
400 5
                if ($type_class === $name) {
401
                    throw new Exception\InvalidConfiguration('class '.$type_class.' can not depend on itself');
402
                }
403
404 5
                $args[$param_name] = $this->findService($name, $type_class);
405 4
            } elseif ($param->isDefaultValueAvailable()) {
406 4
                $args[$param_name] = $param->getDefaultValue();
407
            } elseif ($param->allowsNull() && $param->hasType()) {
408
                $args[$param_name] = null;
409
            } else {
410 19
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
411
            }
412
        }
413
414 19
        return $args;
415
    }
416
417
    /**
418
     * Parse param value.
419
     *
420
     * @param mixed  $param
421
     * @param string $name
422
     *
423
     * @return mixed
424
     */
425 20
    protected function parseParam($param, string $name)
426
    {
427 20
        if (is_iterable($param)) {
428
            foreach ($param as $key => $value) {
429
                $param[$key] = $this->parseParam($value, $name);
430
            }
431
432
            return $param;
433
        }
434
435 20
        if (is_string($param)) {
436 20
            $param = $this->config->getEnv($param);
437
438 19
            if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) {
439
                return '{'.$matches[1].'}';
440
            }
441 19
            if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) {
442
                return $this->findService($name, $matches[1]);
443
            }
444
445 19
            return $param;
446
        }
447
448
        return $param;
449
    }
450
451
    /**
452
     * Locate service.
453
     *
454
     * @param string $current_service
455
     * @param string $service
456
     */
457 5
    protected function findService(string $current_service, string $service)
458
    {
459 5
        if (isset($this->children[$current_service])) {
460
            return $this->children[$current_service]->get($service);
461
        }
462
463 5
        $config = $this->config->get($current_service);
464 5
        if (isset($config['services'])) {
465 1
            $this->children[$current_service] = new self($config['services'], $this);
466
467 1
            return $this->children[$current_service]->get($service);
468
        }
469
470 4
        return $this->get($service);
471
    }
472
}
473