Completed
Push — master ( 0c1faf...c6b404 )
by Raffael
02:34
created

Container::getAll()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
declare(strict_types = 1);
3
4
/**
5
 * Micro
6
 *
7
 * @author    Raffael Sahli <[email protected]>
8
 * @copyright Copyright (c) 2017 gyselroth GmbH (https://gyselroth.com)
9
 * @license   MIT https://opensource.org/licenses/MIT
10
 */
11
12
namespace Micro;
13
14
use \ReflectionClass;
15
use \Closure;
16
use \Micro\Container\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Micro\Exception.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
17
use \Micro\Container\AdapterAwareInterface;
18
use \Psr\Container\ContainerInterface;
19
20
class Container implements ContainerInterface
21
{
22
    /**
23
     * Config
24
     *
25
     * @var array
26
     */
27
    protected $config = [];
28
29
30
    /**
31
     * Service registry
32
     *
33
     * @var array
34
     */
35
    protected $service = [];
36
37
38
    /**
39
     * Registered but not initialized service registry
40
     *
41
     * @var array
42
     */
43
    protected $registry = [];
44
45
46
    /**
47
     * Create container
48
     *
49
     * @param array $config
50
     */
51
    public function __construct(array $config=[])
52
    {
53
        $this->flattenConfig($config);
0 ignored issues
show
Documentation introduced by
$config is of type array, but the function expects a object<Micro\Iterable>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
54
        $container = $this;
55
        $this->add(ContainerInterface::class, function() use($container){
56
            return $container;
57
        });
58
    }
59
60
61
    /**
62
     * Flatten config
63
     *
64
     * @param  Iterable $config
65
     * @param  string $parent
66
     * @return array
67
     */
68
    protected function flattenConfig(Iterable $config, ?string $parent=null): array
69
    {
70
        $flat = [];
71
        foreach($config as $name => $service) {
72
            if(isset($service['name']) && $parent === null) {
73
                $id = $service['name'];
74
           } else {
75
                if($parent === null) {
76
                    $id = $name;
77
                } else {
78
                    $id = $parent.'.'.$name;
79
                }
80
            }
81
82
            $flat[$id] = [
83
                'name' => $name
84
            ];
85
86
            foreach($service as $option => $value) {
87
                switch($option) {
88
89
                    case 'name' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
90
                    case 'use' :
0 ignored issues
show
Coding Style introduced by
There must be no space before the colon in a CASE statement

As per the PSR-2 coding standard, there must not be a space in front of the colon in case statements.

switch ($selector) {
    case "A": //right
        doSomething();
        break;
    case "B" : //wrong
        doSomethingElse();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
91
                        $flat[$id][$option] = $value;
92
                    break;
93
94
                    case 'options':
95
                        $flat[$id][$option] = (array)$value;
96
                    break;
97
98
                    case 'service':
99
                        $flat[$id]['parent'] = $parent;
100
                        $parent = $parent.'.'.$name;
101
                        $services = $this->flattenConfig($service['service'], $parent);
102
                        $flat[$id]['service'] = [];
103
                        foreach($services as $key => $sub) {
104
                            $flat[$id]['service'][$sub['name']] = $key;
105
                        }
106
                    break;
107
108
                    case 'adapter':
109
                        $flat[$id]['adapter'] = $this->flattenConfig($service['adapter']);
110
                    break;
111
112
                    default:
113
                        throw new Exception('invalid container configuration '.$option.' given');
114
                }
115
            }
116
        }
117
118
        $this->config = array_merge($this->config, $flat);
119
        return $flat;
120
    }
121
122
123
    /**
124
     * Get all services
125
     *
126
     * @return array
127
     */
128
    public function getAll(): array
129
    {
130
        return $this->service;
131
    }
132
133
134
    /**
135
     * Get service
136
     *
137
     * @param  string $name
138
     * @return mixed
139
     */
140
    public function get($name)
141
    {
142
        if($this->has($name)) {
143
            return $this->service[$name];
144
        } else {
145
            if(isset($this->registry[$name])) {
146
                $this->service[$name] = $this->registry[$name]->call($this);
147
                unset($this->registry[$name]);
148
                return $this->service[$name];
149
            } else {
150
                return $this->service[$name] = $this->autoWire($name);
151
            }
152
        }
153
    }
154
155
156
    /**
157
     * Add service
158
     *
159
     * @param  string $name
160
     * @param  Closure $service
161
     * @return Container
162
     */
163
    public function add(string $name, Closure $service): Container
164
    {
165
        if($this->has($name)) {
166
            throw new Exception('service '.$name.' is already registered');
167
        }
168
169
        $this->registry[$name] = $service;
170
        return $this;
171
    }
172
173
174
    /**
175
     * Check if service is registered
176
     *
177
     * @return bool
178
     */
179
    public function has($name): bool
180
    {
181
        return isset($this->service[$name]);
182
    }
183
184
185
    /**
186
     * Auto wire service
187
     *
188
     * @param  string $name
189
     * @return mixed
190
     */
191
    protected function autoWire(string $name)
192
    {
193
        if(isset($this->config[$name]['use'])) {
194
            $class = $this->config[$name]['use'];
195
        } elseif(isset($this->config[$name]['name'])) {
196
            $class = $this->config[$name]['name'];
197
        } else {
198
            $class = $name;
199
        }
200
201
        try {
202
            $reflection = new ReflectionClass($class);
203
        } catch(\Exception $e) {
204
            throw new Exception($class.' can not be resolved to an existing class');
205
        }
206
207
        $constructor = $reflection->getConstructor();
208
209
        if($constructor === null) {
210
            return new $class();
211
        } else {
212
            $params = $constructor->getParameters();
213
            $args = [];
214
215
            foreach($params as $param) {
216
                $type = $param->getClass();
217
                $param_name = $param->getName();
218
219
                if($type === null) {
220
                    try {
221
                        $args[$param_name] = $this->getParam($name, $param_name);
222
                    } catch(Exception $e) {
223
                        if($param->isDefaultValueAvailable()) {
224
                            $args[$param_name] = $param->getDefaultValue();
225
                        } else {
226
                            throw $e;
227
                        }
228
                    }
229
                } else {
230
                    $type_class = $type->getName();
231
                    $args[$param_name] = $this->findParentService($name, $type_class);
232
               }
233
            }
234
235
            return $this->createInstance($name, $reflection, $args);
236
        }
237
    }
238
239
240
    /**
241
     * Traverse services with parents and find correct service to use
242
     *
243
     * @param  string $name
244
     * @param  string $class
245
     * @return mixed
246
     */
247
    protected function findParentService(string $name, string $class)
248
    {
249
        if(isset($this->config[$name]['service'][$class])) {
250
            return $this->get($this->config[$name]['service'][$class]);
251
        } elseif(isset($this->config[$name]['parent'])) {
252
            return $this->findParentService($this->config[$name]['parent'], $class);
253
        } else {
254
            return $this->get($class);
255
        }
256
    }
257
258
259
    /**
260
     * Create instance
261
     *
262
     * @param  string $name
263
     * @param  ReflectionClass $class
264
     * @param  array $args
265
     * @return mixed
266
     */
267
    protected function createInstance(string $name, ReflectionClass $class, array $args)
268
    {
269
        $instance = $class->newInstanceArgs($args);
270
        if($instance instanceof AdapterAwareInterface) {
271
            if(isset($this->config[$name]['adapter'])) {
272
                foreach($this->config[$name]['adapter'] as $adapter => $config) {
273
                    if(!isset($config['name'])) {
274
                        throw new Exception('adapter requires name configuration');
275
                    }
276
277
                    if(isset($config['enabled']) && $config['enabled'] === '0') {
278
                        continue;
279
                    }
280
281
                    $adapter_instance = $this->get($config['name']);
282
                    $instance->injectAdapter($adapter, $adapter_instance);
283
                }
284
            }
285
        }
286
287
        return $instance;
288
    }
289
290
291
    /**
292
     * Get config value
293
     *
294
     * @param  string $name
295
     * @param  string $param
296
     * @return mixed
297
     */
298
    public function getParam(string $name, string $param)
299
    {
300
        if(!isset($this->config[$name]) && !isset($this->config[$name]['options'])) {
301
            throw new Exception('no configuration available for service '.$name);
302
        }
303
304
        if(!isset($this->config[$name]['options'][$param])) {
305
            throw new Exception('no configuration available for service parameter '.$param);
306
        }
307
308
        return $this->config[$name]['options'][$param];
309
    }
310
}
311