Completed
Push — master ( 206b69...066e6d )
by Vitaly
06:26
created

Container::resolve()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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