Completed
Push — master ( 00e2c8...42293a )
by Raffael
05:46 queued 03:44
created

RuntimeContainer::autoWireMethod()   B

Complexity

Conditions 7
Paths 10

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.0071

Importance

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

135
            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...
136
        }
137
138 14
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
139
    }
140
141
    /**
142
     * Store service.
143
     */
144 42
    protected function storeService(string $name, array $config, $service)
145
    {
146 42
        if (true === $config['singleton']) {
147 2
            return $service;
148
        }
149 41
        $this->service[$name] = $service;
150
151 41
        if (isset($this->children[$name])) {
152 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

152
            $this->children[$name]->/** @scrutinizer ignore-call */ 
153
                                    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...
153
        }
154
155 41
        return $service;
156
    }
157
158
    /**
159
     * Wrap resolved service in callable if enabled.
160
     */
161 49
    protected function wrapService(string $name)
162
    {
163 49
        $config = $this->config->get($name);
164 46
        if (true === $config['wrap']) {
165 1
            $that = $this;
166
167 1
            return function () use ($that, $name) {
168 1
                return $that->autoWireClass($name);
169 1
            };
170
        }
171
172 45
        return $this->autoWireClass($name);
173
    }
174
175
    /**
176
     * Auto wire.
177
     */
178 46
    protected function autoWireClass(string $name)
179
    {
180 46
        $config = $this->config->get($name);
181 46
        $class = $config['use'];
182
183 46
        if (preg_match('#^\{([^{}]+)\}$#', $class, $match)) {
184 1
            return $this->wireReference($name, $match[1], $config);
185
        }
186
187 46
        $reflection = new ReflectionClass($class);
188
189 46
        if (isset($config['factory'])) {
190 3
            $factory = $reflection->getMethod($config['factory']);
191 3
            $args = $this->autoWireMethod($name, $factory, $config);
192 3
            $instance = call_user_func_array([$class, $config['factory']], $args);
193
194 3
            return $this->prepareService($name, $instance, $reflection, $config);
195
        }
196
197 43
        $constructor = $reflection->getConstructor();
198
199 43
        if (null === $constructor) {
200 3
            return $this->storeService($name, $config, new $class());
201
        }
202
203 40
        $args = $this->autoWireMethod($name, $constructor, $config);
204
205 37
        return $this->createInstance($name, $reflection, $args, $config);
206
    }
207
208
    /**
209
     * Wire named referenced service.
210
     */
211 1
    protected function wireReference(string $name, string $reference, array $config)
212
    {
213 1
        $service = $this->get($reference);
214 1
        $reflection = new ReflectionClass(get_class($service));
215 1
        $config = $this->config->get($name);
216 1
        $service = $this->prepareService($name, $service, $reflection, $config);
217
218 1
        return $service;
219
    }
220
221
    /**
222
     * Get instance (virtual or real instance).
223
     */
224 37
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
225
    {
226 37
        if (true === $config['lazy']) {
227 2
            return $this->getProxyInstance($name, $class, $arguments, $config);
228
        }
229
230 35
        return $this->getRealInstance($name, $class, $arguments, $config);
231
    }
232
233
    /**
234
     * Create proxy instance.
235
     */
236 2
    protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config)
237
    {
238 2
        $factory = new LazyLoadingValueHolderFactory();
239 2
        $that = $this;
240
241 2
        return $factory->createProxy(
242 2
            $class->getName(),
243 2
            function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) {
244 1
                $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config);
245 1
                $initializer = null;
246 2
            }
247
        );
248
    }
249
250
    /**
251
     * Create real instance.
252
     */
253 36
    protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config)
254
    {
255 36
        $instance = $class->newInstanceArgs($arguments);
256 36
        $instance = $this->prepareService($name, $instance, $class, $config);
257
258 34
        return $instance;
259
    }
260
261
    /**
262
     * Prepare service (execute sub selects and excute setter injections).
263
     */
264 39
    protected function prepareService(string $name, $service, ReflectionClass $class, array $config)
