Passed
Push — master ( a9d76a...252da8 )
by Raffael
02:32
created

Container   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 355
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 51
dl 0
loc 355
rs 8.3206
c 0
b 0
f 0

11 Methods

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