Passed
Branch dev (b7ee17)
by Raffael
03:56
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
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;
0 ignored issues
show
Documentation Bug introduced by
It seems like $parent can also be of type Micro\Container\RuntimeContainer. However, the property $parent is declared as type Psr\Container\ContainerInterface. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

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