Passed
Push — master ( 68a1b2...e2bbd7 )
by Sebastian
05:48
created

Container::buildArguments()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 25
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 10
cts 10
cp 1
rs 8.5806
c 0
b 0
f 0
cc 4
eloc 10
nc 6
nop 2
crap 4
1
<?php
2
3
/**
4
 * Linna Framework.
5
 *
6
 * @author Sebastian Rapetti <[email protected]>
7
 * @copyright (c) 2017, Sebastian Rapetti
8
 * @license http://opensource.org/licenses/MIT MIT License
9
 */
10
declare(strict_types=1);
11
12
namespace Linna\DI;
13
14
use Linna\DI\Exception\NotFoundException;
15
use Psr\Container\ContainerInterface;
16
17
/**
18
 * Dependency Injection Container and Resolver.
19
 */
20
class Container implements ContainerInterface, \ArrayAccess
21
{
22
    use PropertyAccessTrait;
23
    use ArrayAccessTrait;
24
25
    /**
26
     * @var array Contains object already resolved
27
     */
28
    private $cache = [];
29
30
    /**
31
     * @var array A map for resolve dependencies
32
     */
33
    protected $tree = [];
34
35
    /**
36
     * @var array For resolve scalar arguments and unexpected behavior
37
     */
38
    protected $rules = [];
39
40
    /**
41
     * {@inheritdoc}
42
     */
43 36
    public function get($key)
44
    {
45 36
        if (isset($this->cache[$key])) {
46 18
            return $this->cache[$key];
47
        }
48
49 18
        throw new NotFoundException('No entry was found for this identifier');
50
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 36
    public function has($key)
56
    {
57 36
        return isset($this->cache[$key]);
58
    }
59
60
    /**
61
     * Store a value inside container.
62
     *
63
     * @param string   $key
64
     * @param mixed $value
65
     */
66 55
    public function set(string $key, $value)
67
    {
68 55
        $this->cache[$key] = $value;
69 55
    }
70
71
    /**
72
     * Delete value from container.
73
     *
74
     * @param string $key
75
     */
76 24
    public function delete(string $key) : bool
77
    {
78 24
        if (array_key_exists($key, $this->cache)) {
79
80
            //delete value
81 18
            unset($this->cache[$key]);
82
83
            //return function result
84 18
            return true;
85
        }
86
87 6
        return false;
88
    }
89
90
    /**
91
     * Set rules for unserolvable classes.
92
     *
93
     * @param array $rules
94
     */
95 1
    public function setRules(array $rules)
96
    {
97 1
        $this->rules = $rules;
98 1
    }
99
100
    /**
101
     * Resolve dependencies for given class.
102
     *
103
     * @param string $class
104
     * @param array  $rules
105
     *
106
     * @return object|null Instance of resolved class
107
     */
108 11
    public function resolve(string $class, array $rules = [])
109
    {
110
        //merge rules passed as parameter with general rules
111 11
        $this->rules = array_merge($this->rules, $rules);
112
113
        //build dependency tree
114 11
        $this->buildTree($class);
115
116
        //build objects
117 11
        $this->buildObjects();
118
119
        //return required class
120 11
        return $this->cache[$class] ?? null;
121
    }
122
123
    /**
124
     * Create a dependencies map for a class.
125
     *
126
     * @param string $class
127
     */
128 11
    private function buildTree(string $class)
129
    {
130 11
        $level = 0;
131 11
        $stack = new \SplStack();
132
133 11
        while (true) {
134
135
            //initialize array if not already initialized
136 11
            if (!isset($this->tree[$level][$class])) {
137 11
                $this->tree[$level][$class] = [];
138
            }
139
140
            //get parameter from constructor
141 11
            $parameters = (new \ReflectionClass($class))->getConstructor()->getParameters();
142
143
            //loop parameter
144 11
            foreach ($parameters as $param) {
145
146
                //check if argument is already stored
147 7
                $notAlreadyStored = !in_array($param, $this->tree[$level][$class]);
148
149
                //if there is parameter with callable type
150 7
                if (class_exists((string) $param->getType()) && $notAlreadyStored) {
151
152
                    //push values in stack for simulate later recursive function
153 7
                    $stack->push([$level, $class]);
154
155
                    //store dependency
156 7
                    $this->tree[$level][$class][] = $param;
157
158
                    //update values for simulate recursive function
159 7
                    $level++;
160 7
                    $class = $param->getClass()->name;
161
162
                    //return to main while
163 7
                    continue 2;
164
                }
165
166 7
                if ($notAlreadyStored) {
167
                    //store dependency
168 2
                    $this->tree[$level][$class][] = $param;
169
                }
170
            }
171
172
            //if stack is empty break while end exit from function
173 11
            if ($stack->count() === 0) {
174 11
                break;
175
            }
176
177
            //get last value pushed into stack;
178 7
            list($level, $class) = $stack->pop();
179
        }
180 11
    }
181
182
    /**
183
     * Build objects from dependencyTree.
184
     */
185 11
    private function buildObjects()
186
    {
187
        //deep dependency level, start to array end for not use array_reverse
188 11
        for ($i = count($this->tree) - 1; $i >= 0; $i--) {
189
190
            //class
191 11
            foreach ($this->tree[$i] as $class => $arguments) {
192
193
                //try to find object in class
194 11
                $object = $this->cache[$class] ?? null;
195
196
                //if object is not in cache and need arguments try to build
197 11
                if ($object === null && count($arguments)) {
198
199
                    //build arguments
200 7
                    $args = $this->buildArguments($class, $arguments);
201
202
                    //store object with dependencies in cache
203 7
                    $this->cache[$class] = (new \ReflectionClass($class))->newInstanceArgs($args);
204
205 7
                    continue;
206
                }
207
208 11
                if ($object === null) {
209
210
                    //store object in cache
211 11
                    $this->cache[$class] = (new \ReflectionClass($class))->newInstance();
212
                }
213
            }
214
        }
215 11
    }
216
217
    /**
218
     * Build dependency for a object.
219
     *
220
     * @param string $class
221
     * @param array  $dependency
222
     *
223
     * @return array
224
     */
225 7
    private function buildArguments(string $class, array $dependency): array
226
    {
227
        //initialize arguments array
228 7
        $args = [];
229
230
        //argument required from class
231 7
        foreach ($dependency as $argValue) {
232 7
            $paramClass = null;
233
            
234 7
            if (class_exists((string) $argValue->getType())) {
235 7
                $paramClass = $argValue->getClass()->name;
236
            }
237
238
            //add to array of arguments
239 7
            $args[] = $this->cache[$paramClass] ?? null;
240
        }
241
242
        //check if there is rules for this class
243 7
        if (isset($this->rules[$class])) {
244
            //merge arguments
245 1
            $args = array_replace($args, $this->rules[$class]);
246
        }
247
248 7
        return $args;
249
    }
250
}
251