265
    {
266
        //This is deprecated as of v2.0.2 and gets removed in 3.0.0
267 39
        foreach ($config['selects'] as $select) {
268 2
            $args = $this->autoWireMethod($name, $class->getMethod($select['method']), $select);
269 2
            $service = call_user_func_array([&$service, $select['method']], $args);
270
        }
271
272 39
        $this->storeService($name, $config, $service);
273
274 39
        foreach ($config['calls'] as $call) {
275 10
            if (!is_array($call)) {
276 1
                continue;
277
            }
278
279 10
            if (!isset($call['method'])) {
280 1
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
281
            }
282
283 9
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
284
285
            try {
286 9
                $method = $class->getMethod($call['method']);
287 1
            } catch (\ReflectionException $e) {
288 1
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
289
            }
290
291 8
            $arguments = $this->autoWireMethod($name, $method, $call);
292 8
            $result = call_user_func_array([&$service, $call['method']], $arguments);
293
294 8
            if (isset($call['select']) && true === $call['select']) {
295 8
                $service = $result;
296
            }
297
        }
298
299 37
        return $service;
300
    }
301
302
    /**
303
     * Autowire method.
304
     */
305 43
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
306
    {
307 43
        $params = $method->getParameters();
308 43
        $args = [];
309
310 43
        foreach ($params as $param) {
311 43
            $type = $param->getClass();
312 43
            $param_name = $param->getName();
313
314 43
            if (isset($config['arguments'][$param_name])) {
315 36
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
316 20
            } elseif (null !== $type) {
317 10
                $args[$param_name] = $this->resolveServiceArgument($name, $type, $param);
318 12
            } elseif ($param->isDefaultValueAvailable()) {
319 12
                $args[$param_name] = $param->getDefaultValue();
320 1
            } elseif ($param->allowsNull()) {
321 1
                $args[$param_name] = null;
322
            } else {
323
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
324
            }
325
326 40
            if (!$param->canBePassedByValue()) {
327 2
                $value = &$args[$param_name];
328 40
                $args[$param_name] = &$value;
329
            }
330
        }
331
332 40
        return $args;
333
    }
334
335
    /**
336
     * Resolve service argument.
337
     */
338 10
    protected function resolveServiceArgument(string $name, ReflectionClass $type, ReflectionParameter $param)
339
    {
340 10
        $type_class = $type->getName();
341
342 10
        if ($type_class === $name) {
343 1
            throw new RuntimeException('class '.$type_class.' can not depend on itself');
344
        }
345
346
        try {
347 9
            return $this->traverseTree($name, $type_class);
348 2
        } catch (\Exception $e) {
349 2
            if ($param->isDefaultValueAvailable() && null === $param->getDefaultValue()) {
350 1
                return null;
351
            }
352
353 1
            throw $e;
354
        }
355
    }
356
357
    /**
358
     * Parse param value.
359
     */
360 36
    protected function parseParam($param, string $name)
361
    {
362 36
        if (is_iterable($param)) {
363 1
            foreach ($param as $key => $value) {
364 1
                $param[$key] = $this->parseParam($value, $name);
365
            }
366
367 1
            return $param;
368
        }
369
370 36
        if (is_string($param)) {
371 35
            $param = $this->config->getEnv($param);
372
373 34
            if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) {
374 1
                return '{'.$matches[1].'}';
375
            }
376 33
            if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) {
377 1
                return $this->traverseTree($name, $matches[1]);
378
            }
379
380 33
            return $param;
381
        }
382
383 1
        return $param;
384
    }
385
386
    /**
387
     * Locate service.
388
     */
389 10
    protected function traverseTree(string $current_service, string $service)
390
    {
391 10
        if (isset($this->children[$current_service])) {
392 1
            return $this->children[$current_service]->get($service);
393
        }
394
395 10
        $config = $this->config->get($current_service);
396 10
        if (isset($config['services'])) {
397 2
            $this->children[$current_service] = new self($config['services'], $this, $this->service[ContainerInterface::class]);
398
399 2
            return $this->children[$current_service]->get($service);
400
        }
401
402 9
        return $this->get($service);
403
    }
404
}
405