Completed
Push — master ( 1755c0...c18405 )
by Vitaly
02:35
created

Container::generateLogicConditions()   D

Complexity

Conditions 9
Paths 24

Size

Total Lines 45
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 29
CRAP Score 9.0029

Importance

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