Completed
Push — master ( b03211...1366fa )
by Vitaly
10:18
created

Container   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 345
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 93.55%

Importance

Changes 29
Bugs 1 Features 14
Metric Value
wmc 38
c 29
b 1
f 14
lcom 1
cbo 5
dl 0
loc 345
ccs 116
cts 124
cp 0.9355
rs 8.3999

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A logic() 0 4 1
A getClassName() 0 7 3
B buildDependenciesTree() 0 36 6
D generateLogicConditions() 0 45 9
B generateLogicFunction() 0 60 8
A get() 0 15 3
A has() 0 6 3
A callback() 0 11 1
A service() 0 6 1
A instance() 0 5 1
A set() 0 14 1
1
<?php
2
/**
3
 * Created by Vitaly Iegorov <[email protected]>.
4
 * on 22.01.16 at 23:53
5
 */
6
namespace samsonframework\di;
7
8
use samsonframework\di\exception\ClassNotFoundException;
9
use samsonframework\di\exception\ConstructorParameterNotSetException;
10
use samsonframework\di\exception\ContainerException;
11
use samsonframework\di\exception\NotFoundException;
12
use samsonphp\generator\Generator;
13
14
//TODO: Interface & abstract class resolving.
15
//TODO: Lazy creation by default, need to use Mocks and magic methods.
16
//TODO: existing instances passing to logic function.
17
//TODO: closure and other fully qualified name resolving
18
19
/**
20
 * Dependency injection container.
21
 *
22
 * @package samsonframework\di
23
 */
