Passed
Branch master (a5918e)
by Raffael
02:18
created

Container::getProxyInstance()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 7
nc 1
nop 4
dl 0
loc 10
ccs 7
cts 7
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
7
 *
8
 * @copyright   Copryright (c) 2015-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 Psr\Container\ContainerInterface;
16
use ReflectionClass;
17
use ReflectionMethod;
18
19
class Container implements ContainerInterface
20
{
21
    /**
22
     * Config.
23
     *
24
     * @var Config
25
     */
26
    protected $config;
27
28
    /**
29
     * Service registry.
30
     *
31
     * @var array
32
     */
33
    protected $service = [];
34
35
    /**
36
     * Registered but not initialized service registry.
37
     *
38
     * @var array
39
     */
40
    protected $registry = [];
41
42
    /**
43
     * Parent container.
44
     *
45
     * @var ContainerInterface
46
     */
47
    protected $parent;
48
49
    /**
50
     * Children container.
51
     *
52
     * @var ContainerInterface[]
53
     */
54
    protected $children = [];
55
56
    /**
57
     * Parent service.
58
     *
59
     * @var mixed
60
     */
61
    protected $parent_service;
62
63
    /**
64
     * Create container.
65
     *
66
     * @param iterable           $config
67
     * @param ContainerInterface $parent
68
     */
69 21
    public function __construct(Iterable $config = [], ?ContainerInterface $parent = null)
70
    {
71 21
        $this->config = new Config($config, $this);
72 21
        $this->parent = $parent;
73 21
        $this->service[ContainerInterface::class] = $this;
74 21
    }
75
76
    /**
77
     * Get service.
78
     *
79
     * @param string $name
80
     *
81
     * @return mixed
82
     */
83 20
    public function get($name)
84
    {
85 20
        $service = $this->resolve($name);
86 18
        if (null !== $service) {
87 16
            return $service;
88
        }
89
90
        try {
91 7
            return $this->lookupService($name);
92 7
        } catch (Exception\ServiceNotFound $e) {
93 7
            return $this->autoWireClass($name);
94
        }
95
    }
96
97
    /**
98
     * Traverse tree up and look for service.
99
     *
100
     * @param string $name
101
     *
102
     * @return mixed
103
     */
104 7
    public function lookupService(string $name)
105
    {
106 7
        $service = $this->resolve($name);
107 7
        if (null !== $service) {
108
            return $service;
109
        }
110
111 7
        if (null !== $this->parent) {
112
            return $this->parent->lookupService($name);
113
        }
114
115 7
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
116
    }
117
118
    /**
119
     * Get parent container.
120
     *
121
     * @return ContainerInterface
122
     */
123
    public function getParent(): ?ContainerInterface
124
    {
125
        return $this->parent;
126 3
    }
127
128 3
    /**
129 1
     * Add service.
130
     *
131
     * @param string $name
132 3
     * @param mixed  $service
133
     *
134 3
     * @return Container
135
     */
136
    public function add(string $name, $service): self
137
    {
138
        if ($this->has($name)) {
139
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
140
        }
141
142
        $this->registry[$name] = $service;
143
144 21
        return $this;
145
    }
146 21
147
    /**
148
     * Check if service is registered.
149
     *
150
     * @param mixed $name
151
     *
152
     * @return bool
153
     */
154
    public function has($name): bool
155
    {
156
        return isset($this->service[$name]);
157
    }
158
159
    /**
160
     * Set parent service on container
161
     * (Used internally, there is no point to call this method directly).
162
     *
163
     * @param mixed $service
164
     *
165
     * @return ContainerInterface
166
     */
167
    public function setParentService($service): ContainerInterface
168
    {
169
        $this->parent_service = $service;
170
171 20
        return $this;
172
    }
173 20
174 2
    /**
175
     * Get config.
176
     *
177 20
     * @return Config
178 3
     */
179
    public function getConfig(): Config
180
    {
181 17
        return $this->config;
182 14
    }
183
184
    /**
185 7
     * Resolve service.
186
     *
187
     * @param string $name
188
     *
189
     * @return mixed
190
     */
191
    protected function resolve(string $name)
192
    {
193 7
        if ($this->has($name)) {
194
            return $this->service[$name];
195
        }
196
197
        if (isset($this->registry[$name])) {
198
            return $this->addStaticService($name);
199
        }
200
201
        if ($this->config->has($name)) {
202
            return $this->autoWireClass($name);
203 3
        }
204
205 3
        if (null !== $this->parent_service) {
206 1
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
207
208 2
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
209
                return $this->parent_service;
210
            }
211 3
        }
212
213 3
        return null;
214
    }
