Passed
Push — master ( a2c0fb...8c9da3 )
by Sebastian
03:31
created

Container::buildTreeRecursive()   A

Complexity

Conditions 6
Paths 18

Size

Total Lines 46
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
c 1
b 0
f 0
dl 0
loc 46
ccs 13
cts 13
cp 1
rs 9.2222
cc 6
nc 18
nop 2
crap 6
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
use SplStack;
19
20
/**
21
 * Dependency Injection Container and Resolver.
22
 */
23
class Container implements ContainerInterface, ArrayAccess
24
{
25
    use PropertyAccessTrait;
26
    use ArrayAccessTrait;
27
28
    /**
29
     * @const string
30
     */
31
    private const NO_TYPE = 'NO_TYPE';
32
33
    /**
34
     * @var array<mixed> Contains object already resolved.
35
     */
36
    private array $cache = [];
37
38
    /**
39
     * @var array<mixed> Hierarchical structure of dependencies.
40
     */
41
    protected array $tree = [];
42
43
    /**
44
     * @var array<mixed> Rules for resolve scalar arguments or unexpected behaviors.
45
     */
46
    protected array $rules = [];
47
48
    /**
49
     * Class Constructor.
50
     *
51
     * @param array<mixed> $rules Rules for resolve scalar arguments or unexpected behaviors.
52
     */
53 96
    public function __construct(array $rules = [])
54
    {
55 96
        $this->rules = $rules;
56 96
    }
57
58
    /**
59
     * Finds an entry of the container by its identifier and returns it.
60
     *
61
     * @param string $id Identifier of the entry to look for.
62
     *
63
     * @throws NotFoundException No entry was found for **this** identifier.
64
     *
65
     * @return mixed Entry.
66
     */
67 36
    public function get($id)
68
    {
69 36
        if (isset($this->cache[$id])) {
70 18
            return $this->cache[$id];
71
        }
72
73 18
        throw new NotFoundException('No entry was found for this identifier.');
74
    }
75
76
    /**
77
     * Returns true if the container can return an entry for the given identifier.
78
     * Returns false otherwise.
79
     *
80
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
81
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
82
     *
83
     * @param string $id Identifier of the entry to look for.
84
     *
85
     * @return bool
86
     */
87 36
    public function has($id)
88
    {
89 36
        return isset($this->cache[$id]);
90
    }
91
92
    /**
93
     * Store a value inside container.
94
     *
95
     * @param string $id
96
     * @param mixed  $value
97
     *
98
     * @return void
99
     */
100 55
    public function set(string $id, $value): void
101
    {
102 55
        $this->cache[$id] = $value;
103 55
    }
104
105
    /**
106
     * Delete value from container.
107
     *
108
     * @param string $id
109
     *
110
     * @return bool
111
     */
112 24
    public function delete(string $id): bool
113
    {
114 24
        if (\array_key_exists($id, $this->cache)) {
115
116
            //delete value
117 18
            unset($this->cache[$id]);
118
119
            //return function result
120 18
            return true;
121
        }
122
123 6
        return false;
124
    }
125
126
    /**
127
     * Resolve dependencies for given class.
128
     *
129
     * @param string       $class An existing class.
130
     * @param array<mixed> $rules Custom rules.
131
     *
132
     * @return object|null Instance of resolved class
133
     */
134 18
    public function resolve(string $class, array $rules = []): ?object
135
    {
136
        //reset tree;
137 18
        $this->tree = [];
138
139
        //merge rules passed as parameter with general rules
140 18
        $this->rules = \array_merge($this->rules, $rules);
141
142
        //build dependency tree
143 18
        $this->buildTreeRecursive($class);
144
        //$this->buildTree($class);
145
146
        //build objects
147 18
        $this->buildObjects();
148
149
        //return required class
150 18
        return $this->cache[$class] ?? null;
151
    }
152
153
    /**
154
     * Create a map of dependencies for a class
155
     *
156
     * @param string    $class Class wich tree will build
157
     * @param int       $level Level for dependency
158
     *
159
     * @return void
160
     */
161 18
    private function buildTreeRecursive(string $class, int $level = 0): void
162
    {
163
        //initialize array
164 18
        $this->tree[$level][$class] = [];
165
166
        //get parameter from constructor
167
        //can return error when constructor not declared
168 18
        $constructor = (new ReflectionClass($class))->getConstructor();//->getParameters();
169
        //this should resolve the error when a class without constructor is encountered
170 18
        $parameters = \is_null($constructor) ? [] : $constructor->getParameters();
171
172
        //loop parameter
173 18
        foreach ($parameters as $param) {
174
175
            //Function ReflectionType::__toString() is deprecated
176
            //FROM https://www.php.net/manual/en/migration74.deprecated.php
177
            //Calls to ReflectionType::__toString() now generate a deprecation notice.
178
            //This method has been deprecated in favor of ReflectionNamedType::getName()
179
            //in the documentation since PHP 7.1, but did not throw a deprecation notice
180
            //for technical reasons.
181
            //Get the data type of the parameter
182 13
            $type = \is_null($param->getType()) ? self::NO_TYPE : $param->getType()->getName();
183
184
            //if parameter is an interface
185
            //check rules for an implementation and
186
            //replace interface with implementation
187 13
            if (\interface_exists($type)) {
188
                //override type with interface implamentation
189
                //declared in rules
190 3
                $type = $this->rules[$type];
191
            }
192
193
            //if there is a parameter with callable type
194 13
            if (\class_exists($type)) {
195
196
                //store dependency
197 13
                $this->tree[$level][$class][] = $param;
198
199
                //call recursive, head recursion
200 13
                $this->buildTreeRecursive($type, $level + 1);
201
202
                //continue
203 13
                continue;
204
            }
205
206 2
            $this->tree[$level][$class][] = $param;
207
        }
208 18
    }
209
210
    /**
211
     * Create a dependencies map for a class.
212
     *
213
     * @param string $class
214
     *
215
     * @return void
216
     *
217
     * @deprecated since version 0.27.0 Return to recursive build of the tree
218
     *                                  because is 1.5x time fast.
219
     */
220
    private function buildTree(string $class): void
0 ignored issues
show
Unused Code introduced by
The method buildTree() is not used, and could be removed.

This check looks for private methods that have been defined, but are not used inside the class.

Loading history...
221
    {
222
        $level = 0;
223
        $stack = new SplStack();
224
225
        while (true) {
226
227
            //initialize array if not already initialized
228
            $this->tree[$level][$class] ??= [];
229
230
            //get parameter from constructor
231
            //can return error when constructor not declared
232
            $constructor = (new ReflectionClass($class))->getConstructor();//->getParameters();
233
            //this should resolve the error when a class without constructor is encountered
234
            $parameters = \is_null($constructor) ? [] : $constructor->getParameters();
235
236
            //loop parameter
237
            foreach ($parameters as $param) {
238
239
                //check if argument is already stored
240
                $notAlreadyStored = !\in_array($param, $this->tree[$level][$class]);
241
242
                //Function ReflectionType::__toString() is deprecated
243
                //FROM https://www.php.net/manual/en/migration74.deprecated.php
244
                //Calls to ReflectionType::__toString() now generate a deprecation notice.
245
                //This method has been deprecated in favor of ReflectionNamedType::getName()
246
                //in the documentation since PHP 7.1, but did not throw a deprecation notice
247
                //for technical reasons.
248
                $type = ($param->getType() !== null) ? $param->getType()->getName() : self::NO_TYPE;
249
250
                //if there is parameter with callable type
251
                if (\class_exists($type) && $notAlreadyStored) {
252
253
                    //push values in stack for simulate later recursive function
254
                    $stack->push([$level, $class]);
255
256
                    //store dependency
257
                    $this->tree[$level][$class][] = $param;
258
259
                    //update values for simulate recursive function
260
                    $level++;
261
                    $class = $param->getClass()->name;
262
263
                    //return to main while
264
                    continue 2;
265
                }
266
267
                if ($notAlreadyStored) {
268
                    //store dependency
269
                    $this->tree[$level][$class][] = $param;
270
                }
271
            }
272
273
            //if stack is empty break while end exit from function
274
            if ($stack->count() === 0) {
275
                break;
276
            }
277
278
            //get last value pushed into stack;
279
            list($level, $class) = $stack->pop();
280
        }
281
    }
282
283
    /**
284
     * Build objects from dependencyTree.
285
     *
286
     * @return void
287
     */
288 18
    private function buildObjects(): void
289
    {
290
        //deep dependency level, start to array end for not use array_reverse
291 18
        for ($i = \count($this->tree) - 1; $i >= 0; $i--) {
292
293
            //class
294 18
            foreach ($this->tree[$i] as $class => $arguments) {
295
296
                //try to find object in class
297 18
                if (isset($this->cache[$class])) {
298
                    // no build need
299 9
                    continue;
300
                }
301
302
                //object is not in cache and need arguments try to build it
303 18
                if (\count($arguments)) {
304
305
                    //build arguments
306 13
                    $args = $this->buildArguments($class, $arguments);
307
308
                    //store object with dependencies in cache
309 13
                    $this->cache[$class] = (new ReflectionClass($class))->newInstanceArgs($args);
310
311 13
                    continue;
312
                }
313
314
                //store object in cache
315 18
                $this->cache[$class] = (new ReflectionClass($class))->newInstance();
316
            }
317
        }
318 18
    }
319
320
    /**
321
     * Build dependency for a object.
322
     *
323
     * @param string       $class
324
     * @param array<mixed> $dependency
325
     *
326
     * @return array<mixed>
327
     */
328 13
    private function buildArguments(string $class, array $dependency): array
329
    {
330
        //initialize arguments array
331 13
        $args = [];
332
333
        //argument required from class
334 13
        foreach ($dependency as $argValue) {
335 13
            $argType = \is_null($argValue->getType()) ? self::NO_TYPE : $argValue->getType()->getName();
336
337 13
            if (\interface_exists($argType)) {
338
                //retrive concrete class bound to inteface in rules
339 3
                $args[] = $this->cache[$this->rules[$argType]];
340 3
                continue;
341
            }
342
343 13
            if (\class_exists($argType)) {
344
                //add to array of arguments
345 13
                $args[] = $this->cache[$argType];
346 13
                continue;
347
            }
348
349 1
            $args[] = null;
350
        }
351
352
        //check if there is rules for this class
353 13
        if (isset($this->rules[$class])) {
354
            //merge arguments
355 1
            $args = \array_replace($args, $this->rules[$class]);
356
        }
357
358 13
        return $args;
359
    }
360
}
361