Container   B
last analyzed

Complexity

Total Complexity 51

Size/Duplication

Total Lines 336
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 2
dl 0
loc 336
rs 8.3206
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 8 1
A get() 0 14 3
A getNew() 0 8 2
A add() 0 9 2
A has() 0 4 1
C autoWire() 0 64 13
C findParentService() 0 29 7
C createInstance() 0 47 10
D parseParam() 0 29 9
A getParam() 0 12 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
declare(strict_types = 1);
3
4
/**
5
 * Micro
6
 *
7
 * @author    Raffael Sahli <[email protected]>
8
 * @copyright Copyright (c) 2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license   MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro;
13
14
use \ReflectionClass;
15
use \Closure;
16
use \Micro\Container\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Micro\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use \Micro\Container\AdapterAwareInterface;
18
use \Psr\Container\ContainerInterface;
19
20
class Container implements ContainerInterface
21
{
22
    /**
23
     * Config
24
     *
25
     * @var array
26
     */
27
    protected $config = [];
28
29
30
    /**
31
     * Service registry
32
     *
33
     * @var array
34
     */
35
    protected $service = [];
36
37
38
    /**
39
     * Registered but not initialized service registry
40
     *
41
     * @var array
42
     */
43
    protected $registry = [];
44
45
46
    /**
47
     * Create container
48
     *
49
     * @param array $config
50
     */
51
    public function __construct(Iterable $config=[])
52
    {
53
        $this->config = $config;
54
        $container = $this;
55
        $this->add(ContainerInterface::class, function() use($container){
56
            return $container;
57
        });
58
    }
59
60
61
    /**
62
     * Get service
63
     *
64
     * @param  string $name
65
     * @return mixed
66
     */
67
    public function get($name)
68
    {
69
        if($this->has($name)) {
70
            return $this->service[$name]['instance'];
71
        } else {
72
            if(isset($this->registry[$name])) {
73
                $this->service[$name]['instance'] = $this->registry[$name]->call($this);
74
                unset($this->registry[$name]);
75
                return $this->service[$name]['instance'];
76
            } else {
77
                return $this->autoWire($name);
78
            }
79
        }
80
    }
81
82
83
    /**
84
     * Get new instance (Do not store in container)
85
     *
86
     * @param  string $name
87
     * @return mixed
88
     */
89
    public function getNew(string $name)
90
    {
91
        if(isset($this->registry[$name])) {
92
            return $this->registry[$name]->call($this);
93
        } else {
94
            return $this->autoWire($name);
95
        }
96
    }
97
98
99
    /**
100
     * Add service
101
     *
102
     * @param  string $name
103
     * @param  Closure $service
104
     * @return Container
105
     */
106
    public function add(string $name, Closure $service): Container
107
    {
108
        if($this->has($name)) {
109
            throw new Exception('service '.$name.' is already registered');
110
        }
111
112
        $this->registry[$name] = $service;
113
        return $this;
114
    }
115
116
117
    /**
118
     * Check if service is registered
119
     *
120
     * @return bool
121
     */
122
    public function has($name): bool
123
    {
124
        return isset($this->service[$name]);
125
    }
126
127
128
    /**
129
     * Auto wire
130
     *
131
     * @param string $name
132
     * @param Iterable $config
133
     * @param array $parents
134
     * @return mixed
135
     */
136
    protected function autoWire(string $name, $config=null, array $parents=[])
137
    {
138
        if($config === null) {
139
            $config = $this->config;
140
        }
141
142
        $class = $name;
143
        $sub_config = $config;
144
        if(isset($config[$name])) {
145
            if(isset($config[$name]['use'])) {
146
                $class = $config[$name]['use'];
147
            } elseif(isset($config[$name]['name'])) {
148
                $class = $config[$name]['name'];
149
            }
150
151
            $config = $config[$name];
152
        } else {
153
            $config = [];
154
        }
155
156
        try {
157
            $reflection = new ReflectionClass($class);
158
        } catch(\Exception $e) {
159
            throw new Exception($class.' can not be resolved to an existing class');
160
        }
161
162
        $constructor = $reflection->getConstructor();
163
164
        if($constructor === null) {
165
            return new $class();
166
        } else {
167
            $params = $constructor->getParameters();
168
            $args = [];
169
170
            foreach($params as $param) {
171
                $type = $param->getClass();
172
                $param_name = $param->getName();
173
174
                if($type === null) {
175
                    try {
176
                        $args[$param_name] = $this->getParam($name, $param_name, $sub_config);
177
                    } catch(Exception $e) {
178
                        if($param->isDefaultValueAvailable()) {
179
                            $args[$param_name] = $param->getDefaultValue();
180
                        } elseif($param->allowsNull()) {
181
                            $args[$param_name] = null;
182
                        } else {
183
                            throw $e;
184
                        }
185
                    }
186
                } else {
187
                    $type_class = $type->getName();
188
189
                    if($type_class === $name) {
190
                        throw new Exception('class '.$type_class.' can not depend on itself');
191
                    }
192
193
                    $args[$param_name] = $this->findParentService($name, $type_class, $config, $parents);
194
               }
195
            }
196
197
            return $this->createInstance($name, $reflection, $args, $config, $parents);
198
        }
199
    }