24
class Container implements ContainerInterface
25
{
26
    const LOGIC_FUNCTION_NAME = 'diContainer';
27
28
    /** @var array[string] Collection of alias => class name for alias resolving */
29
    protected $aliases = array();
30
31
    /** @var array[string] Collection of alias => closure for alias resolving */
32
    protected $callbacks = array();
33
34
    /** @var array[string] Collection of loaded services */
35
    protected $services = array();
36
37
    /** @var array[string] Collection of class name dependencies trees */
38
    protected $dependencies = array();
39
40
    /** @var Generator */
41
    protected $generator;
42
43
    /**
44
     * Container constructor.
45
     *
46
     * @param Generator $generator
47
     */
48 4
    public function __construct(Generator $generator)
49
    {
50 4
        $this->generator = $generator;
51 4
    }
52
53
    /**
54
     * Internal logic handler. Calls generated logic function
55
     * for performing entity creation or search. This is encapsulated
56
     * method for further overriding.
57
     *
58
     * @param string $alias Entity alias
59
     *
60
     * @return mixed Created instance or null
61
     */
62 3
    protected function logic($alias)
63
    {
64 3
        return call_user_func(self::LOGIC_FUNCTION_NAME, $alias);
65
    }
66
67
    /**
68
     * Get reflection paramater class name type hint if present without
69
     * auto loading and throwing exceptions.
70
     *
71
     * @param \ReflectionParameter $param Parameter for parsing
72
     *
73
     * @return string|null Class name typehint or null
74
     */
75 4
    protected function getClassName(\ReflectionParameter $param)
76
    {
77 4
        preg_match('/\[\s\<\w+?>\s(?<class>[\w\\\\]+)/', (string)$param, $matches);
78 4
        return array_key_exists('class', $matches) && $matches['class'] !== 'array'
79 4
            ? '\\' . ltrim($matches[1], '\\')
80 4
            : null;
81
    }
82
83
    /**
84
     * Recursively build class constructor dependencies tree.
85
     * TODO: Analyze recurrent dependencies and throw an exception
86
     *
87
     * @param string $className    Current class name for analyzing
88
     * @param array  $dependencies Reference to tree for filling up
89
     *
90
     * @return array [string] Multidimensional array as dependency tree
91
     * @throws ClassNotFoundException
92
     */
93 4
    protected function buildDependenciesTree($className, array &$dependencies)
94
    {
95
        // We need this class to exists to use reflections, it will try to autoload it also
96 4
        if (class_exists($className)) {
97 4
            $class = new \ReflectionClass($className);
98
            // We can build dependency tree only from constructor dependencies
99 4
            $constructor = $class->getConstructor();
100 4
            if (null !== $constructor) {
101
                // Iterate all dependencies
102 4
                foreach ($constructor->getParameters() as $parameter) {
103
                    // Ignore optional parameters
104 4
                    if (!$parameter->isOptional()) {
105
                        // Read dependency class name
106 4
                        $dependencyClass = $this->getClassName($parameter);
107
108
                        // Set pointer to parameter as it can be set before
109 4
                        $parameterPointer = &$dependencies[$className][$parameter->getName()];
110
111
                        // If we have found dependency class
112 4
                        if ($dependencyClass !== null) {
113
                            // Point dependency class name
114 4
                            $parameterPointer = $dependencyClass;
115
                            // Go deeper in recursion and pass new branch there
116 4
                            $this->buildDependenciesTree($dependencyClass, $dependencies);
117 4
                        }
118 4
                    } else { // Stop iterating as first optional parameter is met
119 4
                        break;
120
                    }
121 4
                }
122 4
            }
123 4
        } else { // Something went wrong and class is not auto loaded and missing
124
            throw new ClassNotFoundException($className);
125
        }
126
127 4
        return $dependencies;
128
    }
129
130
    /**
131
     * Recursive object creation with dependencies.
132
     *
133
     * @param array  $dependencies Collection of current class dependenices
134
     * @param string $class        Current class name
135
     *
136
     * @throws ConstructorParameterNotSetException
137
     */
138
139 1
    public function generateLogicConditions(array &$dependencies, $class)
140
    {
141
        // Start service creation
142 1
        if (array_key_exists($class, $this->services)) {
143 1
            $this->generator->newLine('isset($services[\''.$class.'\'])');
144 1
            $this->generator->newLine('? $services[\''.$class.'\']');
145 1
            $this->generator->newLine(': $services[\''.$class.'\'] = new '.$class.'(');
146 1
        } else { // Regular entity creation
147 1
            $this->generator->newLine('new ' . $class . '(');
148
        }
149 1
        $this->generator->tabs++;
150
151
        // Get last dependency variable name
152 1
        end($dependencies);
153 1
        $last = key($dependencies);
154
155
        // Iterate all dependencies for this class
156 1
        foreach ($dependencies as $variable => $dependency) {
157
            // If dependency value is a string
158 1
            if (is_string($dependency)) {
159
                // Define if we have this dependency described in dependency tree
160 1
                $dependencyPointer = &$this->dependencies[$dependency];
161 1
                if (null !== $dependencyPointer) {
162
                    // We have dependencies tree for this entity
163 1
                    $this->generateLogicConditions($dependencyPointer, $dependency);
164 1
                } elseif (class_exists($dependency, false)) {
165
                    // There are no dependencies for this entity
166 1
                    $this->generator->newLine('new ' . $dependency . '()');
167 1
                } else { // String variable
168 1
                    $this->generator->newLine()->stringValue($dependency);
169
                }
170 1
            } elseif (is_array($dependency)) { // Dependency value is array
171 1
                $this->generator->newLine()->arrayValue($dependency);
172 1
            } elseif ($dependency === null) { // Parameter is not set
173
                throw new ConstructorParameterNotSetException($class . '::' . $variable);
174
            }
175
176
            // Add comma if this is not last dependency
177 1
            if ($variable !== $last) {
178 1
                $this->generator->text(',');
179 1
            }
180 1
        }
181 1
        $this->generator->tabs--;
182 1
        $this->generator->newLine(')');
183 1
    }
184
185
    /**
186
     * @param string $functionName
187
     *
188
     * @return string
189
     * @throws ConstructorParameterNotSetException
190
     */
191 1
    public function generateLogicFunction($functionName = self::LOGIC_FUNCTION_NAME)
192
    {
193 1
        $inputVariable = '$aliasOrClassName';
194 1
        $this->generator
195 1
            ->defFunction($functionName, array($inputVariable))
196 1
            ->defVar('static $services')
197 1
        ->newLine();
198
199 1
        reset($this->dependencies);
200 1
        $first = key($this->dependencies);
201
202 1
        foreach ($this->dependencies as $className => $dependencies) {
203
            // Generate condition statement to define if this class is needed
204 1
            $conditionFunc = $className === $first ? 'defIfCondition' : 'defElseIfCondition';
205 1
            $this->generator->$conditionFunc($inputVariable . ' === \'' . $className . '\'');
206
207
            // If this is a callback entity
208 1
            if (array_key_exists($className, $this->callbacks)) {
209
                // Get closure reflection
210 1
                $reflection = new \ReflectionFunction($this->callbacks[$className]);
211
                // Read closure file
212 1
                $lines = file($reflection->getFileName());
213 1
                $opened = 0;
214
                // Read only closure lines
215 1
                for($l = $reflection->getStartLine(); $l < $reflection->getEndLine(); $l++) {
0 ignored issues
show
Coding Style introduced by
Expected 1 space after FOR keyword; 0 found
Loading history...
216
                    // Fix openning braces scope
217 1
                    if (strpos($lines[$l], '{') !== false) {
218
                        $opened++;
219
                    }
220
221
                    // Fix closing braces scope
222 1
                    if (strpos($lines[$l], '}') !== false) {
223 1
                        $opened--;
224 1
                    }
225
226
                    // Break if we reached closure end
227 1
                    if ($opened === -1) {
228 1
                        break;
229
                    }
230
231
                    // Add closure code
232 1
                    $this->generator->newLine(trim($lines[$l]));
233 1
                }
234 1
            } else { // Regular entity
235 1
                $this->generator->newLine('return ');
236
237
                // Go to recursive dependencies definition
238 1
                $this->generateLogicConditions($dependencies, $className);
239
240
                // Close top level instance creation
241 1
                $this->generator->text(';');
242
            }
243 1
        }
244
245
        // Add method not found
246 1
        return $this->generator
247 1
            ->endIfCondition()
248 1
            ->endFunction()
249 1
            ->flush();
250
    }
251
252
    /**
253
     * Finds an entry of the container by its identifier and returns it.
254
     *
255
     * @param string $alias Identifier of the entry to look for.
256
     *
257
     * @throws NotFoundException  No entry was found for this identifier.
258
     * @throws ContainerException Error while retrieving the entry.
259
     *
260
     * @return mixed Entry.
261
     */
262 3
    public function get($alias)
263
    {
264
        // Get pointer from logic
265 3
        $module = $this->logic($alias);
266
267 3
        if (null === $module) {
268
            throw new NotFoundException($alias);
269
        } else {
270 3
            if (!is_object($module)) {
271
                throw new ContainerException($alias);
272
            } else {
273 3
                return $module;
274
            }
275
        }
276
    }
277
278
    /**
279
     * Returns true if the container can return an entry for the given identifier.
280
     * Returns false otherwise.
281
     *
282
     * @param string $alias Identifier of the entry to look for.
283
     *
284
     * @return boolean
285
     */
286 1
    public function has($alias)
287
    {
288 1
        return array_key_exists($alias, $this->dependencies)
289 1
        || array_key_exists($alias, $this->services)
290 1
        || array_key_exists($alias, $this->aliases);
291
    }
292
293
    /**
294
     * Set dependency alias with callback function.
295
     *
296
     * @param callable $callable Callable to return dependency
297
     * @param string   $alias    Dependency name
298
     *
299
     * @return self Chaining
300
     */
301 4
    public function callback($callable, $alias = null)
302
    {
303
        // Add unique closure alias
304 4
        $this->aliases[$alias] = 'closure'.uniqid(0, 99999);
305
306
        // Store callback
307 4
        $this->callbacks[$alias] = $callable;
308
309
        // Store dependency
310 4
        $this->dependencies[$alias] = $callable;
311 4
    }
312
313
    /**
314
     * Set service dependency. Upon first creation of this class instance
315
     * it would be used everywhere where this dependency is needed.
316
     *
317
     * @param string $className  Fully qualified class name
318
     * @param string $alias      Dependency name
319
     * @param array  $parameters Collection of parameters needed for dependency creation
320
     *
321
     * @return self Chaining
322
     */
323 4
    public function service($className, $alias = null, array $parameters = array())
324
    {
325 4
        $this->services[$className] = $className;
326
327 4
        return $this->set($className, $alias, $parameters);
328
    }
329
330
    /**
331
     * Set service dependency by passing object instance.
332
     *
333
     * @param mixed  $instance   Instance that needs to be return by this dependency
334
     * @param string $alias      Dependency name
335
     * @param array  $parameters Collection of parameters needed for dependency creation
336
     *
337
     * @return self Chaining
338
     */
339
    public function instance(&$instance, $alias = null, array $parameters = array())
0 ignored issues
show
Unused Code introduced by
The parameter $instance is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $alias is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $parameters is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
340
    {
341
342
        // TODO: Implement instance() method.
343
    }
344
345
    /**
346
     * Set dependency.
347
     *
348
     * @param string $className  Fully qualified class name
349
     * @param string $alias      Dependency name
350
     * @param array  $parameters Collection of parameters needed for dependency creation
351
     *
352
     * @return self Chaining
353
     */
354 4
    public function set($className, $alias = null, array $parameters = array())
355
    {
356
        // Add this class dependencies to dependency tree
357 4
        $this->dependencies = array_merge(
358 4
            $this->dependencies,
359 4
            $this->buildDependenciesTree($className, $this->dependencies)
360 4
        );
361
362
        // Merge other class constructor parameters
363 4
        $this->dependencies[$className] = array_merge($this->dependencies[$className], $parameters);
364
365
        // Store alias for this class name
366 4
        $this->aliases[$alias] = $className;
367 4
    }
368
}
369