Passed
Push — master ( 459291...abc385 )
by Raffael
02:29
created

Container::resolve()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 15
rs 9.2
c 0
b 0
f 0
cc 4
eloc 7
nc 4
nop 1
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 ($service = null !== $this->resolve($name)) {
78
            return $service;
79
        }
80
81
        try {
82
            return $this->lookupService($name);
83
        } catch (Exception\ServiceNotFound $e) {
84
            return $this->autoWireClass($name);
85
        }
86
    }
87
88
    /**
89
     * Traverse tree up and look for service.
90
     *
91
     * @param string $name
92
     *
93
     * @return mixed
94
     */
95
    public function lookupService(string $name)
96
    {
97
        if ($service = null !== $this->resolve($name)) {
98
            return $service;
99
        }
100
101
        if (null !== $this->parent) {
102
            return $this->parent->lookupService($name);
103
        }
104
105
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
106
    }
107
108
    /**
109
     * Add service.
110
     *
111
     * @param string $name
112
     * @param mixed  $service
113
     *
114
     * @return Container
115
     */
116
    public function add(string $name, $service): self
117
    {
118
        if ($this->has($name)) {
119
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
120
        }
121
122
        $this->registry[$name] = $service;
123
124
        return $this;
125
    }
126
127
    /**
128
     * Check if service is registered.
129
     *
130
     * @param mixed $name
131
     *
132
     * @return bool
133
     */
134
    public function has($name): bool
135
    {
136
        return isset($this->service[$name]);
137
    }
138
139
    /**
140
     * Resolve service.
141
     *
142
     * @param string $name
143
     *
144
     * @return mixed
145
     */
146
    protected function resolve(string $name)
147
    {
148
        if ($this->has($name)) {
149
            return $this->service[$name];
150
        }
151
152
        if (isset($this->registry[$name])) {
153
            return $this->addStaticService($name);
154
        }
155
156
        if (isset($this->config[$name])) {
157
            return $this->autoWireClass($name);
158
        }
159
160
        return null;
161
    }
162
163
    /**
164
     * Check for static injections.
165
     *
166
     * @param string $name
167
     *
168
     * @return mixed
169
     */
170
    protected function addStaticService(string $name)
171
    {
172
        if ($this->registry[$name] instanceof Closure) {
173
            $this->service[$name] = $this->registry[$name]->call($this);
174
        } else {
175
            $this->service[$name] = $this->registry[$name];
176
        }
177
178
        unset($this->registry[$name]);
179
180
        return $this->service[$name];
181
    }
182
183
    /**
184
     * Create service config.
185
     *
186
     * @param string $name
187
     *
188
     * @return array
189
     */
190
    protected function createServiceConfig(string $name): array
191
    {
192
        $config = [];
193
        $parents = array_merge(class_implements($name), class_parents($name));
194
        foreach ($parents as $parent) {
195
            if (isset($this->config[$parent])) {
196
                $config = array_merge($config, $this->config[$parent]);
197
            }
198
        }
199
200
        if (isset($this->config[$name])) {
201
            $config = array_merge($config, $this->config[$name]);
202
        }
203
204
        return $config;
205
    }
206
207
    /**
208
     * Auto wire.
209
     *
210
     * @param string $name
211
     * @param array  $config
212
     * @param array  $parents
213
     *
214
     * @return mixed
215
     */
216
    protected function autoWireClass(string $name)
217
    {
218
        $class = $name;
219
        $config = [];
220
221
        if (isset($this->config[$name])) {
222
            $config = $this->config[$name];
223
        }
224
225
        if (isset($config['use'])) {
226
            if (!is_string($config['use'])) {
227
                throw new Exception\InvalidConfiguration('use must be a string for service '.$name);
0 ignored issues
show
Bug introduced by
The type Micro\Container\Exception\InvalidConfiguration was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
228
            }
229
230
            $class = $config['use'];
231
        } else {
232
            $config = $this->createServiceConfig($name);
233
        }
234
235
        if (preg_match('#^\{(.*)\}$#', $class, $match)) {
236
            $service = $this->get($match[1]);
237
238
            if (isset($this->config[$name]['selects'])) {
239
                $reflection = new ReflectionClass(get_class($service));
0 ignored issues
show
Bug introduced by
It seems like $service can also be of type true; however, parameter $object of get_class() does only seem to accept object, 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

239
                $reflection = new ReflectionClass(get_class(/** @scrutinizer ignore-type */ $service));
Loading history...
240
241
                foreach ($this->config[$name]['selects'] as $select) {
242
                    $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
243
                    $service = call_user_func_array([&$service, $select['method']], $args);
244
                }
245
            }
246
247
            return $this->storeService($name, $config, $service);
248
        }
249
250
        try {
251
            $reflection = new ReflectionClass($class);
252
        } catch (\Exception $e) {
253
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
254
        }
255
256
        $constructor = $reflection->getConstructor();
257
258
        if (null === $constructor) {
259
            return new $class();
260
        }
261
262
        $args = $this->autoWireMethod($name, $constructor, $config);
263
264
        return $this->createInstance($name, $reflection, $args, $config);
265
    }
