Completed
Push — master ( 1366fa...209e41 )
by Vitaly
15:06 queued 08:05
created

Container::generateCallback()   B

Complexity

Conditions 5
Paths 9

Size

Total Lines 28
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 5.0488

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 28
ccs 14
cts 16
cp 0.875
rs 8.439
cc 5
eloc 12
nc 9
nop 1
crap 5.0488
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
     * Generate callback logic implementation as plain code.
187
     *
188
     * @param string $className Callback alias
189
     */
190 1
    protected function generateCallback($className)
191
    {
192
        // Get closure reflection
193 1
        $reflection = new \ReflectionFunction($this->callbacks[$className]);
194
        // Read closure file
195 1
        $lines = file($reflection->getFileName());
196 1
        $opened = 0;
197
        // Read only closure lines
198 1
        for ($l = $reflection->getStartLine(); $l < $reflection->getEndLine(); $l++) {
199
            // Fix opening braces scope
200 1
            if (strpos($lines[$l], '{') !== false) {
201
                $opened++;
202
            }
203
204
            // Fix closing braces scope
205 1
            if (strpos($lines[$l], '}') !== false) {
206 1
                $opened--;
207 1
            }
208
209
            // Break if we reached closure end
210 1
            if ($opened === -1) {
211 1
                break;
212
            }
213
214
            // Add closure code
215 1
            $this->generator->newLine(trim($lines[$l]));
216 1
        }
217 1
    }
218
219
    /**
220
     * @param string $functionName
221
     *
222
     * @return string
223
     * @throws ConstructorParameterNotSetException
224
     */
225 1
    public function generateLogicFunction($functionName = self::LOGIC_FUNCTION_NAME)
226
    {
227 1
        $inputVariable = '$aliasOrClassName';
228 1
        $this->generator
229 1
            ->defFunction($functionName, array($inputVariable))
230 1
            ->defVar('static $services')
231 1
        ->newLine();
232
233 1
        reset($this->dependencies);
234 1
        $first = key($this->dependencies);
235
236 1
        foreach ($this->dependencies as $className => $dependencies) {
237
            // Generate condition statement to define if this class is needed
238 1
            $conditionFunc = $className === $first ? 'defIfCondition' : 'defElseIfCondition';
239 1
            $this->generator->$conditionFunc($inputVariable . ' === \'' . $className . '\'');
240
241
            // If this is a callback entity
242 1
            if (array_key_exists($className, $this->callbacks)) {
243 1
                $this->generateCallback($className);
244 1
            } else { // Regular entity
245 1
                $this->generator->newLine('return ');
246
247
                // Go to recursive dependencies definition
248 1
                $this->generateLogicConditions($dependencies, $className);
249
250
                // Close top level instance creation
251 1
                $this->generator->text(';');
252
            }
253 1
        }
254
255
        // Add method not found
256 1
        return $this->generator
257 1
            ->endIfCondition()
258 1
            ->endFunction()
259 1
            ->flush();
260
    }
261
262
    /**
263
     * Finds an entry of the container by its identifier and returns it.
264
     *
265
     * @param string $alias Identifier of the entry to look for.
266
     *
267
     * @throws NotFoundException  No entry was found for this identifier.
268
     * @throws ContainerException Error while retrieving the entry.
269
     *
270
     * @return mixed Entry.
271
     */
272 3
    public function get($alias)
273
    {
274
        // Get pointer from logic
275 3
        $module = $this->logic($alias);
276
277 3
        if (null === $module) {
278
            throw new NotFoundException($alias);
279
        } else {
280 3
            if (!is_object($module)) {
281
                throw new ContainerException($alias);
282
            } else {
283 3
                return $module;
284
            }
285
        }
286
    }
287
288
    /**
289
     * Returns true if the container can return an entry for the given identifier.
290
     * Returns false otherwise.
291
     *
292
     * @param string $alias Identifier of the entry to look for.
293
     *
294
     * @return boolean
295
     */
296 1
    public function has($alias)
297
    {
298 1
        return array_key_exists($alias, $this->dependencies)
299 1
        || array_key_exists($alias, $this->services)
300 1
        || array_key_exists($alias, $this->aliases);
301
    }
302
303
    /**
304
     * Set dependency alias with callback function.
305
     *
306
     * @param callable $callable Callable to return dependency
307
     * @param string   $alias    Dependency name
308
     *
309
     * @return self Chaining
310
     */
311 4
    public function callback($callable, $alias = null)
312
    {
313
        // Add unique closure alias
314 4
        $this->aliases[$alias] = 'closure'.uniqid(0, 99999);
315
316
        // Store callback
317 4
        $this->callbacks[$alias] = $callable;
318
319
        // Store dependency
320 4
        $this->dependencies[$alias] = $callable;
321 4
    }
322
323
    /**
324
     * Set service dependency. Upon first creation of this class instance
325
     * it would be used everywhere where this dependency is needed.
326
     *
327
     * @param string $className  Fully qualified class name
328
     * @param string $alias      Dependency name
329
     * @param array  $parameters Collection of parameters needed for dependency creation
330
     *
331
     * @return self Chaining
332
     */
333 4
    public function service($className, $alias = null, array $parameters = array())
334
    {
335 4
        $this->services[$className] = $className;
336
337 4
        return $this->set($className, $alias, $parameters);
338
    }
339
340
    /**
341
     * Set service dependency by passing object instance.
342
     *
343
     * @param mixed  $instance   Instance that needs to be return by this dependency
344
     * @param string $alias      Dependency name
345
     * @param array  $parameters Collection of parameters needed for dependency creation
346
     *
347
     * @return self Chaining
348
     */
349
    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...
350
    {
351
352
        // TODO: Implement instance() method.
353
    }
354
355
    /**
356
     * Set dependency.
357
     *
358
     * @param string $className  Fully qualified class name
359
     * @param string $alias      Dependency name
360
     * @param array  $parameters Collection of parameters needed for dependency creation
361
     *
362
     * @return self Chaining
363
     */
364 4
    public function set($className, $alias = null, array $parameters = array())
365
    {
366
        // Add this class dependencies to dependency tree
367 4
        $this->dependencies = array_merge(
368 4
            $this->dependencies,
369 4
            $this->buildDependenciesTree($className, $this->dependencies)
370 4
        );
371
372
        // Merge other class constructor parameters
373 4
        $this->dependencies[$className] = array_merge($this->dependencies[$className], $parameters);
374
375
        // Store alias for this class name
376 4
        $this->aliases[$alias] = $className;
377 4
    }
378
}
379