Completed
Push — master ( 1a10c9...f32a6f )
by Vitaly
02:49
created

Container::buildDependenciesTree()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 21
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

Changes 11
Bugs 0 Features 4
Metric Value
c 11
b 0
f 4
dl 0
loc 21
ccs 0
cts 11
cp 0
rs 9.0534
cc 4
eloc 12
nc 6
nop 2
crap 20
1
<?php
2
/**
3
 * Created by Vitaly Iegorov <[email protected]>.
4
 * on 26.01.16 at 15:11
5
 */
6
namespace samsonframework\di;
7
8
use samsonframework\di\exception\ClassNotFoundException;
9
use samsonframework\di\exception\ConstructorParameterNotSetException;
10
11
class Container extends AbstractContainer
12
{
13
    /** Variable name for storing services */
14
    const STATIC_COLLECTION_VARIABLE = '$services';
15
16
    /** @var array[string] Collection of loaded services */
17
    protected $services = array();
18
19
    /**
20
     * Get reflection parameter class name type hint if present without
21
     * auto loading and throwing exceptions.
22
     * TODO: Add resolving configured classes
23
     *
24
     * @param \ReflectionParameter $param Parameter for parsing
25
     *
26
     * @return string|null Class name typehint or null
27
     */
28
    protected function getClassName(\ReflectionParameter $param)
29
    {
30
        preg_match('/\[\s\<\w+?>\s(?<class>[\w\\\\]+)/', (string)$param, $matches);
31
        return array_key_exists('class', $matches) && $matches['class'] !== 'array'
32
            ? '\\' . ltrim($matches[1], '\\')
33
            : null;
34
    }
35
36
    /**
37
     * Analyze class construtor dependencies and create tree for nested dependencies.
38
     *
39
     * @param \ReflectionMethod $constructor Reflection method __constructor instance
40
     * @param string $className
41
     * @param array  $dependencies Reference to tree for filling up
42
     *
43
     * @throws ClassNotFoundException
44
     */
45
    protected function buildConstructorDependencies(\ReflectionMethod $constructor, $className, &$dependencies)
46
    {
47
        // Iterate all dependencies
48
        foreach ($constructor->getParameters() as $parameter) {
49
            // Ignore optional parameters
50
            if (!$parameter->isOptional()) {
51
                // Read dependency class name
52
                $dependencyClass = $this->getClassName($parameter);
53
54
                // Set pointer to parameter as it can be set before
55
                $parameterPointer = &$dependencies[$className][$parameter->name];
56
57
                // If we have found dependency class
58
                if ($dependencyClass !== null) {
59
                    // Point dependency class name
60
                    $parameterPointer = $dependencyClass;
61
                    // Go deeper in recursion and pass new branch there
62
                    $this->buildDependenciesTree($dependencyClass, $dependencies);
63
                }
64
            } else { // Stop iterating as first optional parameter is met
65
                break;
66
            }
67
        }
68
    }
69
70
    /**
71
     * Recursively build class constructor dependencies tree.
72
     * TODO: Analyze recurrent dependencies and throw an exception
73
     *
74
     * @param string $className    Current class name for analyzing
75
     * @param array  $dependencies Reference to tree for filling up
76
     *
77
     * @return array [string] Multidimensional array as dependency tree
78
     * @throws ClassNotFoundException
79
     */
80
    protected function buildDependenciesTree($className, array &$dependencies)
81
    {
82
        // Resolve class name if present
83
        $className = array_key_exists($className, $this->resolver)
84
            ? $this->resolver[$className]
85
            : $className;
86
87
        // We need this class to exists to use reflections, it will try to autoload it also
88
        if (class_exists($className)) {
89
            $class = new \ReflectionClass($className);
90
            // We can build dependency tree only from constructor dependencies
91
            $constructor = $class->getConstructor();
92
            if (null !== $constructor) {
93
                $this->buildConstructorDependencies($constructor, $className, $dependencies);
94
            }
95
        } else { // Something went wrong and class is not auto loaded and missing
96
            throw new ClassNotFoundException($className);
97
        }
98
99
        return $dependencies;
100
    }
101
102
    /**
103
     * Set service dependency. Upon first creation of this class instance
104
     * it would be used everywhere where this dependency is needed.
105
     *
106
     * @param string $className  Fully qualified class name
107
     * @param string $alias      Dependency name
108
     * @param array  $parameters Collection of parameters needed for dependency creation
109
     *
110
     * @return Container Chaining
111
     */
112
    public function service($className, $alias = null, array $parameters = array())
113
    {
114
        $this->services[$className] = $className;
115
116
        return $this->set($className, $alias, $parameters);
117
    }
118
119
    /**
120
     * Set dependency.
121
     *
122
     * @param string $className  Fully qualified class name
123
     * @param string $alias      Dependency name
124
     * @param array  $parameters Collection of parameters needed for dependency creation
125
     *
126
     * @return Container Chaining
127
     */
128
    public function set($className, $alias = null, array $parameters = array())
129
    {
130
        // Add this class dependencies to dependency tree
131
        $this->dependencies = array_merge(
132
            $this->dependencies,
133
            $this->buildDependenciesTree($className, $this->dependencies)
134
        );
135
136
        // Merge other class constructor parameters
137
        $this->dependencies[$className] = array_merge($this->dependencies[$className], $parameters);
138
139
        // Store alias for this class name
140
        $this->aliases[$className] = $alias;
141
    }
142
143
    /**
144
     * Generate initial class instance declaration
145
     * @param string $className Entity class name
146
     */
147
    protected function generateInitialDeclaration($className)
148
    {
149
        if (array_key_exists($className, $this->services)) {
150
            // Start service search or creation
151
            $this->generator
152
                ->text('isset('.self::STATIC_COLLECTION_VARIABLE.'[\''.$className.'\'])')
153
                ->newLine('? '.self::STATIC_COLLECTION_VARIABLE.'[\''.$className.'\']')
154
                ->newLine(': '.self::STATIC_COLLECTION_VARIABLE.'[\''.$className.'\'] = new '.$className.'(')->tabs++;
155
        } else {
156
            // Start object instance creation
157
            $this->generator->text('new ' . $className . '(')->tabs++;
158
        }
159
    }
160
161
    /**
162
     * Parse class dependencies generated by dependency tree.
163
     *
164
     * @param mixed $dependency Dependency value
165
     * @param string $variable Dependency name
166
     * @param string $className
167
     * @param int $level Current recursion level
168
     *
169
     * @throws ConstructorParameterNotSetException
170
     */
171
    protected function parseClassDependencies($dependency, $variable, $className, $level)
172
    {
173
        // If dependency value is a string
174
        if (is_string($dependency)) {
175
            // Define if we have this dependency described in dependency tree
176
            $dependencyPointer = &$this->dependencies[$dependency];
177
            if (null !== $dependencyPointer) {
178
                // We have dependencies tree for this entity
179
                $this->generateCondition($dependency, $dependencyPointer, $level + 1);
180
            } elseif (class_exists($dependency, false)) {
181
                // There are no dependencies for this entity
182
                $this->generator->newLine('new ' . $dependency . '()');
183
            } else { // String variable
184
                $this->generator->newLine()->stringValue($dependency);
185
            }
186
        } elseif (is_array($dependency)) { // Dependency value is array
187
            $this->generator->newLine()->arrayValue($dependency);
188
        } elseif ($dependency === null) { // Parameter is not set
189
            throw new ConstructorParameterNotSetException($className . '::' . $variable);
190
        }
191
    }
192
193
    /**
194
     * Generate container dependency condition code.
195
     *
196
     * @param string $className
197
     * @param mixed  $dependencies
198
     * @param int    $level
199
     *
200
     * @throws ConstructorParameterNotSetException
201
     *
202
     */
203
    protected function generateCondition($className, &$dependencies, $level = 0)
204
    {
205
        $this->generator->newLine(($level === 0) ? 'return ' : '');
206
207
        $this->generateInitialDeclaration($className);
208
209
        // Get last dependency variable name
210
        end($dependencies);
211
        $last = key($dependencies);
212
213
        // Iterate all dependencies for this class
214
        foreach ($dependencies as $variable => $dependency) {
215
            $this->parseClassDependencies($dependency, $variable, $className, $level);
216
217
            // Add comma if this is not last dependency
218
            if ($variable !== $last) {
219
                $this->generator->text(',');
220
            }
221
        }
222
        $this->generator->tabs--;
223
        $this->generator->newLine(')' . ($level === 0 ? ';' : ''));
224
    }
225
}
226