Completed
Push — master ( e6d5b1...70c1b4 )
by Vitaly
14:55 queued 04:56
created

Container::buildDependenciesTree()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 25
CRAP Score 6.002

Importance

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