Container   A
last analyzed

Complexity

Total Complexity 25

Size/Duplication

Total Lines 351
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 6
Bugs 1 Features 2
Metric Value
wmc 25
eloc 60
c 6
b 1
f 2
dl 0
loc 351
ccs 64
cts 64
cp 1
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A buildTreeRecursive() 0 48 6
A buildObjects() 0 28 5
A set() 0 3 1
A resolve() 0 17 1
A buildArguments() 0 33 6
A __construct() 0 3 1
A delete() 0 12 2
A get() 0 7 2
A has() 0 3 1
1
<?php
2
3
/**
4
 * Linna Framework.
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2018, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\Container;
13
14
use ArrayAccess;
15
use Linna\Container\Exception\NotFoundException;
16
use Psr\Container\ContainerInterface;
17
use ReflectionClass;
18
19
/**
20
 * Dependency Injection Container and Resolver.
21
 */
22
class Container implements ContainerInterface, ArrayAccess
23
{
24
    use PropertyAccessTrait;
25
    use ArrayAccessTrait;
26
27
    /**
28
     * @const string
29
     */
30
    private const NO_TYPE = 'NO_TYPE';
31
32
    /**
33
     * @const string
34
     */
35
    public const RULE_INTERFACE = 'interfaces';
36
37
    /**
38
     * @const string
39
     */
40
    public const RULE_ARGUMENT = 'arguments';
41
42
    /**
43
     * @var array<mixed> Contains object already resolved.
44
     */
45
    private array $cache = [];
46
47
    /**
48
     * @var array<mixed> Hierarchical structure of dependencies.
49
     */
50
    protected array $tree = [];
51
52
    /**
53
     * @var array<mixed> Rules for resolve scalar arguments or unexpected behaviors.
54
     */
55
    protected array $rules = [];
56
57
    /**
58
     * Class Constructor.
59
     *
60
     * @param array<mixed> $rules Rules for resolve scalar arguments or unexpected behaviors.
61
     */
62 96
    public function __construct(array $rules = [])
63
    {
64 96
        $this->rules = $rules;
65 96
    }
66
67
    /**
68
     * Finds an entry of the container by its identifier and returns it.
69
     *
70
     * @param string $id Identifier of the entry to look for.
71
     *
72
     * @throws NotFoundException No entry was found for **this** identifier.
73
     *
74
     * @return mixed Entry.
75
     */
76 36
    public function get($id)
77
    {
78 36
        if (isset($this->cache[$id])) {
79 18
            return $this->cache[$id];
80
        }
81
82 18
        throw new NotFoundException('No entry was found for this identifier.');
83
    }
84
85
    /**
86
     * Returns true if the container can return an entry for the given identifier.
87
     * Returns false otherwise.
88
     *
89
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
90
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
91
     *
92
     * @param string $id Identifier of the entry to look for.
93
     *
94
     * @return bool
95
     */
96 36
    public function has($id): bool
97
    {
98 36
        return isset($this->cache[$id]);
99
    }
100
101
    /**
102
     * Store a value inside container.
103
     *
104
     * @param string $id
105
     * @param mixed  $value
106
     *
107
     * @return void
108
     */
109 55
    public function set($id, $value): void
110
    {
111 55
        $this->cache[$id] = $value;
112 55
    }
113
114
    /**
115
     * Delete value from container.
116
     *
117
     * @param string $id
118
     *
119
     * @return bool
120
     */
121 24
    public function delete($id): bool
122
    {
123 24
        if (\array_key_exists($id, $this->cache)) {
124
125
            //delete value
126 18
            unset($this->cache[$id]);
127
128
            //return function result
129 18
            return true;
130
        }
131
132 6
        return false;
133
    }
134
135
    /**
136
     * Resolve dependencies for given class.
137
     *
138
     * @param string       $class An existing class.
139
     * @param array<mixed> $rules Custom rules.
140
     *
141
     * @return object|null Instance of resolved class
142
     */
143 18
    public function resolve(string $class, array $rules = []): ?object
144
    {
145
        //reset tree;
146 18
        $this->tree = [];
147
148
        //merge rules passed as parameter with general rules
149 18
        $this->rules = \array_merge($this->rules, $rules);
150
151
        //build dependency tree
152 18
        $this->buildTreeRecursive($class);
153
        //$this->buildTree($class);
154
155
        //build objects
156 18
        $this->buildObjects();
157
158
        //return required class
159 18
        return $this->cache[$class] ?? null;
160
    }
161
162
    /**
163
     * Create a map of dependencies for a class
164
     *
165
     * @param string    $class Class wich tree will build
166
     * @param int       $level Level for dependency
167
     *
168
     * @return void
169
     */
170 18
    private function buildTreeRecursive(string $class, int $level = 0): void
171
    {
172
        //initialize array
173 18
        $this->tree[$level][$class] = [];
174
175
        //get parameter from constructor
176
        //can return error when constructor not declared
177 18
        $constructor = (new ReflectionClass($class))->getConstructor();//->getParameters();
178
        //this should resolve the error when a class without constructor is encountered
179 18
        $parameters = \is_null($constructor) ? [] : $constructor->getParameters();
180
181
        //loop parameter
182 18
        foreach ($parameters as $param) {
183
184
            //Function ReflectionType::__toString() is deprecated
185
            //FROM https://www.php.net/manual/en/migration74.deprecated.php
186
            //Calls to ReflectionType::__toString() now generate a deprecation notice.
187
            //This method has been deprecated in favor of ReflectionNamedType::getName()
188
            //in the documentation since PHP 7.1, but did not throw a deprecation notice
189
            //for technical reasons.
190
            //Get the data type of the parameter
191 13
            $type = \is_null($param->getType()) ? self::NO_TYPE : $param->getType()->getName();
192
193
            //if parameter is an interface
194
            //check rules for an implementation and
195
            //replace interface with implementation
196 13
            if (\interface_exists($type)) {
197
                //get the position of the current parameter for resolve the rule
198 3
                $position = $param->getPosition();
199
                //override type with interface implamentation
200
                //declared in rules
201 3
                $type = $this->rules[self::RULE_INTERFACE][$class][$position];
202
            }
203
204
            //if there is a parameter with callable type
205 13
            if (\class_exists($type)) {
206
207
                //store dependency
208 13
                $this->tree[$level][$class][] = $param;
209
210
                //call recursive, head recursion
211 13
                $this->buildTreeRecursive($type, $level + 1);
212
213
                //continue
214 13
                continue;
215
            }
216
217 2
            $this->tree[$level][$class][] = $param;
218
        }
219 18
    }
220
221
    /**
222
     * Create a dependencies map for a class.
223
     * !!!This method will be removed!!!
224
     *
225
     * @param string $class
226
     *
227
     * @return void
228
     *
229
     * @deprecated since version 0.27.0 Return to recursive build of the tree
230
     *                                  because is 1.5x time fast.
231
     */
232
    /*private function buildTree(string $class): void
233
    {
234
        $level = 0;
235
        $stack = new SplStack();
236
237
        while (true) {
238
239
            //initialize array if not already initialized
240
            $this->tree[$level][$class] ??= [];
241
242
            //get parameter from constructor
243
            //can return error when constructor not declared
244
            $constructor = (new ReflectionClass($class))->getConstructor();//->getParameters();
245
            //this should resolve the error when a class without constructor is encountered
246
            $parameters = \is_null($constructor) ? [] : $constructor->getParameters();
247
248
            //loop parameter
249
            foreach ($parameters as $param) {
250
251
                //check if argument is already stored
252
                $notAlreadyStored = !\in_array($param, $this->tree[$level][$class]);
253
254
                //Function ReflectionType::__toString() is deprecated
255
                //FROM https://www.php.net/manual/en/migration74.deprecated.php
256
                //Calls to ReflectionType::__toString() now generate a deprecation notice.
257
                //This method has been deprecated in favor of ReflectionNamedType::getName()
258
                //in the documentation since PHP 7.1, but did not throw a deprecation notice
259
                //for technical reasons.
260
                $type = ($param->getType() !== null) ? $param->getType()->getName() : self::NO_TYPE;
261
262
                //if there is parameter with callable type
263
                if (\class_exists($type) && $notAlreadyStored) {
264
265
                    //push values in stack for simulate later recursive function
266
                    $stack->push([$level, $class]);
267
268
                    //store dependency
269
                    $this->tree[$level][$class][] = $param;
270
271
                    //update values for simulate recursive function
272
                    $level++;
273
                    $class = $param->getClass()->name;
274
275
                    //return to main while
276
                    continue 2;
277
                }
278
279
                if ($notAlreadyStored) {
280
                    //store dependency
281
                    $this->tree[$level][$class][] = $param;
282
                }
283
            }
284
285
            //if stack is empty break while end exit from function
286
            if ($stack->count() === 0) {
287
                break;
288
            }
289
290
            //get last value pushed into stack;
291
            list($level, $class) = $stack->pop();
292
        }
293
    }*/
294
295
    /**
296
     * Build objects from dependencyTree.
297
     *
298
     * @return void
299
     */
300 18
    private function buildObjects(): void
301
    {
302
        //deep dependency level, start to array end for not use array_reverse
303 18
        for ($i = \count($this->tree) - 1; $i >= 0; $i--) {
304
305
            //class
306 18
            foreach ($this->tree[$i] as $class => $arguments) {
307
308
                //try to find object in class
309 18
                if (isset($this->cache[$class])) {
310
                    // no build need
311 9
                    continue;
312
                }
313
314
                //object is not in cache and need arguments try to build it
315 18
                if (\count($arguments)) {
316
317
                    //build arguments
318 13
                    $args = $this->buildArguments($class, $arguments);
319
320
                    //store object with dependencies in cache
321 13
                    $this->cache[$class] = (new ReflectionClass($class))->newInstanceArgs($args);
322
323 13
                    continue;
324
                }
325
326
                //store object in cache
327 18
                $this->cache[$class] = (new ReflectionClass($class))->newInstance();
328
            }
329
        }
330 18
    }
331
332
    /**
333
     * Build dependency for a object.
334
     *
335
     * @param string       $class
336
     * @param array<mixed> $dependency
337
     *
338
     * @return array<mixed>
339
     */
340 13
    private function buildArguments(string $class, array $dependency): array
341
    {
342
        //initialize arguments array
343 13
        $args = [];
344
345
        //argument required from class
346 13
        foreach ($dependency as $argValue) {
347 13
            $argType = \is_null($argValue->getType()) ? self::NO_TYPE : $argValue->getType()->getName();
348
349 13
            if (\interface_exists($argType)) {
350
                //get the position of the current parameter for resolve the rule
351 3
                $position = $argValue->getPosition();
352
                //retrive concrete class bound to inteface in rules
353 3
                $args[] = $this->cache[$this->rules[self::RULE_INTERFACE][$class][$position]];
354 3
                continue;
355
            }
356
357 13
            if (\class_exists($argType)) {
358
                //add to array of arguments
359 13
                $args[] = $this->cache[$argType];
360 13
                continue;
361
            }
362
363 1
            $args[] = null;
364
        }
365
366
        //check if there is rules for this class
367 13
        if (isset($this->rules['arguments'][$class])) {
368
            //merge arguments
369 1
            $args = \array_replace($args, $this->rules[self::RULE_ARGUMENT][$class]);
370
        }
371
372 13
        return $args;
373
    }
374
}
375