Completed
Push — master ( 5d9a92...ab5aec )
by Raffael
01:46
created

Container   B

Complexity

Total Complexity 42

Size/Duplication

Total Lines 293
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 42
lcom 1
cbo 1
dl 0
loc 293
rs 8.295
c 0
b 0
f 0

9 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
D createInstance() 0 43 10
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
                foreach($config['adapter'] as $adapter => $service) {
271
                    if(isset($service['enabled']) && $service['enabled'] === '0') {
272
                        continue;
273
                    }
274
275
                    $parents = $parents_orig;
276
                    $parents[$adapter] = $service;
277
                    $class = $adapter;
278
                    $adapter_instance = $this->autoWire($class, $config['adapter'], $parents);
279
280
                    if(isset($service['expose']) && $service['expose']) {
281
                        $this->service[$adapter]['instance'] = $adapter_instance;
282
                    }
283
284
                    $instance->injectAdapter($adapter, $adapter_instance);
285
                }
286
            }
287
        }
288
289
        return $instance;
290
    }
291
292
293
    /**
294
     * Get config value
295
     *
296
     * @param  string $name
297
     * @param  string $param
298
     * @return mixed
299
     */
300
    public function getParam(string $name, string $param, ?Iterable $config=null)
301
    {
302
        if($config === null) {
303
            $config = $this->config;
304
        }
305
306
        if(!isset($config[$name]['options'][$param])) {
307
            throw new Exception('no configuration available for required service parameter '.$param);
308
        }
309
310
        return $config[$name]['options'][$param];
311
    }
312
}
313