Passed
Push — master ( a1d984...241d4d )
by Raffael
04:27
created

RuntimeContainer::storeService()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 3
crap 3
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Micro\Container
7
 *
8
 * @copyright   Copryright (c) 2018 gyselroth GmbH (https://gyselroth.com)
9
 * @license     MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro\Container;
13
14
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
15
use Psr\Container\ContainerInterface;
16
use ReflectionClass;
17
use ReflectionMethod;
18
use ReflectionParameter;
19
use RuntimeException;
20
21
class RuntimeContainer
22
{
23
    /**
24
     * Config.
25
     *
26
     * @var Config
27
     */
28
    protected $config;
29
30
    /**
31
     * Service registry.
32
     *
33
     * @var array
34
     */
35
    protected $service = [];
36
37
    /**
38
     * Parent container.
39
     *
40
     * @var ContainerInterface|RuntimeContainer
41
     */
42
    protected $parent;
43
44
    /**
45
     * Children container.
46
     *
47
     * @var ContainerInterface[]
48
     */
49
    protected $children = [];
50
51
    /**
52
     * Parent service.
53
     *
54
     * @var mixed
55
     */
56
    protected $parent_service;
57
58
    /**
59
     * Create container.
60
     *
61
     * @param iterable                            $config
62
     * @param ContainerInterface|RuntimeContainer $parent
63
     */
64 47
    public function __construct(Iterable $config = [], $parent = null)
65
    {
66 47
        $this->config = new Config($config, $this);
67 47
        $this->parent = $parent;
68 47
        $this->service[ContainerInterface::class] = $this;
69 47
    }
70
71
    /**
72
     * Get parent container.
73
     *
74
     * @return ContainerInterface|RuntimeContainer
75
     */
76 42
    public function getParent()
77
    {
78 42
        return $this->parent;
79
    }
80
81
    /**
82
     * Set parent service on container.
83
     *
84
     * @param mixed $service
85
     *
86
     * @return ContainerInterface|RuntimeContainer
87
     */
88 2
    public function setParentService($service)
89
    {
90 2
        $this->parent_service = $service;
91
92 2
        return $this;
93
    }
94
95
    /**
96
     * Get config.
97
     *
98
     * @return Config
99
     */
100 2
    public function getConfig(): Config
101
    {
102 2
        return $this->config;
103
    }
104
105
    /**
106
     * Get service.
107
     *
108
     * @param string $name
109
     *
110
     * @return mixed
111
     */
112 47
    public function get(string $name)
113
    {
114
        try {
115 47
            return $this->resolve($name);
116 17
        } catch (Exception\ServiceNotFound $e) {
117 13
            return $this->wrapService($name);
118
        }
119
    }
120
121
    /**
122
     * Resolve service.
123
     *
124
     * @param string $name
125
     *
126
     * @return mixed
127
     */
128 47
    public function resolve(string $name)
129
    {
130 47
        if (isset($this->service[$name])) {
131 6
            return $this->service[$name];
132
        }
133
134 46
        if ($this->config->has($name)) {
135 41
            return $this->wrapService($name);
136
        }
137
138 14
        if (null !== $this->parent_service) {
139 1
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
140
141 1
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
142
                return $this->parent_service;
143
            }
144
        }
145
146 14
        if (null !== $this->parent) {
147 1
            return $this->parent->resolve($name);
0 ignored issues
show
Bug introduced by
The method resolve() does not exist on Psr\Container\ContainerInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

147
            return $this->parent->/** @scrutinizer ignore-call */ resolve($name);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
148
        }
149
150 13
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
151
    }
152
153
    /**
154
     * Store service.
155
     *
156
     * @param param string $name
157
     * @param array        $config
158
     * @param mixed        $service
159
     *
160
     * @return mixed
161
     */
162 39
    protected function storeService(string $name, array $config, $service)
163
    {
164 39
        if (true === $config['singleton']) {
165 2
            return $service;
166
        }
167 38
        $this->service[$name] = $service;
168
169 38
        if (isset($this->children[$name])) {
170 2
            $this->children[$name]->setParentService($service);
0 ignored issues
show
Bug introduced by
The method setParentService() does not exist on Psr\Container\ContainerInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

170
            $this->children[$name]->/** @scrutinizer ignore-call */ 
171
                                    setParentService($service);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
171
        }
172
173 38
        return $service;
174
    }
175
176
    /**
177
     * Wrap resolved service in callable if enabled.
178
     *
179
     * @param string $name
180
     *
181
     * @return mixed
182
     */
183 46
    protected function wrapService(string $name)
184
    {
185 46
        $config = $this->config->get($name);
186 43
        if (true === $config['wrap']) {
187 1
            $that = $this;
188
189 1
            return function () use ($that, $name) {
190 1
                return $that->autoWireClass($name);
191 1
            };
192
        }
193
194 42
        return $this->autoWireClass($name);
195
    }
196
197
    /**
198
     * Auto wire.
199
     *
200
     * @param string $name
201
     *
202
     * @return mixed
203
     */
204 43
    protected function autoWireClass(string $name)
205
    {
206 43
        $config = $this->config->get($name);
207 43
        $class = $config['use'];
208
209 43
        if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) {
210 1
            return $this->wireReference($name, $match[1], $config);
211
        }
212
213 43
        $reflection = new ReflectionClass($class);
214
215 43
        if (isset($config['factory'])) {
216 3
            $factory = $reflection->getMethod($config['factory']);
217 3
            $args = $this->autoWireMethod($name, $factory, $config);
218 3
            $instance = call_user_func_array([$class, $config['factory']], $args);
219
220 3
            return $this->prepareService($name, $instance, $reflection, $config);
221
        }
222
223 40
        $constructor = $reflection->getConstructor();
224
225 40
        if (null === $constructor) {
226 3
            return $this->storeService($name, $config, new $class());
227
        }
228
229 37
        $args = $this->autoWireMethod($name, $constructor, $config);
230
231 34
        return $this->createInstance($name, $reflection, $args, $config);
232
    }
