Completed
Push — master ( 70c1b4...668273 )
by Vitaly
16:21 queued 11:35
created

Container::buildDependenciesTree()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 36
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 6.0045

Importance

Changes 7
Bugs 0 Features 2
Metric Value
c 7
b 0
f 2
dl 0
loc 36
ccs 19
cts 20
cp 0.95
rs 8.439
cc 6
eloc 17
nc 4
nop 2
crap 6.0045
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
    protected $generator;
40
41
    /**
42
     * Container constructor.
43
     *
44
     * @param Generator $generator
45
     */
46 1
    public function __construct(Generator $generator)
47
    {
48 1
        $this->generator = $generator;
49 1
    }
50
51
    /**
52
     * Internal logic handler. Calls generated logic function
53
     * for performing entity creation or search. This is encapsulated
54
     * method for further overriding.
55
     *
56
     * @param string $alias Entity alias
57
     *
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
     *
71
     * @return string|null Class name typehint or null
72
     */
73 1
    protected function getClassName(\ReflectionParameter $param)
74
    {
75 1
        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 1
            : null;
79
    }
80
81
    /**
82
     * Recursively build class constructor dependencies tree.
83
     * TODO: Analyze recurrent dependencies and throw an exception
84
     *
85
     * @param string $className    Current class name for analyzing
86
     * @param array  $dependencies Reference to tree for filling up
87
     *
88
     * @return array [string] Multidimensional array as dependency tree
89
     * @throws ClassNotFoundException
90
     */
91 1
    protected function buildDependenciesTree($className, array &$dependencies)
92
    {
93
        // 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
            // We can build dependency tree only from constructor dependencies
97 1
            $constructor = $class->getConstructor();
98 1
            if (null !== $constructor) {
99
                // Iterate all dependencies
100 1
                foreach ($constructor->getParameters() as $parameter) {
101
                    // Ignore optional parameters
102 1
                    if (!$parameter->isOptional()) {
103
                        // Read dependency class name
104 1
                        $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
            throw new ClassNotFoundException($className);
123
        }
124
125 1
        return $dependencies;
126
    }
127
128
    /**
129
     * Recursive object creation with dependencies.
130
     *
131
     * @param array  $dependencies Collection of current class dependenices
132
     * @param string $class        Current class name
133
     *
134
     * @throws ConstructorParameterNotSetException
135
     */
136
137 1
    public function generateLogicConditions(array &$dependencies, $class)
138
    {
139
        // Start entity creation
140 1
        $this->generator->newLine('new ' . $class . '(');
141 1
        $this->generator->tabs++;
142
143
        // Get last dependency variable name
144 1
        end($dependencies);
145 1
        $last = key($dependencies);
146
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
                // Define if we have this dependency described in dependency tree
152 1
                $dependencyPointer = &$this->dependencies[$dependency];
153 1
                if (null !== $dependencyPointer) {
154 1
                    $this->generateLogicConditions($dependencyPointer, $dependency);
155 1
                } elseif (class_exists($dependency, false)) {
156 1
                    $this->generator->newLine('new ' . $dependency . '()');
157 1
                } else { // String variable
158 1
                    $this->generator->newLine()->stringValue($dependency);
159
                }
160 1
            } elseif (is_array($dependency)) { // Regular constructor parameter
161 1
                $this->generator->newLine()->arrayValue($dependency);
162 1
            } elseif ($dependency === null) { // Parameter is not set
163
                throw new ConstructorParameterNotSetException($class . '::' . $variable);
164
            }
165
166
            // Add comma if this is not last dependency
167 1
            if ($variable !== $last) {
168 1
                $this->generator->text(',');
169 1
            }
170 1
        }
171 1
        $this->generator->tabs--;
172 1
        $this->generator->newLine(')');
173 1
    }
174
175
    /**
176
     * @param string $functionName
177
     *
178
     * @return string
179
     * @throws ConstructorParameterNotSetException
180
     */
181 1
    public function generateLogicFunction($functionName = self::LOGIC_FUNCTION_NAME)
