Completed
Push — master ( fa9885...459291 )
by Raffael
02:52 queued 53s
created

Container::storeService()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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