215
216
    /**
217
     * Check for static injections.
218
     *
219
     * @param string $name
220
     *
221
     * @return mixed
222
     */
223
    protected function addStaticService(string $name)
224
    {
225 17
        if ($this->registry[$name] instanceof Closure) {
226
            $this->service[$name] = $this->registry[$name]->call($this);
227 17
        } else {
228 16
            $this->service[$name] = $this->registry[$name];
229
        }
230 16
231
        unset($this->registry[$name]);
232
233
        return $this->service[$name];
234
    }
235 16
236 1
    /**
237 1
     * Auto wire.
238
     *
239
     * @param string $name
240 15
     * @param array  $config
241
     * @param array  $parents
242 15
     *
243 3
     * @return mixed
244
     */
245
    protected function autoWireClass(string $name)
246 12
    {
247
        $config = $this->config->get($name);
248 12
        $class = $config['use'];
249
250
        if (preg_match('#^\{(.*)\}$#', $class, $match)) {
251
            return $this->wireReference($name, $match[1], $config);
252
        }
253
254
        try {
255
            $reflection = new ReflectionClass($class);
256
        } catch (\Exception $e) {
257
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
258
        }
259
260
        $constructor = $reflection->getConstructor();
261
262
        if (null === $constructor) {
263
            return $this->storeService($name, $config, new $class());
264
        }
265
266
        $args = $this->autoWireMethod($name, $constructor, $config);
267
268
        return $this->createInstance($name, $reflection, $args, $config);
269
    }
270
271
    /**
272
     * Wire named referenced service.
273
     *
274
     * @param string $name
275
     * @param string $refrence
276
     * @param array  $config
277
     *
278
     * @return mixed
279
     */
280
    protected function wireReference(string $name, string $reference, array $config)
281
    {
282
        $service = $this->get($reference);
283
284
        if (isset($config['selects'])) {
285 15
            $reflection = new ReflectionClass(get_class($service));
286
287 15
            foreach ($config['selects'] as $select) {
288 2
                $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
289
                $service = call_user_func_array([&$service, $select['method']], $args);
290 14
            }
291
        }
292 14
293
        return $this->storeService($name, $config, $service);
294
    }
295
296 14
    /**
297
     * Store service.
298
     *
299
     * @param param string $name
300
     * @param array        $config
301
     * @param mixed        $service
302
     *
303
     * @return mixed
304
     */
305
    protected function storeService(string $name, array $config, $service)
306
    {
307
        if (isset($config['singleton']) && true === $config['singleton']) {
308
            return $service;
309 12
        }
310
        $this->service[$name] = $service;
311 12
312 12
        if (isset($this->children[$name])) {
313 12
            $this->children[$name]->setParentService($service);
314
        }
315 12
316 9
        return $service;
317
    }
318
319 3
    /**
320 3
     * Get instance (virtual or real instance).
321 1
     *
322
     * @param string          $name
323
     * @param ReflectionClass $class
324 2
     * @param array           $arguments
325
     * @param array           $config
326
     *
327 2
     * @return mixed
328
     */
329
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
330
    {
331
        if (isset($config['lazy']) && true === $config['lazy']) {
332 2
            return $this->getProxyInstance($name, $class, $arguments, $config);
333 2
        }
334
335
        return $this->getRealInstance($name, $class, $arguments, $config);
336 2
    }
337
338
    /**
339
     * Create proxy instance.
340
     *
341
     * @param string          $name
342
     * @param ReflectionClass $class
343
     * @param array           $arguments
344
     * @param array           $config
345
     *
346
     * @return mixed
347
     */
348 12
    protected function getProxyInstance(string $name, ReflectionClass $class, array $arguments, array $config)
