Completed
Push — master ( 3ba97d...3bf8a8 )
by Raffael
06:40
created

Container::autoWireClass()   B

Complexity

Conditions 6
Paths 18

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 19
nc 18
nop 3
dl 0
loc 32
rs 8.439
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 array
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
     * Create container.
46
     *
47
     * @param Iterable $config
48
     */
49
    public function __construct(Iterable $config = [])
50
    {
51
        $this->config = $config;
0 ignored issues
show
Documentation Bug introduced by
It seems like $config can also be of type iterable. However, the property $config is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
52
        $this->add(ContainerInterface::class, $this);
53
    }
54
55
    /**
56
     * Get service.
57
     *
58
     * @param string $name
59
     *
60
     * @return mixed
61
     */
62
    public function get($name)
63
    {
64
        if ($this->has($name)) {
65
            return $this->service[$name]['instance'];
66
        }
67
        if (isset($this->registry[$name])) {
68
            if( $this->registry[$name] instanceof Closure) {
69
                $this->service[$name]['instance'] = $this->registry[$name]->call($this);
70
            } else {
71
                $this->service[$name]['instance'] = $this->registry[$name];
72
            }
73
74
            unset($this->registry[$name]);
75
            return $this->service[$name]['instance'];
76
        }
77
78
        return $this->autoWireClass($name);
79
    }
80
81
82
    /**
83
     * Debug container service tree
84
     *
85
     * @return array
86
     */
87
    public function __debug(?array $container=null): array
88
    {
89
        if($container === null) {
90
            $container = $this->service;
91
        }
92
93
        foreach($container as $name => &$service) {
94
            if(isset($service['instance'])) {
95
                $service['instance'] = 'instanceof '.get_class($service['instance']);
96
            }
97
98
            if(isset($service['services'])) {
99
                $service['services'] = $this->__debug($service['services']);
100
            }
101
        }
102
103
        return $container;
104
    }
105
106
107
    /**
108
     * Get new instance (Do not store in container).
109
     *
110
     * @param string $name
111
     *
112
     * @return mixed
113
     */
114
    public function getNew(string $name)
115
    {
116
        if (isset($this->registry[$name])) {
117
            return $this->registry[$name]->call($this);
118
        }
119
120
        return $this->autoWireClass($name);
121
    }
122
123
    /**
124
     * Add service.
125
     *
126
     * @param string $name
127
     * @param mixed $service
128
     *
129
     * @return Container
130
     */
131
    public function add(string $name, $service): self
132
    {
133
        if ($this->has($name)) {
134
            throw new Exception\ServiceAlreadyExists('service '.$name.' is already registered');
135
        }
136
137
        $this->registry[$name] = $service;
138
139
        return $this;
140
    }
141
142
    /**
143
     * Check if service is registered.
144
     *
145
     * @param mixed $name
146
     *
147
     * @return bool
148
     */
149
    public function has($name): bool
150
    {
151
        return isset($this->service[$name]);
152
    }
153
154
    /**
155
     * Auto wire.
156
     *
157
     * @param string   $name
158
     * @param array $config
159
     * @param array $parents
160
     *
161
     * @return mixed
162
     */
163
    protected function autoWireClass(string $name, ?array $config = null, array $parents = [])
164
    {
165
        if (null === $config) {
166
            $config = $this->config;
167
        }
168
169
        $class = $name;
170
        $sub_config = $config;
0 ignored issues
show
Unused Code introduced by
The assignment to $sub_config is dead and can be removed.
Loading history...
171
        if (isset($config[$name])) {
172
            if (isset($config[$name]['use'])) {
173
                $class = $config[$name]['use'];
174
            }
175
176
            $config = $config[$name];
177
        } else {
178
            $config = [];
179
        }
180
181
        try {
182
            $reflection = new ReflectionClass($class);
183
        } catch (\Exception $e) {
184
            throw new Exception\Configuration($class.' can not be resolved to an existing class for service '.$name);
185
        }
186
187
        $constructor = $reflection->getConstructor();
188
189
        if (null === $constructor) {
190
            return new $class();
191
        }
192
193
        $args = $this->autoWireMethod($name, $constructor, $config, $parents);
194
        return $this->createInstance($name, $reflection, $args, $config, $parents);
195
    }
196
197
    /**
198
     * Traverse services with parents and find correct service to use.
199
     *
200
     * @param string $name
201
     * @param string $class
202
     * @param mixed $config
203
     * @param mixed $parents
204
     *
205
     * @return mixed
206
     */