266
267
    /**
268
     * Store service.
269
     *
270
     * @param param string $name
271
     * @param array        $config
272
     * @param mixed        $service
273
     *
274
     * @return mixed
275
     */
276
    protected function storeService(string $name, array $config, $service)
277
    {
278
        if (isset($config['singleton']) && true === $config['singleton']) {
279
            return $service;
280
        }
281
282
        $this->service[$name] = $service;
283
284
        return $service;
285
    }
286
287
    /**
288
     * Create instance.
289
     *
290
     * @param string          $name
291
     * @param ReflectionClass $class
292
     * @param array           $arguments
293
     * @param array           $config
294
     *
295
     * @return mixed
296
     */
297
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
298
    {
299
        $instance = $class->newInstanceArgs($arguments);
300
        $this->storeService($name, $config, $instance);
301
302
        if (!isset($this->config[$name]['calls'])) {
303
            return $instance;
304
        }
305
306
        foreach ($this->config[$name]['calls'] as $call) {
307
            if (!isset($call['method'])) {
308
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
309
            }
310
311
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
312
313
            try {
314
                $method = $class->getMethod($call['method']);
315
            } catch (\ReflectionException $e) {
316
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
317
            }
318
319
            $arguments = $this->autoWireMethod($name, $method, $call);
320
            call_user_func_array([&$instance, $call['method']], $arguments);
321
        }
322
323
        return $instance;
324
    }
325
326
    /**
327
     * Autowire method.
328
     *
329
     * @param string           $name
330
     * @param ReflectionMethod $method
331
     * @param array            $config
332
     *
333
     * @return array
334
     */
335
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
336
    {
337
        $params = $method->getParameters();
338
        $args = [];
339
340
        foreach ($params as $param) {
341
            $type = $param->getClass();
342
            $param_name = $param->getName();
343
344
            if (isset($config['arguments'][$param_name])) {
345
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
346
            } elseif (null !== $type) {
347
                $type_class = $type->getName();
348
349
                if ($type_class === $name) {
350
                    throw new Exception\InvalidConfiguration('class '.$type_class.' can not depend on itself');
351
                }
352
353
                $args[$param_name] = $this->findService($name, $type_class);
354
            } elseif ($param->isDefaultValueAvailable()) {
355
                $args[$param_name] = $param->getDefaultValue();
356
            } elseif ($param->allowsNull() && $param->hasType()) {
357
                $args[$param_name] = null;
358
            } else {
359
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
360
            }
361
        }
362
363
        return $args;
364
    }
365
366
    /**
367
     * Parse param value.
368
     *
369
     * @param mixed  $param
370
     * @param string $name
371
     *
372
     * @return mixed
373
     */
374
    protected function parseParam($param, string $name)
375
    {
376
        if (is_iterable($param)) {
377
            foreach ($param as $key => $value) {
378
                $param[$key] = $this->parseParam($value, $name);
379
            }
380
381
            return $param;
382
        }
383
        if (is_string($param)) {
384
            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...
385
                if (4 !== count($matches)) {
386
                    return $param;
387
                }
388
389
                for ($i = 0; $i < 1; ++$i) {
390
                    $env = getenv($matches[1][$i]);
391
                    if (false === $env && !empty($matches[3][$i])) {
392
                        $param = str_replace($matches[0][$i], $matches[3][$i], $param);
393
                    } elseif (false === $env) {
394
                        throw new Exception\EnvVariableNotFound('env variable '.$matches[1][$i].' required but it is neither set not a default value exists');
395
                    } else {
396
                        $param = str_replace($matches[0][$i], $env, $param);
397
                    }
398
                }
399
400
                return $param;
401
            }
402
403
            if (preg_match('#^\{(.*)\}$#', $param, $matches)) {
404
                return $this->findService($name, $matches[1]);
405
            }
406
407
            return $param;
408
        }
409
410
        return $param;
411
    }
412
413
    /**
414
     * Locate service.
415
     *
416
     * @param string $current_service
417
     * @param string $service
418
     */
419
    protected function findService(string $current_service, string $service)
420
    {
421
        if (isset($this->children[$current_service])) {
422
            return $this->children[$current_service]->get($service);
423
        }
424
425
        if (isset($this->config[$current_service]['services'])) {
426
            $this->children[$current_service] = new self($this->config[$current_service]['services'], $this);
427
428
            return $this->children[$current_service]->get($service);
429
        }
430
431
        return $this->get($service);
432
    }
433
}
434