Completed
Push — master ( ff50fc...e48f9b )
by Vitaly
02:52
created

MetadataBuilder::buildResolvingDeclaration()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
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 2
            foreach ($this->classMetadata[$className]->scopes as $scope) {
105 2
                $this->scopes[$scope][] = $className;
106
            }
107
        }
108
109 2
        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
    public function build($containerClass = 'Container', $namespace = '')
168
    {
169
        // Build dependency injection container function name
170
        $this->resolverFunction = uniqid(self::DI_FUNCTION_PREFIX);
171
172
        $this->generator
173
            ->text('<?php declare(strict_types = 1);')
174
            ->newLine()
175
            ->defNamespace($namespace)
176
            ->multiComment(['Application container'])
177
            ->defClass($containerClass, '\\' . Container::class)
178
            ->commentVar('array', 'Loaded dependencies')
179
            ->defClassVar('$dependencies', 'protected', array_keys($this->classMetadata))
180
            ->commentVar('array', 'Loaded services')
181
            ->defClassVar('$' . self::SCOPE_SERVICES, 'protected', $this->scopes[self::SCOPE_SERVICES])
182
            ->defClassFunction('logic', 'protected', ['$dependency'], ['Overridden dependency resolving function'])
183
            ->newLine('return $this->' . $this->resolverFunction . '($dependency);')
184
            ->endClassFunction();
185
186
        foreach ($this->classMetadata as $className => $classMetadata) {
187
            $dependencyName = $classMetadata->name ?? $className;
188
189
            // Generate camel case getter method
190
            $camelMethodName = 'get' . str_replace(' ', '', ucwords(ucfirst(str_replace(['\\', '_'], ' ', $dependencyName))));
191
192
            $this->generator
193
                ->defClassFunction($camelMethodName, 'public', [], ['@return \\' . $dependencyName . ' Get ' . $dependencyName . ' instance'])
194
                ->newLine('return $this->' . $this->resolverFunction . '(\'' . $dependencyName . '\');')
195
                ->endClassFunction();
196
        }
197
198
        // Build di container function and add to container class and return class code
199
        $this->buildDependencyResolver($this->resolverFunction);
200
201
        return $this->generator
202
            ->endClass()
203
            ->flush();
204
    }
205
206
    /**
207
     * Build dependency resolving function.
208
     *
209
     * @param string $functionName Function name
210
     *
211
     * @throws \InvalidArgumentException
212
     */
213
    protected function buildDependencyResolver($functionName)
214
    {
215
        $inputVariable = '$aliasOrClassName';
216
        $this->generator
217
            ->defClassFunction($functionName, 'protected', [$inputVariable], ['Dependency resolving function'])
218
            ->defVar('static $services')
219
            ->newLine();
220
221
        // Generate all container and delegate conditions
222
        $this->generateConditions($inputVariable, false);
223
224
        // Add method not found
225
        $this->generator->endIfCondition()->endFunction();
226
    }
227
228
    /**
229
     * Generate logic conditions and their implementation for container and its delegates.
230
     *
231
     * @param string     $inputVariable Input condition parameter variable name
232
     * @param bool|false $started       Flag if condition branching has been started
233
     */
234
    public function generateConditions($inputVariable = '$alias', $started = false)
