Completed
Push — master ( cec675...ca4bb7 )
by Raffael
01:59
created

Container::lookupService()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 9
nc 5
nop 1
dl 0
loc 19
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Balloon
7
 *
8
 * @author      Raffael Sahli <[email protected]>
9
 * @copyright   Copryright (c) 2012-2017 gyselroth GmbH (https://gyselroth.com)
10
 * @license     GPL-3.0 https://opensource.org/licenses/GPL-3.0
11
 */
12
13
namespace Micro\Container;
14
15
use Closure;
16
use Psr\Container\ContainerInterface;
17
use ReflectionClass;
18
use ReflectionMethod;
19
use Micro\Container\Exception;
20
21
class Container implements ContainerInterface
22
{
23
    /**
24
     * Config.
25
     *
26
     * @var Iterable
27
     */
28
    protected $config = [];
29
30
    /**
31
     * Service registry.
32
     *
33
     * @var array
34
     */
35
    protected $service = [];
36
37
    /**
38
     * Registered but not initialized service registry.
39
     *
40
     * @var array
41
     */
42
    protected $registry = [];
43
44
    /**
45
     * Parent container
46
     *
47
     * @var ContainerInterface
48
     */
49
    protected $parent;
50
51
    /**
52
     * Children container
53
     *
54
     * @var ContainerInterface[]
55
     */
56
    protected $children = [];
57
58
    /**
59
     * Create container.
60
     *
61
     * @param Iterable $config
62
     */
63
    public function __construct(Iterable $config = [], ?ContainerInterface $parent=null)
64
    {
65
        $this->config = $config;
66
        $this->parent = $parent;
67
        $this->add(ContainerInterface::class, $this);
68
    }
69
70
    /**
71
     * Get service.
72
     *
73
     * @param string $name
74
     *
75
     * @return mixed
76
     */
77
    public function get($name)
78
    {
79
        if ($this->has($name)) {
80
            return $this->service[$name];
81
        }
82
83
        if (isset($this->registry[$name])) {
84
            return $this->addStaticService($name);
85
        }
86
87
        if(isset($this->config[$name])) {
88
            return $this->autoWireClass($name);
89
        }
90
91
        try {
92
            return $this->lookupService($name);
93
        } catch(Exception\ServiceNotFound $e) {
94
            return $this->autoWireClass($name);
95
        }
96
    }
97
98
    /**
99
     * Check for static injections
100
     *
101
     * @param string $name
102
     * @return mixed
103
     */
104
    protected function addStaticService(string $name)
105
    {
106
        if( $this->registry[$name] instanceof Closure) {
107
            $this->service[$name] = $this->registry[$name]->call($this);
108
        } else {
109
            $this->service[$name] = $this->registry[$name];
110
        }
111
112
        unset($this->registry[$name]);
113
        return $this->service[$name];
114
    }
115
116
    /**
117
     * Traverse tree up and look for service
118
     *
119
     * @param string $name
120
     * @return mixed
121
     */
122
    public function lookupService(string $name)
123
    {
124
        if ($this->has($name)) {
125
            return $this->service[$name];
126
        }
127
128
        if (isset($this->registry[$name])) {
129
            return $this->addStaticService($name);
130
        }
131
132
        if(isset($this->config[$name])) {
133
            return $this->autoWireClass($name);
134
        }
135
136
        if($this->parent !== null) {
137
            return $this->parent->lookupService($name);
138
        }
139
140
        throw new Exception\ServiceNotFound("service $name was not found in service tree");
141
    }
142
143
    /**
144
     * Get new instance (Do not store in container).
145
     *
146
     * @param string $name
147
     *
148
     * @return mixed
149
     */
150
    public function getNew(string $name)
151
    {
152
        if (isset($this->registry[$name])) {
153
            return $this->registry[$name]->call($this);
154
        }
155
156
        return $this->autoWireClass($name);
157
    }
158
159
    /**
160
     * Add service.
161
     *
162
     * @param string $name
163
     * @param mixed $service
164
     *
165
     * @return Container
166
     */
167
    public function add(string $name, $service): self
168
    {
169
        if ($this->has($name)) {
170
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
171
        }
172
173
        $this->registry[$name] = $service;
174
175
        return $this;
176
    }
177
178
    /**
179
     * Check if service is registered.
180
     *
181
     * @param mixed $name
182
     *
183
     * @return bool
184
     */
185
    public function has($name): bool
186
    {
187
        return isset($this->service[$name]);
188
    }
189
190
    /**
191
     * Auto wire.
192
     *
193
     * @param string   $name
194
     * @param array $config
195
     * @param array $parents
196
     *
197
     * @return mixed
198
     */
199
    protected function autoWireClass(string $name)
200
    {
201
        $class = $name;
202
203
        $config = $this->config;
0 ignored issues
show
Unused Code introduced by
The assignment to $config is dead and can be removed.
Loading history...
204
        if (isset($this->config[$name])) {
205
            $config = $this->config[$name];
206
        } else {
207
            $config = [];
208
        }
209
210
        if (isset($config['use'])) {
211
            $class = $config['use'];
212
        }
213
214
        if(preg_match('#^\{(.*)\}$#', $class, $match)) {
215
            $service = $this->get($match[1]);
216
217
            if(isset($this->config[$name]['selects'])) {
218
                $reflection = new ReflectionClass(get_class($service));
0 ignored issues
show
Bug introduced by
It seems like $service can also be of type mixed; however, parameter $object of get_class() does only seem to accept object, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

218
                $reflection = new ReflectionClass(get_class(/** @scrutinizer ignore-type */ $service));
Loading history...
219
220
                foreach($this->config[$name]['selects'] as $select) {
221
                    $args = $this->autoWireMethod($name, $reflection->getMethod($select['method']), $select);
222
                    $service = call_user_func_array([&$service, $select['method']], $args);
223
                }
224
            }
225
226
            return $this->service[$name] = $service;
227
        }
228
229
        try {
230
            $reflection = new ReflectionClass($class);
231
        } catch (\Exception $e) {
232
            throw new Exception\ServiceNotFound($class.' can not be resolved to an existing class for service '.$name);
233
        }
234
235
        $constructor = $reflection->getConstructor();
236
237
        if (null === $constructor) {
238
            return new $class();
239
        }
240
241
        $args = $this->autoWireMethod($name, $constructor, $config);
242
        return $this->createInstance($name, $reflection, $args);
243
    }
244
245
    /**
246
     * Create instance.
247
     *
248
     * @param string          $name
249
     * @param ReflectionClass $class
250
     * @param array $arguments
251
     *
252
     * @return mixed
253
     */
254
    protected function createInstance(string $name, ReflectionClass $class, array $arguments)
255
    {
256
        $instance = $class->newInstanceArgs($arguments);
257
        $this->service[$name] = $instance;
258
259
        if(isset($this->config[$name]['calls'])) {
260
            foreach($this->config[$name]['calls'] as $call) {
261
                if(!isset($call['method'])) {
262
                    throw new Exception\Configuration('method is required for setter injection in service '.$name);
263
                }
264
265
                $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
266
                try {
267
                    $method = $class->getMethod($call['method']);
268
                } catch(\ReflectionException $e) {
269
                    throw new Exception\Configuration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
270
                }
271
272
                $arguments = $this->autoWireMethod($name, $method, $call);
273
                call_user_func_array([&$instance, $call['method']], $arguments);
274
            }
275
        }
276
277
        return $instance;
278
    }
279
280
    /**
281
     * Autowire method
282
     *
283
     * @param string $name
284
     * @param ReflectionMethod $method
285
     * @param array $config
286
     * @return array
287
     */
288
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config): array
289
    {
290
        $params = $method->getParameters();
291
        $args = [];
292
293
        foreach ($params as $param) {
294
            $type = $param->getClass();
295
            $param_name = $param->getName();
296
297
            if(isset($config['arguments'][$param_name])) {
298
                $args[$param_name] = $this->parseParam($config['arguments'][$param_name], $name);
299
            } elseif($type !== null) {
300
                $type_class = $type->getName();
301
302
                if ($type_class === $name) {
303
                    throw new Exception\Logic('class '.$type_class.' can not depend on itself');
304
                }
305
306
                $args[$param_name] = $this->findService($name, $type_class);
307
            } elseif($param->isDefaultValueAvailable()) {
308
                 $args[$param_name] = $param->getDefaultValue();
309
            } elseif($param->allowsNull() && $param->hasType()) {
310
                 $args[$param_name] = null;
311
            } else {
312
                throw new Exception\Configuration('no value found for argument '.$param_name.' in method '.$method->getName().' for service '.$name);
313
            }
314
        }
315
316
        return $args;
317
    }
318
319
320
    /**
321
     * Parse param value.
322
     *
323
     * @param mixed $param
324
     * @param string $name
325
     *
326
     * @return mixed
327
     */
328
    protected function parseParam($param, string $name)
329
    {
330
        if (is_iterable($param)) {
331
            foreach ($param as $key => $value) {
332
                $param[$key] = $this->parseParam($value, $name);
333
            }
334
335
            return $param;
336
        }
337
        if (is_string($param)) {
338
            if (preg_match('#\{ENV\(([A-Za-z0-9_]+)(?:(,?)(.*))\)\}#', $param, $match)) {
339
                if (4 !== count($match)) {
340
                    return $param;
341
                }
342
343
                $env = getenv($match[1]);
344
                if (false === $env && !empty($match[3])) {
345
                    return str_replace($match[0], $match[3], $param);
346
                }
347
                if (false === $env) {
348
                    throw new Exception\EnvVariableNotFound('env variable '.$match[1].' required but it is neither set not a default value exists');
349
                }
350
351
                return str_replace($match[0], $env, $param);
352
            } elseif(preg_match('#^\{(.*)\}$#', $param, $match)) {
353
                return $this->findService($name, $match[1]);
354
            }
355
356
            return $param;
357
        }
358
359
        return $param;
360
    }
361
362
363
    /**
364
     * Locate service
365
     *
366
     * @param string $current_service
367
     * @param string $service
368
     */
369
    protected function findService(string $current_service, string $service)
370
    {
371
        if(isset($this->children[$current_service])) {
372
            return $this->children[$current_service]->get($service);
373
        }
374
375
        if(isset($this->config[$current_service]['services'])) {
376
            $this->children[$current_service] = new self($this->config[$current_service]['services'], $this);
377
            return $this->children[$current_service]->get($service);
378
        }
379
380
        return $this->get($service);
381
    }
382
}
383