RuntimeContainer   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 397
Duplicated Lines 0 %

Test Coverage

Coverage 97.58%

Importance

Changes 3
Bugs 1 Features 0
Metric Value
wmc 63
eloc 156
c 3
b 1
f 0
dl 0
loc 397
ccs 161
cts 165
cp 0.9758
rs 3.36

18 Methods

Rating   Name   Duplication   Size   Complexity  
A storeService() 0 12 3
A getProxyInstance() 0 10 1
A getConfig() 0 3 1
A getParent() 0 3 1
A setParentService() 0 5 1
A wrapService() 0 12 2
A createInstance() 0 7 2
B resolve() 0 23 7
A __construct() 0 5 1
A getRealInstance() 0 6 1
B prepareService() 0 41 10
A autoWireClass() 0 32 5
A wireReference() 0 8 1
A get() 0 6 2
A resolveServiceArgument() 0 16 5
B autoWireMethod() 0 37 9
A traverseTree() 0 14 3
B parseParam() 0 28 8

How to fix   Complexity   

Complex Class

Complex classes like RuntimeContainer 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 RuntimeContainer, 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-2019 gyselroth GmbH (https://gyselroth.com)
9
 * @license     MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro\Container;
13
14
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
15
use Psr\Container\ContainerInterface;
16
use ReflectionClass;
17
use ReflectionMethod;
18
use ReflectionParameter;
19
use RuntimeException;
20
21
class RuntimeContainer
22
{
23
    /**
24
     * Config.
25
     *
26
     * @var Config
27
     */
28
    protected $config;
29
30
    /**
31
     * Service registry.
32
     *
33
     * @var array
34
     */
35
    protected $service = [];
36
37
    /**
38
     * Parent container.
39
     *
40
     * @var ContainerInterface|RuntimeContainer
41
     */
42
    protected $parent;
43
44
    /**
45
     * Children container.
46
     *
47
     * @var ContainerInterface[]
48
     */
49
    protected $children = [];
50
51
    /**
52
     * Parent service.
53
     *
54
     * @var mixed
55
     */
56
    protected $parent_service;
57
58
    /**
59
     * Create container.
60
     */
61 64
    public function __construct(iterable $config, $parent, ContainerInterface $interface)
62
    {
63 64
        $this->config = new Config($config, $this);
64 64
        $this->parent = $parent;
65 64
        $this->service[ContainerInterface::class] = $interface;
66 64
    }
67
68
    /**
69
     * Get parent container.
70
     */
71 58
    public function getParent()
72
    {
73 58
        return $this->parent;
74
    }
75
76
    /**
77
     * Set parent service on container.
78
     */
79 2
    public function setParentService($service)
80
    {
81 2
        $this->parent_service = $service;
82
83 2
        return $this;
84
    }
85
86
    /**
87
     * Get config.
88
     */
89 2
    public function getConfig(): Config
90
    {
91 2
        return $this->config;
92
    }
93
94
    /**
95
     * Get service.
96
     */
97 64
    public function get(string $name, ?array $parameters = null)
98
    {
99
        try {
100 64
            return $this->resolve($name, $parameters);
101 20
        } catch (Exception\ServiceNotFound $e) {
102 16
            return $this->wrapService($name, $parameters);
103
        }
104
    }
105
106
    /**
107
     * Resolve service.
108
     */
109 64
    public function resolve(string $name, ?array $parameters = null)
110
    {
111 64
        if (isset($this->service[$name])) {
112 7
            return $this->service[$name];
113
        }
114
115 62
        if ($this->config->has($name)) {
116 54
            return $this->wrapService($name, $parameters);
117
        }
118
119 17
        if (null !== $this->parent_service) {
120 1
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
121
122 1
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
123
                return $this->parent_service;
124
            }
125
        }
126
127 17
        if (null !== $this->parent) {
128 1
            return $this->parent->resolve($name, $parameters);
0 ignored issues
show
Bug introduced by
The method resolve() 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

128
            return $this->parent->/** @scrutinizer ignore-call */ resolve($name, $parameters);

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...
129
        }
130
131 16
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
132
    }
133
134
    /**
135
     * Store service.
136
     */
137 52
    protected function storeService(string $name, array $config, $service)
138
    {
139 52
        if (false === $config['singleton']) {
140 6
            return $service;
141
        }
142 46
        $this->service[$name] = $service;
143
144 46
        if (isset($this->children[$name])) {
145 2
            $this->children[$name]->setParentService($service);
0 ignored issues
show
Bug introduced by
The method setParentService() 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

145
            $this->children[$name]->/** @scrutinizer ignore-call */ 
146
                                    setParentService($service);

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...
146
        }
147
148 46
        return $service;
149
    }
150
151
    /**
152
     * Wrap resolved service in callable if enabled.
153
     */
154 62
    protected function wrapService(string $name, ?array $parameters = null)
155
    {
156 62
        $config = $this->config->get($name);
157 59
        if (true === $config['wrap']) {
158 1
            $that = $this;
159
160 1
            return function () use ($that, $name, $parameters) {
161 1
                return $that->autoWireClass($name, $parameters);
162 1
            };
163
        }
164
165 58
        return $this->autoWireClass($name, $parameters);
166
    }
167
168
    /**
169
     * Auto wire.
170
     */
171 59
    protected function autoWireClass(string $name, ?array $parameters = null)
172
    {
173 59
        $config = $this->config->get($name);
174 59
        $class = $config['use'];
175
176 59
        if (null !== $parameters) {
177 3
            $config['singleton'] = false;
178
        }
179
180 59
        if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) {
181 1
            return $this->wireReference($name, $match[1], $config);
182
        }
183
184 59
        $reflection = new ReflectionClass($class);
185
186 59
        if (isset($config['factory'])) {
187 3
            $factory = $reflection->getMethod($config['factory']);
188 3
            $args = $this->autoWireMethod($name, $factory, $config, $parameters);
189 3
            $instance = call_user_func_array([$class, $config['factory']], $args);
190
191 3
            return $this->prepareService($name, $instance, $reflection, $config);
192
        }
193
194 56
        $constructor = $reflection->getConstructor();
195
196 56
        if (null === $constructor) {
197 8
            return $this->createInstance($name, $reflection, [], $config);
198
        }
199
200 48
        $args = $this->autoWireMethod($name, $constructor, $config, $parameters);
201
202 42
        return $this->createInstance($name, $reflection, $args, $config);
203
    }
