Completed
Push — master ( d7d6fe...fa9885 )
by Raffael
02:08
created

Container   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 357
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 357
rs 7.9487
c 0
b 0
f 0
wmc 52

11 Methods

Rating   Name   Duplication   Size   Complexity  
B lookupService() 0 19 5
A __construct() 0 5 1
B get() 0 18 5
A findService() 0 13 3
A addStaticService() 0 11 2
A has() 0 3 1
A add() 0 9 2
C parseParam() 0 37 11
C autoWireMethod() 0 29 8
C autoWireClass() 0 47 9
B createInstance() 0 25 5

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
7
 *
8
 * @copyright   Copryright (c) 2015-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
19
class Container implements ContainerInterface
20
{
21
    /**
22
     * Config.
23
     *
24
     * @var iterable
25
     */
26
    protected $config = [];
27
28
    /**
29
     * Service registry.
30
     *
31
     * @var array
32
     */
33
    protected $service = [];
34
35
    /**
36
     * Registered but not initialized service registry.
37
     *
38
     * @var array
39
     */
40
    protected $registry = [];
41
42
    /**
43
     * Parent container.
44
     *
45
     * @var ContainerInterface
46
     */
47
    protected $parent;
48
49
    /**
50
     * Children container.
51
     *
52
     * @var ContainerInterface[]
53
     */
54
    protected $children = [];
55
56
    /**
57
     * Create container.
58
     *
59
     * @param iterable $config
60
     */
61
    public function __construct(Iterable $config = [], ?ContainerInterface $parent = null)
62
    {
63
        $this->config = $config;
64
        $this->parent = $parent;
65
        $this->add(ContainerInterface::class, $this);
66
    }
67
68
    /**
69
     * Get service.
70
     *
71
     * @param string $name
72
     *
73
     * @return mixed
74
     */
75
    public function get($name)
76
    {
77
        if ($this->has($name)) {
78
            return $this->service[$name];
79
        }
80
81
        if (isset($this->registry[$name])) {
82
            return $this->addStaticService($name);
83
        }
84
85
        if (isset($this->config[$name])) {
86
            return $this->autoWireClass($name);
87
        }
88
89
        try {
90
            return $this->lookupService($name);
91
        } catch (Exception\ServiceNotFound $e) {
92
            return $this->autoWireClass($name);
93
        }
94
    }
95
96
    /**
97
     * Traverse tree up and look for service.
98
     *
99
     * @param string $name
100
     *
101
     * @return mixed
102
     */
103
    public function lookupService(string $name)
104
    {
105
        if ($this->has($name)) {
106
            return $this->service[$name];
107
        }
108
109
        if (isset($this->registry[$name])) {
110
            return $this->addStaticService($name);
111
        }
112
113
        if (isset($this->config[$name])) {
114
            return $this->autoWireClass($name);
115
        }
116
117
        if (null !== $this->parent) {
118
            return $this->parent->lookupService($name);
119
        }
120
121
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
122
    }
123
124
    /**
125
     * Add service.
126
     *
127
     * @param string $name
128
     * @param mixed  $service
129
     *
130
     * @return Container
131
     */
132
    public function add(string $name, $service): self
133
    {
134
        if ($this->has($name)) {
135
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
136
        }
137
138
        $this->registry[$name] = $service;
139
140
        return $this;
141
    }
142
143
    /**
144
     * Check if service is registered.
145
     *
146
     * @param mixed $name
147
     *
148
     * @return bool
149
     */
150
    public function has($name): bool
151
    {
152
        return isset($this->service[$name]);
153
    }
154
155
    /**
156
     * Check for static injections.
157
     *
158
     * @param string $name
159
     *
160
     * @return mixed
161
     */
162
    protected function addStaticService(string $name)
163
    {
164
        if ($this->registry[$name] instanceof Closure) {
165
            $this->service[$name] = $this->registry[$name]->call($this);
166
        } else {
167
            $this->service[$name] = $this->registry[$name];
168
        }
169
170
        unset($this->registry[$name]);
171
172
        return $this->service[$name];
173
    }
174
175
    /**
176
     * Auto wire.
177
     *
178
     * @param string $name
179
     * @param array  $config
180
     * @param array  $parents
181
     *
182
     * @return mixed
183
     */
184
    protected function autoWireClass(string $name)
185
    {
186
        $class = $name;
187
        $config = [];
188
189
        if (isset($this->config[$name])) {
190
            $config = $this->config[$name];
191
        }
192
193
        if (isset($config['use'])) {
194
            if (!is_string($config['use'])) {
195
                throw new Exception\Configuration('use must be a string for service '.$name);
196
            }
197
198
            $class = $config['use'];
199
        }
200
201
        if (preg_match('#^\{(.*)\}$#', $class, $match)) {
202
            $service = $this->get($match[1]);
203
204
            if (isset($this->config[$name]['selects'])) {
205
                $reflection = new ReflectionClass(get_class($service));
206
207
                foreach ($this->config[$name]['selects'] as $select) {
208
                    $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
209
                    $service = call_user_func_array([&$service, $select['method']], $args);
210
                }
211
            }
212
213
            return $this->service[$name] = $service;
214
        }
215
216
        try {
217
            $reflection = new ReflectionClass($class);
218
        } catch (\Exception $e) {
219
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
220
        }
221
222
        $constructor = $reflection->getConstructor();
223
224
        if (null === $constructor) {
225
            return new $class();
226
        }
227
228
        $args = $this->autoWireMethod($name, $constructor, $config);
229
230
        return $this->createInstance($name, $reflection, $args, $config);
231
    }
232
233
    /**
234
     * Create instance.
235
     *
236
     * @param string          $name
237
     * @param ReflectionClass $class
238
     * @param array           $arguments
239
     * @param array           $config
240
     *
241
     * @return mixed
242
     */
243
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
244
    {
245
        $instance = $class->newInstanceArgs($arguments);
246
        $this->service[$name] = $instance;
247
248
        if (isset($this->config[$name]['calls'])) {
249
            foreach ($this->config[$name]['calls'] as $call) {
250
                if (!isset($call['method'])) {
251
                    throw new Exception\Configuration('method is required for setter injection in service '.$name);
252
                }
253
254
                $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
255
256
                try {
257
                    $method = $class->getMethod($call['method']);
258
                } catch (\ReflectionException $e) {
259
                    throw new Exception\Configuration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
260
                }
261
262
                $arguments = $this->autoWireMethod($name, $method, $call);
263
                call_user_func_array([&$instance, $call['method']], $arguments);
264
            }
265
        }
266
267
        return $instance;
268
    }
269
270
    /**
271
     * Autowire method.
272
     *
273
     * @param string           $name
274
     * @param ReflectionMethod $method
275
     * @param array            $config
276
     *
277
     * @return array
278
     */
279
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
280
    {
281
        $params = $method->getParameters();
282
        $args = [];
283
284
        foreach ($params as $param) {
285
            $type = $param->getClass();
286
            $param_name = $param->getName();
287
288
            if (isset($config['arguments'][$param_name])) {
289
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
290
            } elseif (null !== $type) {
291
                $type_class = $type->getName();
292
293
                if ($type_class === $name) {
294
                    throw new Exception\Logic('class '.$type_class.' can not depend on itself');
295
                }
296
297
                $args[$param_name] = $this->findService($name, $type_class);
298
            } elseif ($param->isDefaultValueAvailable()) {
299
                $args[$param_name] = $param->getDefaultValue();
300
            } elseif ($param->allowsNull() && $param->hasType()) {
301
                $args[$param_name] = null;
302
            } else {
303
                throw new Exception\Configuration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
304
            }
305
        }
306
307
        return $args;
308
    }
309
310
    /**
311
     * Parse param value.
312
     *
313
     * @param mixed  $param
314
     * @param string $name
315
     *
316
     * @return mixed
317
     */
318
    protected function parseParam($param, string $name)
319
    {
320
        if (is_iterable($param)) {
321
            foreach ($param as $key => $value) {
322
                $param[$key] = $this->parseParam($value, $name);
323
            }
324
325
            return $param;
326
        }
327
        if (is_string($param)) {
328
            if ($found = preg_match_all('#\{ENV\(([A-Za-z0-9_]+)(?:(,?)([^}]*))\)\}#', $param, $matches)) {
0 ignored issues
show
Unused Code introduced by
The assignment to $found is dead and can be removed.
Loading history...
329
                if (4 !== count($matches)) {
330
                    return $param;
331
                }
332
333
                for ($i = 0; $i < 1; ++$i) {
334
                    $env = getenv($matches[1][$i]);
335
                    if (false === $env && !empty($matches[3][$i])) {
336
                        $param = str_replace($matches[0][$i], $matches[3][$i], $param);
337
                    } elseif (false === $env) {
338
                        throw new Exception\EnvVariableNotFound('env variable '.$matches[1][$i].' required but it is neither set not a default value exists');
339
                    } else {
340
                        $param = str_replace($matches[0][$i], $env, $param);
341
                    }
342
                }
343
344
                return $param;
345
            }
346
347
            if (preg_match('#^\{(.*)\}$#', $param, $matches)) {
348
                return $this->findService($name, $matches[1]);
349
            }
350
351
            return $param;
352
        }
353
354
        return $param;
355
    }
356
357
    /**
358
     * Locate service.
359
     *
360
     * @param string $current_service
361
     * @param string $service
362
     */
363
    protected function findService(string $current_service, string $service)
364
    {
365
        if (isset($this->children[$current_service])) {
366
            return $this->children[$current_service]->get($service);
367
        }
368
369
        if (isset($this->config[$current_service]['services'])) {
370
            $this->children[$current_service] = new self($this->config[$current_service]['services'], $this);
371
372
            return $this->children[$current_service]->get($service);
373
        }
374
375
        return $this->get($service);
376
    }
377
}
378