Test Failed
Push — master ( 2d1715...3b11bb )
by Raffael
03:29
created

RuntimeContainer::autoWireMethod()   B

Complexity

Conditions 8
Paths 12

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 8

Importance

Changes 0
Metric Value
eloc 21
dl 0
loc 30
ccs 21
cts 21
cp 1
rs 8.4444
c 0
b 0
f 0
cc 8
nc 12
nop 4
crap 8
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 54
    public function __construct(array $config, $parent, ContainerInterface $interface)
62
    {
63 54
        $this->config = new Config($config, $this);
64 54
        $this->parent = $parent;
65 54
        $this->service[ContainerInterface::class] = $interface;
66 54
    }
67
68
    /**
69
     * Get parent container.
70
     */
71 48
    public function getParent()
72
    {
73 48
        return $this->parent;
74
    }
75
76
    /**
77
     * Set parent service on container.
78
     */
79 2
    public function setParentService($service)
80
    {
81 2
        $this->parent_service = $service;
82
83 2
        return $this;
84
    }
85
86
    /**
87
     * Get config.
88
     */
89 2
    public function getConfig(): Config
90
    {
91 2
        return $this->config;
92
    }
93
94
    /**
95
     * Get service.
96
     */
97 54
    public function get(string $name, ?array $parameters = null)
98
    {
99
        try {
100 54
            return $this->resolve($name, $parameters);
101 22
        } catch (Exception\ServiceNotFound $e) {
102 15
            return $this->wrapService($name, $parameters);
103
        }
104
    }
105
106
    /**
107
     * Resolve service.
108
     */
109 54
    public function resolve(string $name, ?array $parameters = null)
110
    {
111 54
        if (isset($this->service[$name])) {
112 7
            return $this->service[$name];
113
        }
114
115 52
        if ($this->config->has($name)) {
116 45
            return $this->wrapService($name, $parameters);
117
        }
118
119 16
        if (null !== $this->parent_service) {
120 1
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
121
122 1
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
123
                return $this->parent_service;
124
            }
125
        }
126
127 16
        if (null !== $this->parent) {
128 1
            return $this->parent->resolve($name, $parameters);
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

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

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...
129
        }
130
131 15
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
132
    }
133
134
    /**
135
     * Store service.
136
     */
137 39
    protected function storeService(string $name, array $config, $service)
138
    {
139 39
        if (false === $config['singleton']) {
140 4
            return $service;
141
        }
142 35
        $this->service[$name] = $service;
143
144 35
        if (isset($this->children[$name])) {
145 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

145
            $this->children[$name]->/** @scrutinizer ignore-call */ 
146
                                    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...
146
        }
147
148 35
        return $service;
149
    }
150
151
    /**
152
     * Wrap resolved service in callable if enabled.
153
     */
154 52
    protected function wrapService(string $name, ?array $parameters = null)
155
    {
156 52
        $config = $this->config->get($name);
157 49
        if (true === $config['wrap']) {
158 1
            $that = $this;
159
160 1
            return function () use ($that, $name, $parameters) {
0 ignored issues
show
Unused Code introduced by
The import $parameters is not used and could be removed.

This check looks for imports that have been defined, but are not used in the scope.

Loading history...
161 1
                return $that->autoWireClass($name);
162 1
            };
163
        }
164
165 48
        return $this->autoWireClass($name, $parameters);
166
    }
167
168
    /**
169
     * Auto wire.
170
     */
171 49
    protected function autoWireClass(string $name, ?array $parameters = null)
172
    {
173 49
        $config = $this->config->get($name);
174 49
        $class = $config['use'];
175
176 49
        if (null !== $parameters) {
177 3
            $config['singleton'] = false;
178
        }
179
180 49
        if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) {
181 1
            return $this->wireReference($name, $match[1], $config);
182
        }
183
184 49
        $reflection = new ReflectionClass($class);
185
186 49
        if (isset($config['factory'])) {
187 3
            $factory = $reflection->getMethod($config['factory']);
188 3
            $args = $this->autoWireMethod($name, $factory, $config, $parameters);
189 3
            $instance = call_user_func_array([$class, $config['factory']], $args);
190
191 3
            return $this->prepareService($name, $instance, $reflection, $config);
192
        }
193
194 46
        $constructor = $reflection->getConstructor();
195
196 46
        if (null === $constructor) {
197 3
            return $this->storeService($name, $config, new $class(), is_null($parameters));
0 ignored issues
show
Unused Code introduced by
The call to Micro\Container\RuntimeContainer::storeService() has too many arguments starting with is_null($parameters). ( Ignorable by Annotation )

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

197
            return $this->/** @scrutinizer ignore-call */ storeService($name, $config, new $class(), is_null($parameters));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
198
        }
199
200 43
        $args = $this->autoWireMethod($name, $constructor, $config, $parameters);
201
202 34
        return $this->createInstance($name, $reflection, $args, $config, is_null($parameters));
0 ignored issues
show
Unused Code introduced by
The call to Micro\Container\RuntimeContainer::createInstance() has too many arguments starting with is_null($parameters). ( Ignorable by Annotation )

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

202
        return $this->/** @scrutinizer ignore-call */ createInstance($name, $reflection, $args, $config, is_null($parameters));

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
203
    }
204
205
    /**
206
     * Wire named referenced service.
207
     */