235
    {
236
        // Iterate all container dependencies
237
        foreach ($this->classMetadata as $className => $classMetadata) {
238
            // Generate condition statement to define if this class is needed
239
            $conditionFunc = !$started ? 'defIfCondition' : 'defElseIfCondition';
240
241
            // Output condition branch
242
            $this->generator->$conditionFunc(
243
                $this->buildResolverCondition($inputVariable, $className, $classMetadata->name)
244
            );
245
246
            $this->generator->newLine('return ');
247
248
            $this->buildResolvingDeclaration($className, $classMetadata->name);
249
250
            // Process constructor dependencies
251
            if (array_key_exists('__construct', $classMetadata->methodsMetadata)) {
252
                $constructorArguments = $classMetadata->methodsMetadata['__construct']->dependencies;
253
                $argumentsCount = count($constructorArguments);
254
                $i = 0;
255
256
                // Add indentation to move declaration arguments
257
                $this->generator->tabs++;
258
259
                // Process constructor arguments
260
                foreach ($constructorArguments as $argument => $dependency) {
261
                    $this->buildResolverArgument($dependency);
262
263
                    // Add comma if this is not last dependency
264
                    if (++$i < $argumentsCount) {
265
                        $this->generator->text(',');
266
                    }
267
                }
268
269
                // Restore indentation
270
                $this->generator->tabs--;
271
            }
272
273
            // Close declaration block
274
            $this->generator->newLine(');');
275
276
            // Set flag that condition is started
277
            $started = true;
278
        }
279
    }
280
281
    /**
282
     * Build resolving function condition.
283
     *
284
     * @param string      $inputVariable Condition variable
285
     * @param string      $className
286
     * @param string|null $alias
287
     *
288
     * @return string Condition code
289
     */
290
    protected function buildResolverCondition(string $inputVariable, string $className, string $alias = null) : string
291
    {
292
        // Create condition branch
293
        $condition = $inputVariable . ' === \'' . $className . '\'';
294
295
        if ($alias !== null && $alias !== $className) {
296
            $condition .= '||' . $this->buildResolverCondition($inputVariable, $alias);
297
        }
298
299
        return $condition;
300
    }
301
302
    /**
303
     * Build resolving function declaration block.
304
     *
305
     * @param string $className Service class name for new instance creation
306
     * @param string $alias     Service alias for static storage and retrieval
307
     */
308
    protected function buildResolvingDeclaration(string $className, string $alias = null)
309
    {
310
        if (in_array($className, $this->scopes[self::SCOPE_SERVICES], true)) {
311
            $this->buildResolvingServiceDeclaration($className, $alias);
312
        } else {
313
            $this->buildResolvingClassDeclaration($className);
314
        }
315
    }
316
317
    /**
318
     * Build resolving function service block.
319
     *
320
     * @param string $alias     Service alias for static storage and retrieval
321
     * @param string $className Service class name for new instance creation
322
     */
323
    protected function buildResolvingServiceDeclaration(string $className, string $alias = null)
324
    {
325
        // Use class name if alias is not passed
326
        $alias = $alias ?? $className;
327
328
        // Start service search or creation
329
        $this->generator
330
            ->text('array_key_exists(\'' . $alias . '\', ' . self::DI_FUNCTION_SERVICES . ')')
331
            ->newLine('? ' . self::DI_FUNCTION_SERVICES . '[\'' . $alias . '\']')
332
            ->newLine(': ' . self::DI_FUNCTION_SERVICES . '[\'' . $alias . '\'] = ');
333
334
        // Regular class creation
335
        $this->buildResolvingClassDeclaration($className);
336
    }
337
338
    /**
339
     * Build resolving function class block.
340
     *
341
     * @param string $className Class name for new instance creation
342
     */
343
    protected function buildResolvingClassDeclaration(string $className)
344
    {
345
        $this->generator->text('new \\' . ltrim($className, '\\') . '(');
346
    }
347
348
    /**
349
     * Build resolving function dependency argument.
350
     *
351
     * @param mixed $argument Dependency argument
352
     */
353
    protected function buildResolverArgument($argument)
354
    {
355
        // This is a dependency which invokes resolving function
356
        if (array_key_exists($argument, $this->classMetadata)) {
357
            // Call container logic for this dependency
358
            $this->generator->newLine('$this->' . $this->resolverFunction . '(\'' . $argument . '\')');
359
        } elseif (is_string($argument)) { // String variable
360
            $this->generator->newLine()->stringValue($argument);
361
        } elseif (is_array($argument)) { // Dependency value is array
362
            $this->generator->newLine()->arrayValue($argument);
363
        }
364
    }
365
}
366