Completed
Push — master ( 3c9296...c4901b )
by Raffael
02:19
created

Container::getRealInstance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 4
dl 0
loc 6
ccs 4
cts 4
cp 1
crap 1
rs 9.4285
c 0
b 0
f 0
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 Closure;
15
use ProxyManager\Factory\LazyLoadingValueHolderFactory;
16
use Psr\Container\ContainerInterface;
17
use ReflectionClass;
18
use ReflectionMethod;
19
20
class Container implements ContainerInterface
21
{
22
    /**
23
     * Config.
24
     *
25
     * @var Config
26
     */
27
    protected $config;
28
29
    /**
30
     * Service registry.
31
     *
32
     * @var array
33
     */
34
    protected $service = [];
35
36
    /**
37
     * Registered but not initialized service registry.
38
     *
39
     * @var array
40
     */
41
    protected $registry = [];
42
43
    /**
44
     * Parent container.
45
     *
46
     * @var ContainerInterface
47
     */
48
    protected $parent;
49
50
    /**
51
     * Children container.
52
     *
53
     * @var ContainerInterface[]
54
     */
55
    protected $children = [];
56
57
    /**
58
     * Parent service.
59
     *
60
     * @var mixed
61
     */
62
    protected $parent_service;
63
64
    /**
65
     * Create container.
66
     *
67
     * @param iterable           $config
68
     * @param ContainerInterface $parent
69
     */
70 40
    public function __construct(Iterable $config = [], ?ContainerInterface $parent = null)
71
    {
72 40
        $this->config = new Config($config, $this);
73 40
        $this->parent = $parent;
74 40
        $this->service[ContainerInterface::class] = $this;
75 40
    }
76
77
    /**
78
     * Get service.
79
     *
80
     * @param string $name
81
     *
82
     * @return mixed
83
     */
84 31
    public function get($name)
85
    {
86
        try {
87 31
            return $this->resolve($name);
88 12
        } catch (Exception\ServiceNotFound $e) {
89 9
            return $this->autoWireClass($name);
90
        }
91
    }
92
93
    /**
94
     * Get parent container.
95
     *
96
     * @return ContainerInterface
97
     */
98 31
    public function getParent(): ?ContainerInterface
99
    {
100 31
        return $this->parent;
101
    }
102
103
    /**
104
     * Add service.
105
     *
106
     * @param string $name
107
     * @param mixed  $service
108
     *
109
     * @return Container
110
     */
111 3
    public function add(string $name, $service): self
112
    {
113 3
        if ($this->has($name)) {
114 1
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
115
        }
116
117 3
        $this->registry[$name] = $service;
118
119 3
        return $this;
120
    }
121
122
    /**
123
     * Check if service is registered.
124
     *
125
     * @param mixed $name
126
     *
127
     * @return bool
128
     */
129 32
    public function has($name): bool
130
    {
131 32
        return isset($this->service[$name]);
132
    }
133
134
    /**
135
     * Set parent service on container
136
     * (Used internally, there is no point to call this method directly).
137
     *
138
     * @param mixed $service
139
     *
140
     * @return ContainerInterface
141
     */
142 1
    public function setParentService($service): ContainerInterface
143
    {
144 1
        $this->parent_service = $service;
145
146 1
        return $this;
147
    }
148
149
    /**
150
     * Get config.
151
     *
152
     * @return Config
153
     */
154 1
    public function getConfig(): Config
155
    {
156 1
        return $this->config;
157
    }
158
159
    /**
160
     * Resolve service.
161
     *
162
     * @param string $name
163
     *
164
     * @return mixed
165
     */
166 31
    public function resolve(string $name)
167
    {
168 31
        if ($this->has($name)) {
169 2
            return $this->service[$name];
170
        }
171
172 31
        if (isset($this->registry[$name])) {
173 3
            return $this->addStaticService($name);
174
        }
175
176 28
        if ($this->config->has($name)) {
177 25
            return $this->autoWireClass($name);
178
        }
179
180 9
        if (null !== $this->parent_service) {
181
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
182
183
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
184
                return $this->parent_service;
185
            }
186
        }
187
188 9
        if (null !== $this->parent) {
189
            return $this->parent->lookupService($name);
0 ignored issues
show
Bug introduced by
The method lookupService() 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

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