233
234
    /**
235
     * Wire named referenced service.
236
     *
237
     * @param string $name
238
     * @param string $refrence
239
     * @param array  $config
240
     *
241
     * @return mixed
242
     */
243 1
    protected function wireReference(string $name, string $reference, array $config)
244
    {
245 1
        $service = $this->get($reference);
246 1
        $reflection = new ReflectionClass(get_class($service));
247 1
        $config = $this->config->get($name);
248 1
        $service = $this->prepareService($name, $service, $reflection, $config);
249
250 1
        return $service;
251
    }
252
253
    /**
254
     * Get instance (virtual or real instance).
255
     *
256
     * @param string          $name
257
     * @param ReflectionClass $class
258
     * @param array           $arguments
259
     * @param array           $config
260
     *
261
     * @return mixed
262
     */
263 34
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
264
    {
265 34
        if (true === $config['lazy']) {
266 2
            return $this->getProxyInstance($name, $class, $arguments, $config);
267
        }
268
269 32
        return $this->getRealInstance($name, $class, $arguments, $config);
270
    }
271
272
    /**
273
     * Create proxy instance.
274
     *
275
     * @param string          $name
276
     * @param ReflectionClass $class
277
     * @param array           $arguments
278
     * @param array           $config
279
     *
280
     * @return mixed
281
     */
282 2
    protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config)
283
    {
284 2
        $factory = new LazyLoadingValueHolderFactory();
285 2
        $that = $this;
286
287 2
        return $factory->createProxy(
288 2
            $class->getName(),
289 2
            function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) {
290 1
                $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config);
291 1
                $initializer = null;
292 2
            }
293
        );
294
    }
295
296
    /**
297
     * Create real instance.
298
     *
299
     * @param string          $name
300
     * @param ReflectionClass $class
301
     * @param array           $arguments
302
     * @param array           $config
303
     *
304
     * @return mixed
305
     */
306 33
    protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config)
307
    {
308 33
        $instance = $class->newInstanceArgs($arguments);
309 33
        $instance = $this->prepareService($name, $instance, $class, $config);
310
311 31
        return $instance;
312
    }
313
314
    /**
315
     * Prepare service (execute sub selects and excute setter injections).
316
     *
317
     * @param string          $name
318
     * @param mixed           $service
319
     * @param ReflectionClass $class
320
     * @param array           $config
321
     *
322
     * @return mixed
323
     */