349
    {
350 12
        $factory = new \ProxyManager\Factory\LazyLoadingValueHolderFactory();
351 12
        $that = $this;
352
353 12
        return $factory->createProxy(
354 12
            $class->getName(),
355 12
            function (&$wrappedObject, $proxy, $method, $parameters, &$initializer) use ($that, $name,$class,$arguments,$config) {
356
                $wrappedObject = $that->getRealInstance($name, $class, $arguments, $config);
357 12
                $initializer = null;
358 12
            }
359 7
        );
360 4
    }
361
362 4
    /**
363
     * Create real instance.
364
     *
365
     * @param string          $name
366 4
     * @param ReflectionClass $class
367 3
     * @param array           $arguments
368 3
     * @param array           $config
369
     *
370
     * @return mixed
371
     */
372 12
    protected function getRealInstance(string $name, ReflectionClass $class, array $arguments, array $config)
373
    {
374
        $instance = $class->newInstanceArgs($arguments);
375
        $this->storeService($name, $config, $instance);
376 12
        $config = $this->config->get($name);
377
378
        if (!isset($config['calls'])) {
379
            return $instance;
380
        }
381
382
        foreach ($config['calls'] as $call) {
383
            if (!isset($call['method'])) {
384
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
385
            }
386
387 12
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
388
389 12
            try {
390
                $method = $class->getMethod($call['method']);
391
            } catch (\ReflectionException $e) {
392
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
393
            }
394
395
            $arguments = $this->autoWireMethod($name, $method, $call);
396
            call_user_func_array([&$instance, $call['method']], $arguments);
397 12
        }
398 12
399
        return $instance;
400 12
    }
401
402
    /**
403 12
     * Autowire method.
404
     *
405
     * @param string           $name
406
     * @param ReflectionMethod $method
407 12
     * @param array            $config
408
     *
409
     * @return array
410
     */
411
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
412
    {
413
        $params = $method->getParameters();
414
        $args = [];
415
416
        foreach ($params as $param) {
417
            $type = $param->getClass();
418
            $param_name = $param->getName();
419 4
420
            if (isset($config['arguments'][$param_name])) {
421 4
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
422
            } elseif (null !== $type) {
423
                $type_class = $type->getName();
424
425 4
                if ($type_class === $name) {
426 4
                    throw new Exception\InvalidConfiguration('class '.$type_class.' can not depend on itself');
427
                }
428
429
                $args[$param_name] = $this->findService($name, $type_class);
430
            } elseif ($param->isDefaultValueAvailable()) {
431
                $args[$param_name] = $param->getDefaultValue();
432 4
            } elseif ($param->allowsNull() && $param->hasType()) {
433
                $args[$param_name] = null;
434
            } else {
435
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
436
            }
437
        }
438
439
        return $args;
440
    }
441
442
    /**
443
     * Parse param value.
444
     *
445
     * @param mixed  $param
446
     * @param string $name
447
     *
448
     * @return mixed
449
     */
450
    protected function parseParam($param, string $name)
451
    {
452
        if (is_iterable($param)) {
453
            foreach ($param as $key => $value) {
454
                $param[$key] = $this->parseParam($value, $name);
455
            }
456
457
            return $param;
458
        }
459
460
        if (is_string($param)) {
461
            $param = $this->config->getEnv($param);
462
463
            if (preg_match('#^\{\{([^{}]+)\}\}$#', $param, $matches)) {
464
                return '{'.$matches[1].'}';
465
            }
466
            if (preg_match('#^\{([^{}]+)\}$#', $param, $matches)) {
467
                return $this->findService($name, $matches[1]);
468
            }
469
470
            return $param;
471
        }
472
473
        return $param;
474
    }
475
476
    /**
477
     * Locate service.
478
     *
479
     * @param string $current_service
480
     * @param string $service
481
     */
482
    protected function findService(string $current_service, string $service)
483
    {
484
        if (isset($this->children[$current_service])) {
485
            return $this->children[$current_service]->get($service);
486
        }
487
488
        $config = $this->config->get($current_service);
489
        if (isset($config['services'])) {
490
            $this->children[$current_service] = new self($config['services'], $this);
491
492
            return $this->children[$current_service]->get($service);
493
        }
494
495
        return $this->get($service);
496
    }
497
}
498