204
205
    /**
206
     * Wire named referenced service.
207
     */
208 1
    protected function wireReference(string $name, string $reference, array $config)
209
    {
210 1
        $service = $this->get($reference);
211 1
        $reflection = new ReflectionClass(get_class($service));
212 1
        $config = $this->config->get($name);
213 1
        $service = $this->prepareService($name, $service, $reflection, $config);
214
215 1
        return $service;
216
    }
217
218
    /**
219
     * Get instance (virtual or real instance).
220
     */
221 50
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
222
    {
223 50
        if (true === $config['lazy']) {
224 2
            return $this->getProxyInstance($name, $class, $arguments, $config);
225
        }
226
227 48
        return $this->getRealInstance($name, $class, $arguments, $config);
228
    }
229
230
    /**
231
     * Create proxy instance.
232
     */
233 2
    protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config)
234
    {
235 2
        $factory = new LazyLoadingValueHolderFactory();
236 2
        $that = $this;
237
238 2
        return $factory->createProxy(
239 2
            $class->getName(),
240 2
            function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) {
241 1
                $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config);
242 1
                $initializer = null;
243 2
            }
244
        );
245
    }
246
247
    /**
248
     * Create real instance.
249
     */
250 49
    protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config)
251
    {
252 49
        $instance = $class->newInstanceArgs($arguments);
253 49
        $instance = $this->prepareService($name, $instance, $class, $config);
254
255 49
        return $instance;
256
    }
257
258
    /**
259
     * Prepare service (execute sub selects and excute setter injections).
260
     */
261 52
    protected function prepareService(string $name, $service, ReflectionClass $class, array $config)
