Passed
Push — master ( 4d2a94...142804 )
by Sebastian
03:29
created

Container::delete()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 12
c 0
b 0
f 0
ccs 5
cts 5
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
     * @var array Contains object already resolved.
30
     */
31
    private $cache = [];
32
33
    /**
34
     * @var array Hierarchical structure of dependencies.
35
     */
36
    protected $tree = [];
37
38
    /**
39
     * @var array Rules for resolve scalar arguments or unexpected behaviors.
40
     */
41
    protected $rules = [];
42
43
    /**
44
     * Class Constructor.
45
     *
46
     * @param array $rules Rules for resolve scalar arguments or unexpected behaviors.
47
     */
48 89
    public function __construct(array $rules = [])
49
    {
50 89
        $this->rules = $rules;
51 89
    }
52
53
    /**
54
     * Finds an entry of the container by its identifier and returns it.
55
     *
56
     * @param string $id Identifier of the entry to look for.
57
     *
58
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
59
     * @throws ContainerExceptionInterface Error while retrieving the entry.
60
     *
61
     * @return mixed Entry.
62
     */
63 36
    public function get($id)
64
    {
65 36
        if (isset($this->cache[$id])) {
66 18
            return $this->cache[$id];
67
        }
68
69 18
        throw new NotFoundException('No entry was found for this identifier.');
70
    }
71
72
    /**
73
     * Returns true if the container can return an entry for the given identifier.
74
     * Returns false otherwise.
75
     *
76
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
77
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
78
     *
79
     * @param string $id Identifier of the entry to look for.
80
     *
81
     * @return bool
82
     */
83 36
    public function has($id)
84
    {
85 36
        return isset($this->cache[$id]);
86
    }
87
88
    /**
89
     * Store a value inside container.
90
     *
91
     * @param string $id
92
     * @param mixed  $value
93
     */
94 55
    public function set(string $id, $value): void
95
    {
96 55
        $this->cache[$id] = $value;
97 55
    }
98
99
    /**
100
     * Delete value from container.
101
     *
102
     * @param string $id
103
     */
104 24
    public function delete(string $id): bool
105
    {
106 24
        if (array_key_exists($id, $this->cache)) {
107
108
            //delete value
109 18
            unset($this->cache[$id]);
110
111
            //return function result
112 18
            return true;
113
        }
114
115 6
        return false;
116
    }
117
118
    /**
119
     * Resolve dependencies for given class.
120
     *
121
     * @param string $class An existing class.
122
     * @param array  $rules Custom rules.
123
     *
124
     * @return object|null Instance of resolved class
125
     */
126 11
    public function resolve(string $class, array $rules = [])
127
    {
128
        //merge rules passed as parameter with general rules
129 11
        $this->rules = array_merge($this->rules, $rules);
130
131
        //build dependency tree
132 11
        $this->buildTree($class);
133
134
        //build objects
135 11
        $this->buildObjects();
136
137
        //return required class
138 11
        return $this->cache[$class] ?? null;
139
    }
140
141
    /**
142
     * Create a dependencies map for a class.
143
     *
144
     * @param string $class
145
     */
146 11
    private function buildTree(string $class): void
147
    {
148 11
        $level = 0;
149 11
        $stack = new SplStack();
150
151 11
        while (true) {
152
153
            //initialize array if not already initialized
154 11
            if (empty($this->tree[$level][$class])) {
155 11
                $this->tree[$level][$class] = [];
156
            }
157
158
            //get parameter from constructor
159 11
            $parameters = (new ReflectionClass($class))->getConstructor()->getParameters();
160
161
            //loop parameter
162 11
            foreach ($parameters as $param) {
163
164
                //check if argument is already stored
165 6
                $notAlreadyStored = !in_array($param, $this->tree[$level][$class]);
166
167
                //if there is parameter with callable type
168 6
                if (class_exists((string) $param->getType()) && $notAlreadyStored) {
169
170
                    //push values in stack for simulate later recursive function
171 6
                    $stack->push([$level, $class]);
172
173
                    //store dependency
174 6
                    $this->tree[$level][$class][] = $param;
175
176
                    //update values for simulate recursive function
177 6
                    $level++;
178 6
                    $class = $param->getClass()->name;
179
180
                    //return to main while
181 6
                    continue 2;
182
                }
183
184 6
                if ($notAlreadyStored) {
185
                    //store dependency
186 6
                    $this->tree[$level][$class][] = $param;
187
                }
188
            }
189
190
            //if stack is empty break while end exit from function
191 11
            if ($stack->count() === 0) {
192 11
                break;
193
            }
194
195
            //get last value pushed into stack;
196 6
            list($level, $class) = $stack->pop();
197
        }
198 11
    }
199
200
    /**
201
     * Build objects from dependencyTree.
202
     */
203 11
    private function buildObjects(): void
204
    {
205
        //deep dependency level, start to array end for not use array_reverse
206 11
        for ($i = count($this->tree) - 1; $i >= 0; $i--) {
207
208
            //class
209 11
            foreach ($this->tree[$i] as $class => $arguments) {
210
211
                //try to find object in class
212 11
                $object = $this->cache[$class] ?? null;
213
214
                //if object is not in cache and need arguments try to build
215 11
                if ($object === null && count($arguments)) {
216
217
                    //build arguments
218 6
                    $args = $this->buildArguments($class, $arguments);
219
220
                    //store object with dependencies in cache
221 6
                    $this->cache[$class] = (new ReflectionClass($class))->newInstanceArgs($args);
222
223 6
                    continue;
224
                }
225
226 11
                if ($object === null) {
227
228
                    //store object in cache
229 11
                    $this->cache[$class] = (new ReflectionClass($class))->newInstance();
230
                }
231
            }
232
        }
233 11
    }
234
235
    /**
236
     * Build dependency for a object.
237
     *
238
     * @param string $class
239
     * @param array  $dependency
240
     *
241
     * @return array
242
     */
243 6
    private function buildArguments(string $class, array $dependency): array
244
    {
245
        //initialize arguments array
246 6
        $args = [];
247
248
        //argument required from class
249 6
        foreach ($dependency as $argValue) {
250 6
            $paramClass = null;
251
252 6
            if (class_exists((string) $argValue->getType())) {
253 6
                $paramClass = $argValue->getClass()->name;
254
            }
255
256
            //add to array of arguments
257 6
            $args[] = $this->cache[$paramClass] ?? null;
258
        }
259
260
        //check if there is rules for this class
261 6
        if (isset($this->rules[$class])) {
262
            //merge arguments
263 1
            $args = array_replace($args, $this->rules[$class]);
264
        }
265
266 6
        return $args;
267
    }
268
}
269