200
201
202
    /**
203
     * Traverse services with parents and find correct service to use
204
     *
205
     * @param  string $name
206
     * @param  string $class
207
     * @return mixed
208
     */
209
    protected function findParentService(string $name, string $class, $config, $parents)
0 ignored issues
show
Unused Code introduced by
The parameter $name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $config is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
210
    {
211
        $service = null;
212
        $services = $this->service;
213
        foreach(array_reverse($parents) as $name => $parent) {
214
            if(isset($services[$name])) {
215
                $service = $services[$name];
216
                if(isset($services['service'])) {
217
                    $services = $services['service']; }
218
                else {
219
                    break;
220
                }
221
            } else {
222
                break;
223
            }
224
        }
225
226
        if($service !== null) {
227
            return $service['instance'];
228
        }
229
230
        foreach(array_reverse($parents) as $parent) {
231
            if(isset($parent['service'][$class])) {
232
                return $this->autoWire($class, $parent['service'], $parents);
233
            }
234
        }
235
236
        return $this->get($class);
237
    }
238
239
240
    /**
241
     * Create instance
242
     *
243
     * @param  string $name
244
     * @param  ReflectionClass $class
245
     * @param  array $args
246
     * @return mixed
247
     */
248
    protected function createInstance(string $name, ReflectionClass $class, array $args, Iterable $config, $parents=[])
249
    {
250
        $instance = $class->newInstanceArgs($args);
251
252
        $loop = &$this->service;
253
        foreach($parents as $p => $parent) {
254
            $loop = &$loop[$p];
255
        }
256
        if(count($parents) === 0) {
257
            $loop[$name]['instance'] = $instance;
258
        }   else {
259
            $loop['service'][$name]['instance'] = $instance;
260
        }
261
262
263
        $parents[$name] = $config;
264
        $parents_orig = $parents;
265
266
        array_unshift($parents, $name);
267
268
        if($instance instanceof AdapterAwareInterface) {
269
            if(isset($config['adapter'])) {
270
                $adapters = $config['adapter'];
271
            } else {
272
                $adapters = $instance->getDefaultAdapter();
273
            }
274
275
            foreach($adapters as $adapter => $service) {
276
                if(isset($service['enabled']) && $service['enabled'] === '0') {
277
                    continue;
278
                }
279
280
                $parents = $parents_orig;
281
                $parents[$adapter] = $service;
282
                $class = $adapter;
283
                $adapter_instance = $this->autoWire($class, $adapters, $parents);
284
285
                if(isset($service['expose']) && $service['expose']) {
286
                    $this->service[$adapter]['instance'] = $adapter_instance;
287
                }
288
289
                $instance->injectAdapter($adapter_instance, $adapter);
290
            }
291
        }
292
293
        return $instance;
294
    }
295
296
297
    /**
298
     * Parse param value
299
     *
300
     * @param mixed $param
301
     * @return mixed
302
     */
303
    protected function parseParam($param)
304
    {
305
        if(is_iterable($param)) {
306
            foreach($param as $key => $value) {
307
                $param[$key] = $this->parseParam($value);
308
            }
309
310
            return $param;
311
        } elseif(is_string($param)) {
312
            if(preg_match('#\{ENV\(([A-Za-z0-9_]+)(?:(,?)(.*))\)\}#', $param, $match)) {
313
                if(count($match) !== 4) {
314
                    return $param;
315
                }
316
317
                $env = getenv($match[1]);
318
                if($env === false && !empty($match[3])) {
319
                    return str_replace($match[0], $match[3], $param);
320
                } elseif($env === false) {
321
                    throw new Exception('env variable '.$match[1].' required but it is neither set not a default value exists');
322
                } else {
323
                    return str_replace($match[0], $env, $param);
324
                }
325
            } else {
326
                return $param;
327
            }
328
        } else {
329
            return $param;
330
        }
331
    }
332
333
334
335
336
    /**
337
     * Get config value
338
     *
339
     * @param  string $name
340
     * @param  string $param
341
     * @return mixed
342
     */
343
    public function getParam(string $name, string $param, ?Iterable $config=null)
344
    {
345
        if($config === null) {
346
            $config = $this->config;
347
        }
348
349
        if(!isset($config[$name]['options'][$param])) {
350
            throw new Exception('no configuration available for required service parameter '.$param);
351
        }
352
353
        return $this->parseParam($config[$name]['options'][$param]);
354
    }
355
}
356