208 1
    protected function wireReference(string $name, string $reference, array $config)
209
    {
210 1
        $service = $this->get($reference);
211 1
        $reflection = new ReflectionClass(get_class($service));
212 1
        $config = $this->config->get($name);
213 1
        $service = $this->prepareService($name, $service, $reflection, $config);
214
215 1
        return $service;
216
    }
217
218
    /**
219
     * Get instance (virtual or real instance).
220
     */
221 34
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
222
    {
223 34
        if (true === $config['lazy']) {
224 2
            return $this->getProxyInstance($name, $class, $arguments, $config);
225
        }
226
227 32
        return $this->getRealInstance($name, $class, $arguments, $config);
228
    }
229
230
    /**
231
     * Create proxy instance.
232
     */
233 2
    protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config)
234
    {
235 2
        $factory = new LazyLoadingValueHolderFactory();
236 2
        $that = $this;
237
238 2
        return $factory->createProxy(
239 2
            $class->getName(),
240 2
            function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) {
241 1
                $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config);
242 1
                $initializer = null;
243 2
            }
244
        );
245
    }
246
247
    /**
248
     * Create real instance.
249
     */
250 33
    protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config)
251
    {
252 33
        $instance = $class->newInstanceArgs($arguments);
253 33
        $instance = $this->prepareService($name, $instance, $class, $config);
254
255 33
        return $instance;
256
    }
257
258
    /**
259
     * Prepare service (execute sub selects and excute setter injections).
260
     */
261 36
    protected function prepareService(string $name, $service, ReflectionClass $class, array $config)
262
    {
263 36
        $this->storeService($name, $config, $service);
264
265 36
        foreach ($config['calls'] as $call) {
266 7
            if (!is_array($call)) {
267 1
                continue;
268
            }
269
270 7
            if (!isset($call['method'])) {
271
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
272
            }
273
274 7
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
275
276
            try {
277 7
                $method = $class->getMethod($call['method']);
278
            } catch (\ReflectionException $e) {
279
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
280
            }
281
282 7
            $arguments = $this->autoWireMethod($name, $method, $call);
283 7
            $result = call_user_func_array([&$service, $call['method']], $arguments);
284
285 7
            if (isset($call['select']) && true === $call['select']) {
286 7
                $service = $result;
287
            }
288
        }
289
290 36
        return $service;
291
    }
292
293
    /**
294
     * Autowire method.
295
     */
296 46
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config, ?array $parameters = null): array
297
    {
298 46
        $params = $method->getParameters();
299 46
        $args = [];
300
301 46
        foreach ($params as $param) {
302 46
            $type = $param->getClass();
303 46
            $param_name = $param->getName();
304
305 46
            if (isset($parameters[$param_name])) {
306 3
                $args[$param_name] = $parameters[$param_name];
307 45
            } elseif (isset($config['arguments'][$param_name])) {
308 32
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
309 21
            } elseif (null !== $type) {
310 10
                $args[$param_name] = $this->resolveServiceArgument($name, $type, $param);
311 13
            } elseif ($param->isDefaultValueAvailable()) {
312 7
                $args[$param_name] = $param->getDefaultValue();
313 7
            } elseif ($param->allowsNull()) {
314 1
                $args[$param_name] = null;
315
            } else {
316 6
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
317
            }
318
319 37
            if (!$param->canBePassedByValue()) {
320 2
                $value = &$args[$param_name];
321 37
                $args[$param_name] = &$value;
322
            }
323
        }
324
325 37
        return $args;
326
    }
327
328
    /**
329
     * Resolve service argument.
330
     */
331 10
    protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param)
332
    {
333 10
        $type_class = $type->getName();
334
335 10
        if ($type_class === $name) {
336 1
            throw new RuntimeException('class '.$type_class.' can not depend on itself');
337
        }
338
339
        try {
340 9
            return $this->traverseTree($name, $type_class);
341 2
        } catch (\Exception $e) {
342 2
            if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) {
343 1
                return null;
344
            }
345
346 1
            throw $e;
347
        }
348
    }
349
350
    /**
351
     * Parse param value.
352
     */
353 32
    protected function parseParam($param, string $name)
354
    {
355 32
        if (is_iterable($param)) {
356
            foreach ($param as $key => $value) {
357
                $param[$key] = $this->parseParam($value, $name);
358
            }
359
360
            return $param;
361
        }
362
363 32
        if (is_string($param)) {
364 31
            $param = $this->config->getEnv($param);
365
366 30
            if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) {
367 1
                return '{'.$matches[1].'}';
368
            }
369 29
            if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) {
370 1
                return $this->traverseTree($name, $matches[1]);
371
            }
372
373 29
            return $param;
374
        }
375
376 1
        return $param;
377
    }
378
379
    /**
380
     * Locate service.
381
     */
382 10
    protected function traverseTree(string $current_service, string $service)
383
    {
384 10
        if (isset($this->children[$current_service])) {
385 1
            return $this->children[$current_service]->get($service);
386
        }
387
388 10
        $config = $this->config->get($current_service);
389 10
        if (isset($config['services'])) {
390 2
            $this->children[$current_service] = new self($config['services'], $this, $this->service[ContainerInterface::class]);
391
392 2
            return $this->children[$current_service]->get($service);
393
        }
394
395 9
        return $this->get($service);
396
    }
397
}
398