182
    {
183 1
        $inputVariable = '$aliasOrClassName';
184 1
        $this->generator
185 1
            ->defFunction($functionName, array($inputVariable))
186 1
            ->defVar('static $services')
187 1
        ->newLine();
188
189 1
        reset($this->dependencies);
190 1
        $first = key($this->dependencies);
191
192 1
        foreach ($this->dependencies as $className => $dependencies) {
193
            // Generate condition statement to define if this class is needed
194 1
            $conditionFunc = $className === $first ? 'defIfCondition' : 'defElseIfCondition';
195 1
            $this->generator->$conditionFunc($inputVariable . ' === \'' . $className . '\'');
196
197 1
            $this->generator->newLine('return ');
198
199
            // Go to recursive dependencies definition
200 1
            $this->generateLogicConditions($dependencies, $className);
201
202
            // Close top level instance creation
203 1
            $this->generator->text(';');
204 1
        }
205
206
        // Add method not found
207 1
        return $this->generator
208 1
            ->endIfCondition()
209 1
            ->endFunction()
210 1
            ->flush();
211
    }
212
213
    /**
214
     * Finds an entry of the container by its identifier and returns it.
215
     *
216
     * @param string $alias Identifier of the entry to look for.
217
     *
218
     * @throws NotFoundException  No entry was found for this identifier.
219
     * @throws ContainerException Error while retrieving the entry.
220
     *
221
     * @return mixed Entry.
222
     */
223 1
    public function get($alias)
224
    {
225
        // Get pointer from logic
226 1
        $module = diContainer($alias);
227
228 1
        if (null === $module) {
229
            throw new NotFoundException($alias);
230
        } else {
231 1
            if (!is_object($module)) {
232
                throw new ContainerException($alias);
233
            } else {
234 1
                return $module;
235
            }
236
        }
237
    }
238
239
    /**
240
     * Returns true if the container can return an entry for the given identifier.
241
     * Returns false otherwise.
242
     *
243
     * @param string $alias Identifier of the entry to look for.
244
     *
245
     * @return boolean
246
     */
247
    public function has($alias)
248
    {
249
        return array_key_exists($alias, $this->services)
250
        || array_key_exists($alias, $this->aliases);
251
    }
252
253
    /**
254
     * Set dependency alias with callback function.
255
     *
256
     * @param callable $callable Callable to return dependency
257
     * @param string   $alias    Dependency name
258
     *
259
     * @return self Chaining
260
     */
261
    public function callback($callable, $alias = null)
262
    {
263
        // TODO: Implement callback() method.
264
    }
265
266
    /**
267
     * Set service dependency. Upon first creation of this class instance
268
     * it would be used everywhere where this dependency is needed.
269
     *
270
     * @param string $className  Fully qualified class name
271
     * @param string $alias      Dependency name
272
     * @param array  $parameters Collection of parameters needed for dependency creation
273
     *
274
     * @return self Chaining
275
     */
276
    public function service($className, $alias = null, array $parameters = array())
277
    {
278
        // TODO: Implement service() method.
279
    }
280
281
    /**
282
     * Set service dependency by passing object instance.
283
     *
284
     * @param mixed  $instance   Instance that needs to be return by this dependency
285
     * @param string $alias      Dependency name
286
     * @param array  $parameters Collection of parameters needed for dependency creation
287
     *
288
     * @return self Chaining
289
     */
290
    public function instance(&$instance, $alias = null, array $parameters = array())
291
    {
292
293
        // TODO: Implement instance() method.
294
    }
295
296
    /**
297
     * Set dependency.
298
     *
299
     * @param string $className  Fully qualified class name
300
     * @param string $alias      Dependency name
301
     * @param array  $parameters Collection of parameters needed for dependency creation
302
     *
303
     * @return ContainerInterface Chaining
304
     */
305 1
    public function set($className, $alias = null, array $parameters = array())
306
    {
307
        // Add this class dependencies to dependency tree
308 1
        $this->dependencies = array_merge(
309 1
            $this->dependencies,
310 1
            $this->buildDependenciesTree($className, $this->dependencies)
311 1
        );
312
313
        // Merge other class constructor parameters
314 1
        $this->dependencies[$className] = array_merge($this->dependencies[$className], $parameters);
315
316
        // Store alias for this class name
317 1
        $this->aliases[$alias] = $className;
318 1
    }
319
}
320