207
    protected function findParentService(string $name, ?string $class, $config, $parents)
208
    {
209
        $service = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $service is dead and can be removed.
Loading history...
210
        $services = $this->service;
211
212
        foreach (array_reverse($parents) as $name => $parent) {
213
            if (isset($services[$name])) {
214
                $service = $services[$name];
215
                if (isset($services['services'])) {
216
                    $services = $services['services'];
217
                } else {
218
                    break;
219
                }
220
            } else {
221
                break;
222
            }
223
        }
224
225
        foreach (array_reverse($parents) as $parent) {
226
            if (isset($parent['services'][$class])) {
227
                return $this->autoWireClass($class, $parent['services'], $parents);
0 ignored issues
show
Bug introduced by
It seems like $class can also be of type null; however, parameter $name of Micro\Container\Container::autoWireClass() does only seem to accept string, 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

227
                return $this->autoWireClass(/** @scrutinizer ignore-type */ $class, $parent['services'], $parents);
Loading history...
228
            }
229
        }
230
231
        return $this->get($class);
232
    }
233
234
    /**
235
     * Create instance.
236
     *
237
     * @param string          $name
238
     * @param ReflectionClass $class
239
     * @param array           $args
240
     * @param mixed           $parents
241
     *
242
     * @return mixed
243
     */
244
    protected function createInstance(string $name, ReflectionClass $class, array $args, array $config, $parents = [])
245
    {
246
        $instance = $class->newInstanceArgs($args);
247
248
        $loop = &$this->service;
249
        foreach ($parents as $p => $parent) {
250
            $loop = &$loop[$p];
251
        }
252
253
        if (0 === count($parents)) {
254
            $loop[$name]['instance'] = $instance;
255
        } else {
256
            $loop['services'][$name]['instance'] = $instance;
257
        }
258
259
        $parents[$name] = $config;
260
        $parents_orig = $parents;
0 ignored issues
show
Unused Code introduced by
The assignment to $parents_orig is dead and can be removed.
Loading history...
261
262
        if(isset($config['calls'])) {
263
            foreach($config['calls'] as $call) {
264
                $arguments = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $arguments is dead and can be removed.
Loading history...
265
                try {
266
                    $method = $class->getMethod($call['method']);
267
                } catch(\ReflectionException $e) {
268
                    throw new Exception\Configuration('method '.$call['method'].' is not callable in class '.$class->getName().' for service '.$name);
269
                }
270
271
                $arguments = $this->autoWireMethod($name, $method, $call, $parents);
272
                call_user_func_array([&$instance, $call['method']], $arguments);
273
            }
274
        }
275
276
        return $instance;
277
    }
278
279
    /**
280
     * Autowire method
281
     *
282
     * @param string $name
283
     * @param ReflectionMethod $method
284
     * @param array $config
285
     * @param mixed $parents
286
     * @return array
287
     */
288
    protected function autoWireMethod(string $name, ReflectionMethod $method, array $config, $parents): 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, $type, $config, $parents);
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->findParentService($name, $type_class, $config, $parents);
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
     * @param string $type_class
326
     * @param array $config
327
     * @param mixed $parents
328
     *
329
     * @return mixed
330
     */
331
    protected function parseParam($param, string $name, $type_class, array $config, $parents)
332
    {
333
        if (is_iterable($param)) {
334
            foreach ($param as $key => $value) {
335
                $param[$key] = $this->parseParam($value, $name, $type_class, $config, $parents);
336
            }
337
338
            return $param;
339
        }
340
        if (is_string($param)) {
341
            if (preg_match('#\{ENV\(([A-Za-z0-9_]+)(?:(,?)(.*))\)\}#', $param, $match)) {
342
                if (4 !== count($match)) {
343
                    return $param;
344
                }
345
346
                $env = getenv($match[1]);
347
                if (false === $env && !empty($match[3])) {
348
                    return str_replace($match[0], $match[3], $param);
349
                }
350
                if (false === $env) {
351
                    throw new Exception\EnvVariableNotFound('env variable '.$match[1].' required but it is neither set not a default value exists');
352
                }
353
354
                return str_replace($match[0], $env, $param);
355
            } elseif(preg_match('#^\{(.*)\}$#', $param, $match)) {
356
                return $this->findParentService($match[1], $match[1], $config, $parents);
357
            }
358
359
            return $param;
360
        }
361
362
        return $param;
363
    }
364
}
365