Passed
Push — master ( 81725e...2d0b63 )
by Raffael
02:13
created

Container::wireReference()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 2
nop 3
dl 0
loc 14
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
    public function __construct(Iterable $config = [], ?ContainerInterface $parent = null)
70
    {
71
        $this->config = new Config($config);
72
        $this->parent = $parent;
73
        $this->add(ContainerInterface::class, $this);
74
    }
75
76
    /**
77
     * Get service.
78
     *
79
     * @param string $name
80
     *
81
     * @return mixed
82
     */
83
    public function get($name)
84
    {
85
        $service = $this->resolve($name);
86
        if (null !== $service) {
87
            return $service;
88
        }
89
90
        try {
91
            return $this->lookupService($name);
92
        } catch (Exception\ServiceNotFound $e) {
93
            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
    public function lookupService(string $name)
105
    {
106
        $service = $this->resolve($name);
107
        if (null !== $service) {
108
            return $service;
109
        }
110
111
        if (null !== $this->parent) {
112
            return $this->parent->lookupService($name);
113
        }
114
115
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
116
    }
117
118
    /**
119
     * Add service.
120
     *
121
     * @param string $name
122
     * @param mixed  $service
123
     *
124
     * @return Container
125
     */
126
    public function add(string $name, $service): self
127
    {
128
        if ($this->has($name)) {
129
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
130
        }
131
132
        $this->registry[$name] = $service;
133
134
        return $this;
135
    }
136
137
    /**
138
     * Check if service is registered.
139
     *
140
     * @param mixed $name
141
     *
142
     * @return bool
143
     */
144
    public function has($name): bool
145
    {
146
        return isset($this->service[$name]);
147
    }
148
149
    /**
150
     * Set parent service on container
151
     * (Used internally, there is no point to call this method directly).
152
     *
153
     * @param mixed $service
154
     *
155
     * @return ContainerInterface
156
     */
157
    public function setParentService($service): ContainerInterface
158
    {
159
        $this->parent_service = $service;
160
161
        return $this;
162
    }
163
164
    /**
165
     * Resolve service.
166
     *
167
     * @param string $name
168
     *
169
     * @return mixed
170
     */
171
    protected function resolve(string $name)
172
    {
173
        if ($this->has($name)) {
174
            return $this->service[$name];
175
        }
176
177
        if (isset($this->registry[$name])) {
178
            return $this->addStaticService($name);
179
        }
180
181
        if ($this->config->has($name)) {
182
            return $this->autoWireClass($name);
183
        }
184
185
        if (null !== $this->parent_service) {
186
            $parents = array_merge([$name], class_implements($this->parent_service), class_parents($this->parent_service));
187
188
            if (in_array($name, $parents, true) && $this->parent_service instanceof $name) {
189
                return $this->parent_service;
190
            }
191
        }
192
193
        return null;
194
    }
195
196
    /**
197
     * Check for static injections.
198
     *
199
     * @param string $name
200
     *
201
     * @return mixed
202
     */
203
    protected function addStaticService(string $name)
204
    {
205
        if ($this->registry[$name] instanceof Closure) {
206
            $this->service[$name] = $this->registry[$name]->call($this);
207
        } else {
208
            $this->service[$name] = $this->registry[$name];
209
        }
210
211
        unset($this->registry[$name]);
212
213
        return $this->service[$name];
214
    }
215
216
    /**
217
     * Auto wire.
218
     *
219
     * @param string $name
220
     * @param array  $config
221
     * @param array  $parents
222
     *
223
     * @return mixed
224
     */
225
    protected function autoWireClass(string $name)
226
    {
227
        $config = $this->config->get($name);
228
        $class = $config['use'];
229
230
        if (preg_match('#^\{(.*)\}$#', $class, $match)) {
231
            return $this->wireReference($name, $match[1], $config);
232
        }
233
234
        try {
235
            $reflection = new ReflectionClass($class);
236
        } catch (\Exception $e) {
237
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
238
        }
239
240
        $constructor = $reflection->getConstructor();
241
242
        if (null === $constructor) {
243
            return new $class();
244
        }
245
246
        $args = $this->autoWireMethod($name, $constructor, $config);
247
248
        return $this->createInstance($name, $reflection, $args, $config);
249
    }
250
251
    /**
252
     * Wire named referenced service.
253
     *
254
     * @param string $name
255
     * @param string $refrence
256
     * @param array  $config
257
     *
258
     * @return mixed
259
     */
260
    protected function wireReference(string $name, string $reference, array $config)
261
    {
262
        $service = $this->get($reference);
263
264
        if (isset($config['selects'])) {
265
            $reflection = new ReflectionClass(get_class($service));
266
267
            foreach ($config['selects'] as $select) {
268
                $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
269
                $service = call_user_func_array([&$service, $select['method']], $args);
270
            }
271
        }
272
273
        return $this->storeService($name, $config, $service);
274
    }
275
276
    /**
277
     * Store service.
278
     *
279
     * @param param string $name
280
     * @param array        $config
281
     * @param mixed        $service
282
     *
283
     * @return mixed
284
     */
285
    protected function storeService(string $name, array $config, $service)
286
    {
287
        if (isset($config['singleton']) && true === $config['singleton']) {
288
            return $service;
289
        }
290
291
        $this->service[$name] = $service;
292
293
        if (isset($this->children[$name])) {
294
            $this->children[$name]->setParentService($service);
295
        }
296
297
        return $service;
298
    }
299
300
    /**
301
     * Create instance.
302
     *
303
     * @param string          $name
304
     * @param ReflectionClass $class
305
     * @param array           $arguments
306
     * @param array           $config
307
     *
308
     * @return mixed
309
     */
310
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
311
    {
312
        $instance = $class->newInstanceArgs($arguments);
313
        $this->storeService($name, $config, $instance);
314
        $config = $this->config->get($name);
315
316
        if (!isset($config['calls'])) {
317
            return $instance;
318
        }
319
320
        foreach ($config['calls'] as $call) {
321
            if (!isset($call['method'])) {
322
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
323
            }
324
325
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
326
327
            try {
328
                $method = $class->getMethod($call['method']);
329
            } catch (\ReflectionException $e) {
330
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
331
            }
332
333
            $arguments = $this->autoWireMethod($name, $method, $call);
334
            call_user_func_array([&$instance, $call['method']], $arguments);
335
        }
336
337
        return $instance;
338
    }
339
340
    /**
341
     * Autowire method.
342
     *
343
     * @param string           $name
344
     * @param ReflectionMethod $method
345
     * @param array            $config
346
     *
347
     * @return array
348
     */
349
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
350
    {
351
        $params = $method->getParameters();
352
        $args = [];
353
354
        foreach ($params as $param) {
355
            $type = $param->getClass();
356
            $param_name = $param->getName();
357
358
            if (isset($config['arguments'][$param_name])) {
359
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
360
            } elseif (null !== $type) {
361
                $type_class = $type->getName();
362
363
                if ($type_class === $name) {
364
                    throw new Exception\InvalidConfiguration('class '.$type_class.' can not depend on itself');
365
                }
366
367
                $args[$param_name] = $this->findService($name, $type_class);
368
            } elseif ($param->isDefaultValueAvailable()) {
369
                $args[$param_name] = $param->getDefaultValue();
370
            } elseif ($param->allowsNull() && $param->hasType()) {
371
                $args[$param_name] = null;
372
            } else {
373
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
374
            }
375
        }
376
377
        return $args;
378
    }
379
380
    /**
381
     * Parse param value.
382
     *
383
     * @param mixed  $param
384
     * @param string $name
385
     *
386
     * @return mixed
387
     */
388
    protected function parseParam($param, string $name)
389
    {
390
        if (is_iterable($param)) {
391
            foreach ($param as $key => $value) {
392
                $param[$key] = $this->parseParam($value, $name);
393
            }
394
395
            return $param;
396
        }
397
398
        if (is_string($param)) {
399
            $param = $this->config->getEnv($param);
400
401
            if (preg_match('#^\{\{(.*)\}\}$#', $param, $matches)) {
402
                return '{'.$matches[1].'}';
403
            }
404
            if (preg_match('#^\{(.*)\}$#', $param, $matches)) {
405
                return $this->findService($name, $matches[1]);
406
            }
407
408
            return $param;
409
        }
410
411
        return $param;
412
    }
413
414
    /**
415
     * Locate service.
416
     *
417
     * @param string $current_service
418
     * @param string $service
419
     */
420
    protected function findService(string $current_service, string $service)
421
    {
422
        if (isset($this->children[$current_service])) {
423
            return $this->children[$current_service]->get($service);
424
        }
425
426
        $config = $this->config->get($current_service);
427
        if (isset($config['services'])) {
428
            $this->children[$current_service] = new self($config['services'], $this);
429
430
            return $this->children[$current_service]->get($service);
431
        }
432
433
        return $this->get($service);
434
    }
435
}
436