Completed
Push — master ( 08e893...81725e )
by Raffael
02:48
created

Container::autoWireClass()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 5
nop 1
dl 0
loc 35
rs 8.439
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
            $service = $this->get($match[1]);
232
233
            if (isset($config['selects'])) {
234
                $reflection = new ReflectionClass(get_class($service));
235
236
                foreach ($config['selects'] as $select) {
237
                    $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
238
                    $service = call_user_func_array([&$service, $select['method']], $args);
239
                }
240
            }
241
242
            return $this->storeService($name, $config, $service);
243
        }
244
245
        try {
246
            $reflection = new ReflectionClass($class);
247
        } catch (\Exception $e) {
248
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
249
        }
250
251
        $constructor = $reflection->getConstructor();
252
253
        if (null === $constructor) {
254
            return new $class();
255
        }
256
257
        $args = $this->autoWireMethod($name, $constructor, $config);
258
259
        return $this->createInstance($name, $reflection, $args, $config);
260
    }
261
262
    /**
263
     * Store service.
264
     *
265
     * @param param string $name
266
     * @param array        $config
267
     * @param mixed        $service
268
     *
269
     * @return mixed
270
     */
271
    protected function storeService(string $name, array $config, $service)
272
    {
273
        if (isset($config['singleton']) && true === $config['singleton']) {
274
            return $service;
275
        }
276
277
        $this->service[$name] = $service;
278
279
        if (isset($this->children[$name])) {
280
            $this->children[$name]->setParentService($service);
281
        }
282
283
        return $service;
284
    }
285
286
    /**
287
     * Create instance.
288
     *
289
     * @param string          $name
290
     * @param ReflectionClass $class
291
     * @param array           $arguments
292
     * @param array           $config
293
     *
294
     * @return mixed
295
     */
296
    protected function createInstance(string $name, ReflectionClass $class, array $arguments, array $config)
297
    {
298
        $instance = $class->newInstanceArgs($arguments);
299
        $this->storeService($name, $config, $instance);
300
        $config = $this->config->get($name);
301
302
        if (!isset($config['calls'])) {
303
            return $instance;
304
        }
305
306
        foreach ($config['calls'] as $call) {
307
            if (!isset($call['method'])) {
308
                throw new Exception\InvalidConfiguration('method is required for setter injection in service '.$name);
309
            }
310
311
            $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
312
313
            try {
314
                $method = $class->getMethod($call['method']);
315
            } catch (\ReflectionException $e) {
316
                throw new Exception\InvalidConfiguration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
317
            }
318
319
            $arguments = $this->autoWireMethod($name, $method, $call);
320
            call_user_func_array([&$instance, $call['method']], $arguments);
321
        }
322
323
        return $instance;
324
    }
325
326
    /**
327
     * Autowire method.
328
     *
329
     * @param string           $name
330
     * @param ReflectionMethod $method
331
     * @param array            $config
332
     *
333
     * @return array
334
     */
335
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
336
    {
337
        $params = $method->getParameters();
338
        $args = [];
339
340
        foreach ($params as $param) {
341
            $type = $param->getClass();
342
            $param_name = $param->getName();
343
344
            if (isset($config['arguments'][$param_name])) {
345
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
346
            } elseif (null !== $type) {
347
                $type_class = $type->getName();
348
349
                if ($type_class === $name) {
350
                    throw new Exception\InvalidConfiguration('class '.$type_class.' can not depend on itself');
351
                }
352
353
                $args[$param_name] = $this->findService($name, $type_class);
354
            } elseif ($param->isDefaultValueAvailable()) {
355
                $args[$param_name] = $param->getDefaultValue();
356
            } elseif ($param->allowsNull() && $param->hasType()) {
357
                $args[$param_name] = null;
358
            } else {
359
                throw new Exception\InvalidConfiguration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
360
            }
361
        }
362
363
        return $args;
364
    }
365
366
    /**
367
     * Parse param value.
368
     *
369
     * @param mixed  $param
370
     * @param string $name
371
     *
372
     * @return mixed
373
     */
374
    protected function parseParam($param, string $name)
375
    {
376
        if (is_iterable($param)) {
377
            foreach ($param as $key => $value) {
378
                $param[$key] = $this->parseParam($value, $name);
379
            }
380
381
            return $param;
382
        }
383
384
        if (is_string($param)) {
385
            $param = $this->config->getEnv($param);
386
387
            if (preg_match('#^\{\{(.*)\}\}$#', $param, $matches)) {
388
                return '{'.$matches[1].'}';
389
            }
390
            if (preg_match('#^\{(.*)\}$#', $param, $matches)) {
391
                return $this->findService($name, $matches[1]);
392
            }
393
394
            return $param;
395
        }
396
397
        return $param;
398
    }
399
400
    /**
401
     * Locate service.
402
     *
403
     * @param string $current_service
404
     * @param string $service
405
     */
406
    protected function findService(string $current_service, string $service)
407
    {
408
        if (isset($this->children[$current_service])) {
409
            return $this->children[$current_service]->get($service);
410
        }
411
412
        $config = $this->config->get($current_service);
413
        if (isset($config['services'])) {
414
            $this->children[$current_service] = new self($config['services'], $this);
415
416
            return $this->children[$current_service]->get($service);
417
        }
418
419
        return $this->get($service);
420
    }
421
}
422