324 36
    protected function prepareService(string $name, $service, ReflectionClass $class, array $config)
325
    {
326 36
        foreach ($config['selects'] as $select) {
327 2
            $args = $this->autoWireMethod($name, $class->getMethod($select['method']), $select);
328 2
            $service = call_user_func_array([&$service, $select['method']], $args);
329
        }
330
331 36
        $this->storeService($name, $config, $service);
332
333 36
        foreach ($config['calls'] as $call) {
334 9
            if (!is_array($call)) {
335 1
                continue;
336
            }
337
338 9
            if (!isset($call['method'])) {
339 1
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
340
            }
341
342 8
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
343
344
            try {
345 8
                $method = $class->getMethod($call['method']);
346 1
            } catch (\ReflectionException $e) {
347 1
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
348
            }
349
350 7
            $arguments = $this->autoWireMethod($name, $method, $call);
351 7
            call_user_func_array([&$service, $call['method']], $arguments);
352
        }
353
354 34
        return $service;
355
    }
356
357
    /**
358
     * Autowire method.
359
     *
360
     * @param string           $name
361
     * @param ReflectionMethod $method
362
     * @param array            $config
363
     *
364
     * @return array
365
     */
366 40
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
367
    {
368 40
        $params = $method->getParameters();
369 40
        $args = [];
370
371 40
        foreach ($params as $param) {
372 40
            $type = $param->getClass();
373 40
            $param_name = $param->getName();
374
375 40
            if (isset($config['arguments'][$param_name])) {
376 34
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
377 19
            } elseif (null !== $type) {
378 10
                $args[$param_name] = $this->resolveServiceArgument($name, $type, $param);
379 11
            } elseif ($param->isDefaultValueAvailable()) {
380 11
                $args[$param_name] = $param->getDefaultValue();
381 1
            } elseif ($param->allowsNull()) {
382 1
                $args[$param_name] = null;
383
            } else {
384 37
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
385
            }
386
        }
387
388 37
        return $args;
389
    }
390
391
    /**
392
     * Resolve service argument.
393
     *
394
     * @param string              $name
395
     * @param ReflectionClass     $type
396
     * @param ReflectionParameter $param
397
     *
398
     * @return mixed
399
     */
400 10
    protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param)
401
    {
402 10
        $type_class = $type->getName();
403
404 10
        if ($type_class === $name) {
405 1
            throw new RuntimeException('class '.$type_class.' can not depend on itself');
406
        }
407
408
        try {
409 9
            return $this->traverseTree($name, $type_class);
410 2
        } catch (\Exception $e) {
411 2
            if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) {
412 1
                return null;
413
            }
414
415 1
            throw $e;
416
        }
417
    }
418
419
    /**
420
     * Parse param value.
421
     *
422
     * @param mixed  $param
423
     * @param string $name
424
     *
425
     * @return mixed
426
     */
427 34
    protected function parseParam($param, string $name)
428
    {
429 34
        if (is_iterable($param)) {
430 1
            foreach ($param as $key => $value) {
431 1
                $param[$key] = $this->parseParam($value, $name);
432
            }
433
434 1
            return $param;
435
        }
436
437 34
        if (is_string($param)) {
438 33
            $param = $this->config->getEnv($param);
439
440 32
            if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) {
441 1
                return '{'.$matches[1].'}';
442
            }
443 31
            if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) {
444 1
                return $this->traverseTree($name, $matches[1]);
445
            }
446
447 31
            return $param;
448
        }
449
450 1
        return $param;
451
    }
452
453
    /**
454
     * Locate service.
455
     *
456
     * @param string $current_service
457
     * @param string $service
458
     */
459 10
    protected function traverseTree(string $current_service, string $service)
460
    {
461 10
        if (isset($this->children[$current_service])) {
462 1
            return $this->children[$current_service]->get($service);
463
        }
464
465 10
        $config = $this->config->get($current_service);
466 10
        if (isset($config['services'])) {
467 2
            $this->children[$current_service] = new self($config['services'], $this);
468
469 2
            return $this->children[$current_service]->get($service);
470
        }
471
472 9
        return $this->get($service);
473
    }
474
}
475