Completed
Push — master ( a5918e...ee8ab2 )
by Raffael
07:32
created

Container::has()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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 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 37
    public function __construct(Iterable $config = [], ?ContainerInterface $parent = null)
70
    {
71 37
        $this->config = new Config($config, $this);
72 37
        $this->parent = $parent;
73 37
        $this->service[ContainerInterface::class] = $this;
74 37
    }
75
76
    /**
77
     * Get service.
78
     *
79
     * @param string $name
80
     *
81
     * @return mixed
82
     */
83 28
    public function get($name)
84
    {
85
        //$service = $this->resolve($name);
86
        /*if (null !== $service) {
87
            return $service;
88
        }*/
89
90
        try {
91 28
            return $this->resolve($name);
92
            //return $this->lookupService($name);
93 11
        } catch (Exception\ServiceNotFound $e) {
94 8
            return $this->autoWireClass($name);
95
        }
96
    }
97
98
    /**
99
     * Traverse tree up and look for service.
100
     *
101
     * @param string $name
102
     *
103
     * @return mixed
104
     */
105
    /*public function lookupService(string $name)
106
    {
107
        $service = $this->resolve($name);
108
        if (null !== $service) {
109
            return $service;
110
        }
111
112
        if (null !== $this->parent) {
113
            return $this->parent->lookupService($name);
114
        }
115
116
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
117
    }*/
118
119
    /**
120
     * Get parent container.
121
     *
122
     * @return ContainerInterface
123
     */
124 28
    public function getParent(): ?ContainerInterface
125
    {
126 28
        return $this->parent;
127
    }
128
129
    /**
130
     * Add service.
131
     *
132
     * @param string $name
133
     * @param mixed  $service
134
     *
135
     * @return Container
136
     */
137 3
    public function add(string $name, $service): self
138
    {
139 3
        if ($this->has($name)) {
140 1
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
141
        }
142
143 3
        $this->registry[$name] = $service;
144
145 3
        return $this;
146
    }
147
148
    /**
149
     * Check if service is registered.
150
     *
151
     * @param mixed $name
152
     *
153
     * @return bool
154
     */
155 29
    public function has($name): bool
156
    {
157 29
        return isset($this->service[$name]);
158
    }
159
160
    /**
161
     * Set parent service on container
162
     * (Used internally, there is no point to call this method directly).
163
     *
164
     * @param mixed $service
165
     *
166
     * @return ContainerInterface
167
     */
168 1
    public function setParentService($service): ContainerInterface
169
    {
170 1
        $this->parent_service = $service;
171
172 1
        return $this;
173
    }
174
175
    /**
176
     * Get config.
177
     *
178
     * @return Config
179
     */
180 1
    public function getConfig(): Config
181
    {
182 1
        return $this->config;
183
    }
184
185
    /**
186
     * Resolve service.
187
     *
188
     * @param string $name
189
     *
190
     * @return mixed
191
     */
192 28
    public function resolve(string $name)
193
    {
194 28
        if ($this->has($name)) {
195 2
            return $this->service[$name];
196
        }
197
198 28
        if (isset($this->registry[$name])) {
199 3
            return $this->addStaticService($name);
200
        }
201
202 25
        if ($this->config->has($name)) {
203 22
            return $this->autoWireClass($name);
204
        }
205
206 8
        if (null !== $this->parent_service) {
207
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
208
209
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
210
                return $this->parent_service;
211
            }
212
        }
213
214 8
        if (null !== $this->parent) {
215
            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

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