262
    {
263 52
        $this->storeService($name, $config, $service);
264
265 52
        foreach ($config['calls'] as $call) {
266 15
            if (!is_array($call)) {
267 1
                continue;
268
            }
269
270 15
            if (!isset($call['method'])) {
271
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
272
            }
273
274 15
            $arguments = [];
275 15
            $result = null;
276
277
            try {
278 15
                $method = $class->getMethod($call['method']);
279
            } catch (\ReflectionException $e) {
280
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
281
            }
282
283 15
            if (isset($call['batch']) && is_array($call['batch'])) {
284 1
                foreach ($call['batch'] as $sub) {
285 1
                    $args = array_combine($call['arguments'], $sub);
286 1
                    $arguments = $this->autoWireMethod($name, $method, $call, $args);
0 ignored issues
show
Bug introduced by
It seems like $args can also be of type false; however, parameter $parameters of Micro\Container\RuntimeContainer::autoWireMethod() does only seem to accept array|null, maybe add an additional type check? ( Ignorable by Annotation )

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

286
                    $arguments = $this->autoWireMethod($name, $method, $call, /** @scrutinizer ignore-type */ $args);
Loading history...
287 1
                    $result = call_user_func_array([&$service, $call['method']], $arguments);
288
                }
289
            } else {
290 14
                $arguments = $this->autoWireMethod($name, $method, $call);
291 14
                $result = call_user_func_array([&$service, $call['method']], $arguments);
292
            }
293
294 15
            if (isset($call['select']) && true === $call['select']) {
295 2
                $service = $result;
296
            }
297
        }
298
299 52
        $this->storeService($name, $config, $service);
300
301 52
        return $service;
302
    }
303
304
    /**
305
     * Autowire method.
306
     */
307 56
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config, ?array $parameters = null): array
308
    {
309 56
        $params = $method->getParameters();
310 56
        $args = [];
311
312 56
        foreach ($params as $param) {
313 56
            $type = $param->getClass();
314 56
            $param_name = $param->getName();
315
316 56
            $hint = $param->getType();
317 56
            if (null === $hint) {
318 8
                $hint = 'string';
319
            } else {
320 49
                $hint = $hint->getName();
321
            }
322
323 56
            if (isset($parameters[$param_name])) {
324 4
                $args[$param_name] = $parameters[$param_name];
325 55
            } elseif (isset($config['arguments'][$param_name])) {
326 44
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name, $hint);
327 23
            } elseif (null !== $type) {
328 10
                $args[$param_name] = $this->resolveServiceArgument($name, $type, $param);
329 15
            } elseif ($param->isDefaultValueAvailable()) {
330 12
                $args[$param_name] = $param->getDefaultValue();
331 4
            } elseif ($param->allowsNull()) {
332 1
                $args[$param_name] = null;
333
            } else {
334 3
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
335
            }
336
337 50
            if (!$param->canBePassedByValue()) {
338 2
                $value = &$args[$param_name];
339 2
                $args[$param_name] = &$value;
340
            }
341
        }
342
343 50
        return $args;
344
    }
345
346
    /**
347
     * Resolve service argument.
348
     */
349 10
    protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param)
350
    {
351 10
        $type_class = $type->getName();
352
353 10
        if ($type_class === $name) {
354 1
            throw new RuntimeException('class '.$type_class.' can not depend on itself');
355
        }
356
357
        try {
358 9
            return $this->traverseTree($name, $type_class);
359 2
        } catch (\Exception $e) {
360 2
            if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) {
361 1
                return null;
362
            }
363
364 1
            throw $e;
365
        }
366
    }
367
368
    /**
369
     * Parse param value.
370
     */
371 44
    protected function parseParam($param, string $name, string $type = 'string')
372
    {
373 44
        if (is_iterable($param)) {
374 1
            foreach ($param as $key => $value) {
375 1
                $param[$key] = $this->parseParam($value, $name, 'string');
376
            }
377
378 1
            return $param;
379
        }
380
381 44
        if (is_string($param)) {
382 42
            $param = $this->config->getEnv($param, $type);
383 41
            if (is_string($param)) {
384 37
                if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) {
385 1
                    return '{'.$matches[1].'}';
386
                }
387 36
                if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) {
388 1
                    return $this->traverseTree($name, $matches[1]);
389
                }
390
            }
391
392 40
            return $param;
393
        }
394 2
        if ($param instanceof \Closure) {
395 1
            $param = $param->bindTo($this);
396
        }
397
398 2
        return $param;
399
    }
400
401
    /**
402
     * Locate service.
403
     */
404 10
    protected function traverseTree(string $current_service, string $service)
405
    {
406 10
        if (isset($this->children[$current_service])) {
407 1
            return $this->children[$current_service]->get($service);
408
        }
409
410 10
        $config = $this->config->get($current_service);
411 10
        if (isset($config['services'])) {
412 2
            $this->children[$current_service] = new self($config['services'], $this, $this->service[ContainerInterface::class]);
413
414 2
            return $this->children[$current_service]->get($service);
415
        }
416
417 9
        return $this->get($service);
418
    }
419
}
420