Completed
Push — master ( 8c9da3...617922 )
by Sebastian
03:57
created

Container::get()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 2

Importance

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