Completed
Push — master ( 92204b...b09486 )
by Vitaly
04:29
created

MetadataBuilder::generateConditions()   B

Complexity

Conditions 6
Paths 5

Size

Total Lines 46
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 46
ccs 20
cts 20
cp 1
rs 8.4751
c 0
b 0
f 0
cc 6
eloc 19
nc 5
nop 2
crap 6
1
<?php declare(strict_types = 1);
2
/**
3
 * Created by PhpStorm.
4
 * User: root
5
 * Date: 02.08.16
6
 * Time: 0:46.
7
 */
8
namespace samsonframework\container;
9
10
use samsonframework\container\metadata\ClassMetadata;
11
use samsonframework\container\resolver\ResolverInterface;
12
use samsonframework\di\Container;
13
use samsonframework\filemanager\FileManagerInterface;
14
use samsonphp\generator\Generator;
15
16
/**
17
 * Class Container.
18
 */
19
class MetadataBuilder
20
{
21
    /** Controller classes scope name */
22
    const SCOPE_CONTROLLER = 'controllers';
23
24
    /** Service classes scope name */
25
    const SCOPE_SERVICES = 'services';
26
27
    /** Generated resolving function name prefix */
28
    const DI_FUNCTION_PREFIX = 'container';
29
30
    /** Generated resolving function service static collection name */
31
    const DI_FUNCTION_SERVICES = '$' . self::SCOPE_SERVICES;
32
33
    /** @var string[] Collection of available container scopes */
34
    protected $scopes = [
35
        self::SCOPE_CONTROLLER => [],
36
        self::SCOPE_SERVICES => []
37
    ];
38
39
    /** @var ClassMetadata[] Collection of classes metadata */
40
    protected $classMetadata = [];
41
42
    /** @var FileManagerInterface */
43
    protected $fileManger;
44
45
    /** @var ResolverInterface */
46
    protected $classResolver;
47
48
    /** @var Generator */
49
    protected $generator;
50
51
    /** @var string Resolver function name */
52
    protected $resolverFunction;
53
54
    /**
55
     * Container constructor.
56
     *
57
     * @param FileManagerInterface $fileManger
58
     * @param ResolverInterface    $classResolver
59
     * @param Generator            $generator
60
     */
61 4
    public function __construct(
62
        FileManagerInterface $fileManger,
63
        ResolverInterface $classResolver,
64
        Generator $generator
65
    )
66
    {
0 ignored issues
show
Coding Style introduced by
The closing parenthesis and the opening brace of a multi-line function declaration must be on the same line
Loading history...
67 4
        $this->fileManger = $fileManger;
68 4
        $this->classResolver = $classResolver;
69 4
        $this->generator = $generator;
70 4
    }
71
72
    /**
73
     * Load classes from paths.
74
     *
75
     * @param array $paths Paths for importing
76
     *
77
     * @return $this
78
     */
79 1
    public function loadFromPaths(array $paths)
80
    {
81
        // Iterate all paths and get files
82 1
        foreach ($this->fileManger->scan($paths, ['php']) as $phpFile) {
83
            // Read all classes in given file
84 1
            $this->loadFromClassNames($this->getDefinedClasses(require_once($phpFile)));
85
        }
86
87 1
        return $this;
88
    }
89
90
    /**
91
     * Load classes from class names collection.
92
     *
93
     * @param string[] $classes Collection of class names for resolving
94
     *
95
     * @return $this
96
     */
97 4
    public function loadFromClassNames(array $classes)
98
    {
99
        // Read all classes in given file
100 4
        foreach ($classes as $className) {
101
            // Resolve class metadata
102 3
            $this->classMetadata[$className] = $this->classResolver->resolve(new \ReflectionClass($className));
103
            // Store class in defined scopes
104 3
            foreach ($this->classMetadata[$className]->scopes as $scope) {
105 3
                $this->scopes[$scope][] = $className;
106
            }
107
        }
108
109 4
        return $this;
110
    }
111
112
    /**
113
     * Find class names defined in PHP code.
114
     *
115
     * @param string $php PHP code for scanning
116
     *
117
     * @return string[] Collection of found class names in php code
118
     */
119 2
    protected function getDefinedClasses($php) : array
120
    {
121 2
        $classes = array();
122
123
        // Append php marker for parsing file
124 2
        $php = strpos(is_string($php) ? $php : '', '<?php') !== 0 ? '<?php ' . $php : $php;
125
126 2
        $tokens = token_get_all($php);
127
128 2
        for ($i = 2, $count = count($tokens); $i < $count; $i++) {
129 1
            if ($tokens[$i - 2][0] === T_CLASS
130 1
                && $tokens[$i - 1][0] === T_WHITESPACE
131 1
                && $tokens[$i][0] === T_STRING
132
            ) {
133 1
                $classes[] = $tokens[$i][1];
134
            }
135
        }
136
137 2
        return $classes;
138
    }
139
140
    /**
141
     * Load classes from PHP code.
142
     *
143
     * @param string $php PHP code
144
     *
145
     * @return $this
146
     */
147 1
    public function loadFromCode($php)
148
    {
149 1
        if (count($classes = $this->getDefinedClasses($php))) {
150
            // TODO: Consider writing cache file and require it
151 1
            eval($php);
152 1
            $this->loadFromClassNames($classes);
153
        }
154
155 1
        return $this;
156
    }
157
158
    /**
159
     * Build container class.
160
     *
161
     * @param string|null $containerClass Container class name
162
     * @param string      $namespace      Name space
163
     *
164
     * @return string Generated Container class code
165
     * @throws \InvalidArgumentException
166
     */
167 1
    public function build($containerClass = 'Container', $namespace = '')
168
    {
169
        // Build dependency injection container function name
170 1
        $this->resolverFunction = uniqid(self::DI_FUNCTION_PREFIX);
171
172 1
        $this->generator
173 1
            ->text('<?php declare(strict_types = 1);')
174 1
            ->newLine()
175 1
            ->defNamespace($namespace)
176 1
            ->multiComment(['Application container'])
177 1
            ->defClass($containerClass, '\\' . Container::class)
178 1
            ->commentVar('array', 'Loaded dependencies')
179 1
            ->defClassVar('$dependencies', 'protected', array_keys($this->classMetadata))
180 1
            ->commentVar('array', 'Loaded services')
181 1
            ->defClassVar('$' . self::SCOPE_SERVICES, 'protected', $this->scopes[self::SCOPE_SERVICES]);
182
183 1
        foreach ($this->classMetadata as $className => $classMetadata) {
184 1
            $dependencyName = $className;
185
186
            // Generate camel case getter method
187 1
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
188
189 1
            $this->generator
190 1
                ->defClassFunction($camelMethodName, 'public', [], ['@return \\' . $dependencyName . ' Get ' . $dependencyName . ' instance'])
191 1
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
192 1
                ->endClassFunction();
193
        }
194
195
        // Build di container function and add to container class and return class code
196 1
        $this->buildDependencyResolver($this->resolverFunction);
197
198 1
        return $this->generator
199 1
            ->endClass()
200 1
            ->flush();
201
    }
202
203
    /**
204
     * Build dependency resolving function.
205
     *
206
     * @param string $functionName Function name
207
     *
208
     * @throws \InvalidArgumentException
209
     */
210 1
    protected function buildDependencyResolver($functionName)
211
    {
212 1
        $inputVariable = '$aliasOrClassName';
213 1
        $this->generator
214 1
            ->defClassFunction($functionName, 'protected', [$inputVariable], ['Dependency resolving function'])
215 1
            ->defVar('static $services')
216 1
            ->newLine();
217
218
        // Generate all container and delegate conditions
219 1
        $this->generateConditions($inputVariable, false);
220
221
        // Add method not found
222 1
        $this->generator->endIfCondition()->endFunction();
223 1
    }
224
225
    /**
226
     * Generate logic conditions and their implementation for container and its delegates.
227
     *
228
     * @param string     $inputVariable Input condition parameter variable name
229
     * @param bool|false $started       Flag if condition branching has been started
230
     */
231 1
    public function generateConditions($inputVariable = '$alias', $started = false)
232
    {
233
        // Iterate all container dependencies
234 1
        foreach ($this->classMetadata as $className => $classMetadata) {
235
            // Generate condition statement to define if this class is needed
236 1
            $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition';
237
238
            // Output condition branch
239 1
            $this->generator->$conditionFunc(
240 1
                $this->buildResolverCondition($inputVariable, $className, $classMetadata->name)
241
            );
242
243 1
            $this->generator->newLine('return ');
244
245 1
            $this->buildResolvingDeclaration($className, $classMetadata->name);
246
247
            // Process constructor dependencies
248 1
            if (array_key_exists('__construct', $classMetadata->methodsMetadata)) {
249 1
                $constructorArguments = $classMetadata->methodsMetadata['__construct']->dependencies;
250 1
                $argumentsCount = count($constructorArguments);
251 1
                $i = 0;
252
253
                // Add indentation to move declaration arguments
254 1
                $this->generator->tabs++;
255
256
                // Process constructor arguments
257 1
                foreach ($constructorArguments as $argument => $dependency) {
258 1
                    $this->buildResolverArgument($dependency);
259
260
                    // Add comma if this is not last dependency
261 1
                    if (++$i < $argumentsCount) {
262 1
                        $this->generator->text(',');
263
                    }
264
                }
265
266
                // Restore indentation
267 1
                $this->generator->tabs--;
268
            }
269
270
            // Close declaration block
271 1
            $this->generator->newLine(');');
272
273
            // Set flag that condition is started
274 1
            $started = true;
275
        }
276 1
    }
277
278
    /**
279
     * Build resolving function condition.
280
     *
281
     * @param string      $inputVariable Condition variable
282
     * @param string      $className
283
     * @param string|null $alias
284
     *
285
     * @return string Condition code
286
     */
287 1
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
288
    {
289
        // Create condition branch
290 1
        $condition = $inputVariable . ' === \'' . $className . '\'';
291
292 1
        if ($alias !== null && $alias !== $className) {
293 1
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
294
        }
295
296 1
        return $condition;
297
    }
298
299
    /**
300
     * Build resolving function declaration block.
301
     *
302
     * @param string $className Service class name for new instance creation
303
     * @param string $alias     Service alias for static storage and retrieval
304
     */
305 1
    protected function buildResolvingDeclaration(string $className, string $alias = null)
306
    {
307 1
        if (in_array($className, $this->scopes[self::SCOPE_SERVICES], true)) {
308 1
            $this->buildResolvingServiceDeclaration($className, $alias);
309
        } else {
310 1
            $this->buildResolvingClassDeclaration($className);
311
        }
312 1
    }
313
314
    /**
315
     * Build resolving function service block.
316
     *
317
     * @param string $alias     Service alias for static storage and retrieval
318
     * @param string $className Service class name for new instance creation
319
     */
320 1
    protected function buildResolvingServiceDeclaration(string $className, string $alias = null)
321
    {
322
        // Use class name if alias is not passed
323 1
        $alias = $alias ?? $className;
324
325
        // Start service search or creation
326 1
        $this->generator
327 1
            ->text('array_key_exists(\'' . $alias . '\', ' . self::DI_FUNCTION_SERVICES . ')')
328 1
            ->newLine('? ' . self::DI_FUNCTION_SERVICES . '[\'' . $alias . '\']')
329 1
            ->newLine(': ' . self::DI_FUNCTION_SERVICES . '[\'' . $alias . '\'] = ');
330
331
        // Regular class creation
332 1
        $this->buildResolvingClassDeclaration($className);
333 1
    }
334
335
    /**
336
     * Build resolving function class block.
337
     *
338
     * @param string $className Class name for new instance creation
339
     */
340 1
    protected function buildResolvingClassDeclaration(string $className)
341
    {
342 1
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
343 1
    }
344
345
    /**
346
     * Build resolving function dependency argument.
347
     *
348
     * @param mixed $argument Dependency argument
349
     */
350 1
    protected function buildResolverArgument($argument)
351
    {
352
        // This is a dependency which invokes resolving function
353 1
        if (array_key_exists($argument, $this->classMetadata)) {
354
            // Call container logic for this dependency
355 1
            $this->generator->newLine('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
356
        } elseif (is_string($argument)) { // String variable
357
            $this->generator->newLine()->stringValue($argument);
358
        } elseif (is_array($argument)) { // Dependency value is array
359
            $this->generator->newLine()->arrayValue($argument);
360
        }
361 1
    }
362
}
363