Passed
Push — master ( a657bb...758483 )
by Vitaly
06:27
created

Container::buildConstructorDependencies()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 24
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 24
ccs 13
cts 13
cp 1
rs 8.6845
cc 4
eloc 10
nc 4
nop 3
crap 4
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 6
    protected function getClassName(\ReflectionParameter $param)
29
    {
30 6
        preg_match('/\[\s\<\w+?>\s(?<class>[\w\\\\]+)/', (string)$param, $matches);
31 6
        return array_key_exists('class', $matches) && $matches['class'] !== 'array'
32 6
            ? '\\' . ltrim($matches[1], '\\')
33 6
            : 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 6
    protected function buildConstructorDependencies(\ReflectionMethod $constructor, $className, &$dependencies)
46
    {
47
        // Iterate all dependencies
48 6
        foreach ($constructor->getParameters() as $parameter) {
49
            // Ignore optional parameters
50 6
            if (!$parameter->isOptional()) {
51
                // Read dependency class name
52 6
                $dependencyClass = $this->getClassName($parameter);
53
54
                // Set pointer to parameter as it can be set before
55 6
                $parameterPointer = &$dependencies[$className][$parameter->getName()];
0 ignored issues
show
Bug introduced by
Consider using $parameter->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
56
57
                // If we have found dependency class
58 6
                if ($dependencyClass !== null) {
59
                    // Point dependency class name
60 6
                    $parameterPointer = $dependencyClass;
61
                    // Go deeper in recursion and pass new branch there
62 6
                    $this->buildDependenciesTree($dependencyClass, $dependencies);
63 6
                }
64 6
            } else { // Stop iterating as first optional parameter is met
65 6
                break;
66
            }
67 6
        }
68 6
    }
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 6
    protected function buildDependenciesTree($className, array &$dependencies)
81
    {
82
        // Resolve class name if present
83 6
        $className = array_key_exists($className, $this->resolver)
84 6
            ? $this->resolver[$className]
85 6
            : $className;
86
87
        // We need this class to exists to use reflections, it will try to autoload it also
88 6
        if (class_exists($className)) {
89 6
            $class = new \ReflectionClass($className);
90
            // We can build dependency tree only from constructor dependencies
91 6
            $constructor = $class->getConstructor();
92 6
            if (null !== $constructor) {
93 6
                $this->buildConstructorDependencies($constructor, $className, $dependencies);
94 6
            }
95 6
        } else { // Something went wrong and class is not auto loaded and missing
96 1
            throw new ClassNotFoundException($className);
97
        }
98
99 6
        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 6
    public function service($className, $alias = null, array $parameters = array())
113
    {
114 6
        $this->services[$className] = $className;
115
116 6
        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 6
    public function set($className, $alias = null, array $parameters = array())
129
    {
130
        // Add this class dependencies to dependency tree
131 6
        $this->dependencies = array_merge(
132 6
            $this->dependencies,
133 6
            $this->buildDependenciesTree($className, $this->dependencies)
134 6
        );
135
136
        // Merge other class constructor parameters
137 6
        $this->dependencies[$className] = array_merge($this->dependencies[$className], $parameters);
138
139
        // Store alias for this class name
140 6
        $this->aliases[$className] = $alias;
141 6
    }
142
143
    /**
144
     * Generate initial class instance declaration
145
     * @param string $className Entity class name
146
     */
147 2
    protected function generateInitialDeclaration($className)
148
    {
149 2
        if (array_key_exists($className, $this->services)) {
150
            // Start service search or creation
151 2
            $this->generator
152 2
                ->text('isset('.self::STATIC_COLLECTION_VARIABLE.'[\''.$className.'\'])')
153 2
                ->newLine('? '.self::STATIC_COLLECTION_VARIABLE.'[\''.$className.'\']')
154 2
                ->newLine(': '.self::STATIC_COLLECTION_VARIABLE.'[\''.$className.'\'] = new '.$className.'(')->tabs++;
155 2
        } else {
156
            // Start object instance creation
157 2
            $this->generator->text('new ' . $className . '(')->tabs++;
158
        }
159 2
    }
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 2
    protected function parseClassDependencies($dependency, $variable, $className, $level)
172
    {
173
        // If dependency value is a string
174 2
        if (is_string($dependency)) {
175
            // Define if we have this dependency described in dependency tree
176 2
            $dependencyPointer = &$this->dependencies[$dependency];
177 2
            if (null !== $dependencyPointer) {
178
                // We have dependencies tree for this entity
179 2
                $this->generateCondition($dependency, $dependencyPointer, $level + 1);
180 2
            } elseif (class_exists($dependency, false)) {
181
                // There are no dependencies for this entity
182 2
                $this->generator->newLine('new ' . $dependency . '()');
183 2
            } else { // String variable
184 2
                $this->generator->newLine()->stringValue($dependency);
185
            }
186 2
        } elseif (is_array($dependency)) { // Dependency value is array
187 2
            $this->generator->newLine()->arrayValue($dependency);
188 2
        } elseif ($dependency === null) { // Parameter is not set
189 1
            throw new ConstructorParameterNotSetException($className . '::' . $variable);
190
        }
191 2
    }
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 2
    protected function generateCondition($className, &$dependencies, $level = 0)
204
    {
205 2
        $this->generator->newLine(($level === 0) ? 'return ' : '');
206
207 2
        $this->generateInitialDeclaration($className);
208
209
        // Get last dependency variable name
210 2
        end($dependencies);
211 2
        $last = key($dependencies);
212
213
        // Iterate all dependencies for this class
214 2
        foreach ($dependencies as $variable => $dependency) {
215 2
            $this->parseClassDependencies($dependency, $variable, $className, $level);
216
217
            // Add comma if this is not last dependency
218 2
            if ($variable !== $last) {
219 2
                $this->generator->text(',');
220 2
            }
221 2
        }
222 2
        $this->generator->tabs--;
223 2
        $this->generator->newLine(')' . ($level === 0 ? ';' : ''));
224 2
    }
225
}
226