Generator   B
last analyzed

Complexity

Total Complexity 37

Size/Duplication

Total Lines 346
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 98.75%

Importance

Changes 29
Bugs 4 Features 17
Metric Value
wmc 37
c 29
b 4
f 17
lcom 1
cbo 3
dl 0
loc 346
ccs 158
cts 160
cp 0.9875
rs 8.6

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 1
B scan() 0 19 5
A generate() 0 13 3
C analyze() 0 57 12
A changeName() 0 15 1
B generateClassName() 0 33 3
C generateViewClass() 0 70 8
A generateViewVariableSetter() 0 18 2
A hash() 0 9 2
1
<?php
2
/**
3
 * Created by Vitaly Iegorov <[email protected]>.
4
 * on 18.02.16 at 14:17
5
 */
6
namespace samsonframework\view;
7
8
use samsonframework\view\exception\GeneratedViewPathHasReservedWord;
9
10
/**
11
 * Views generator, this class scans resource for view files and creates
12
 * appropriate View class ancestors with namespace as relative view location
13
 * and file name as View class name ending with "View".
14
 *
15
 * Generator also analyzes view files content and creates protected class field
16
 * members for every variable used inside with chainable setter for this field,
17
 * to help IDE and developer in creating awesome code.
18
 *
19
 * TODO: Check for reserved keywords(like list) in namespaces
20
 * TODO: Somehow know view variable type(typehint??) and add comments and type-hints to generated classes.
21
 * TODO: Clever analysis for foreach, if, and so on language structures, we do not need to create variables for loop iterator.
22
 * TODO: If a variable is used in foreach - this is an array or Iteratable ancestor - we can add typehint automatically
23
 * TODO: Analyze view file php doc comments to get variable types
24
 * TODO: If a token variable is not $this and has "->" - this is object, maybe type-hint needs to be added.
25
 * TODO: Add caching logic to avoid duplicate file reading
26
 * TODO: Do not generate class fields with empty values
27
 * TODO: Generate constants with field names
28
 *
29
 * @package samsonframework\view
30
 */
31
class Generator
32
{
33
    /** string All generated view classes will end with this suffix */
34
    const VIEW_CLASSNAME_SUFFIX = 'View';
35
36
    /** @var array Collection of PHP reserved words */
37
    protected static $reservedWords = array('list');
38
39
    /** @var Metadata[] Collection of view metadata */
40
    public $metadata = array();
41
42
    /** @var \samsonphp\generator\Generator */
43
    protected $generator;
44
45
    /** @var string Generated classes namespace prefix */
46
    protected $namespacePrefix;
47
48
    /** @var string Collection of namespace parts to be ignored in generated namespaces */
49
    protected $ignoreNamespace = array();
50
51
    /** @var array Collection of view files */
52
    protected $files = array();
53
54
    /** @var string Scanning entry path */
55
    protected $entryPath;
56
57
    /** @var string Parent view class name */
58
    protected $parentViewClass;
59
60
    /**
61
     * Generator constructor.
62
     *
63
     * @param \samsonphp\generator\Generator $generator PHP code generator instance
64
     * @param string $namespacePrefix Generated classes namespace will have it
65
     * @param array $ignoreNamespace Namespace parts that needs to ignored
66
     * @param string $parentViewClass Generated classes will extend it
67
     */
68 4
    public function __construct(
69
        \samsonphp\generator\Generator $generator,
70
        $namespacePrefix,
71
        array $ignoreNamespace = array(),
72
        $parentViewClass = \samsonframework\view\View::class
73
    ) {
74 4
        $this->generator = $generator;
75 4
        $this->parentViewClass = $parentViewClass;
76 4
        $this->ignoreNamespace = $ignoreNamespace;
0 ignored issues
show
Documentation Bug introduced by
It seems like $ignoreNamespace of type array is incompatible with the declared type string of property $ignoreNamespace.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
77 4
        $this->namespacePrefix = rtrim(ltrim($namespacePrefix, '\\'), '\\') . '\\';
78 4
    }
79
80
    /**
81
     * Recursively scan path for files with specified extensions.
82
     *
83
     * @param string $source Entry point path
84
     * @param string $path Entry path for scanning
85
     * @param array $extensions Collection of file extensions without dot
86
     */
87 4
    public function scan($source, array $extensions = array(View::DEFAULT_EXT), $path = null)
88
    {
89 4
        $this->entryPath = $source;
90
91 4
        $path = isset($path) ? $path : $source;
92
93
        // Recursively go deeper into inner folders for scanning
94 4
        $folders = glob($path . '/*', GLOB_ONLYDIR);
95 4
        foreach ($folders as $folder) {
96 3
            $this->scan($source, $extensions, $folder);
97 4
        }
98
99
        // Iterate file extensions
100 4
        foreach ($extensions as $extension) {
101 4
            foreach (glob(rtrim($path, '/') . '/*.' . $extension) as $file) {
102 4
                $this->files[str_replace($source, '', $file)] = $file;
103 4
            }
104 4
        }
105 4
    }
106
107
    /**
108
     * Generate view classes.
109
     *
110
     * @param string $path Entry path for generated classes and folders
111
     * @param null|callable $viewHandler View code handler
112
     *
113
     * @throws GeneratedViewPathHasReservedWord
114
     */
115 3
    public function generate($path = __DIR__, $viewHandler = null)
116
    {
117 3
        foreach ($this->files as $relativePath => $absolutePath) {
118 3
            $this->metadata[$absolutePath] = $this->analyze($absolutePath);
119 3
            $this->metadata[$absolutePath]->path = $absolutePath;
120 3
            list($this->metadata[$absolutePath]->className,
121 3
                $this->metadata[$absolutePath]->namespace) = $this->generateClassName($absolutePath, $this->entryPath);
122 3
        }
123
124 2
        foreach ($this->metadata as $metadata) {
125 2
            $this->generateViewClass($metadata, $path, $viewHandler);
126 2
        }
127 2
    }
128
129
    /**
130
     * Analyze view file and create its metadata.
131
     *
132
     * @param string $file Path to view file
133
     *
134
     * @return Metadata View file metadata
135
     */
136 3
    public function analyze($file)
137
    {
138 3
        $metadata = new Metadata();
139 3
        $fileText = file_get_contents($file);
140
        // Use PHP tokenizer to find variables
141 3
        foreach ($tokens = token_get_all($fileText) as $idx => $token) {
142 3
            if (!is_string($token) && $token[0] === T_VARIABLE) {
143
                // Store variable
144 3
                $variableText = $token[1];
145
                // Store variable name
146 3
                $variableName = ltrim($token[1], '$');
147
148
                // Ignore static variables
149 3
                if (isset($tokens[$idx - 1]) && $tokens[$idx - 1][0] === T_DOUBLE_COLON) {
150 1
                    $metadata->static[$variableName] = $variableText;
151 1
                    continue;
152
                }
153
154
                // If next token is object operator
155 3
                if ($tokens[$idx + 1][0] === T_OBJECT_OPERATOR) {
156
                    // Ignore $this
157 3
                    if ($variableName === 'this') {
158 3
                        continue;
159
                    }
160
161
                    // And two more tokens
162 1
                    $variableText .= $tokens[$idx + 1][1] . $tokens[$idx + 2][1];
163
164
                    // Store object variable
165 1
                    $metadata->variables[$this->changeName($variableName)] = $variableText;
166
                    // Store view variable key - actual object name => full variable usage
167 1
                    $metadata->originalVariables[$this->changeName($variableName)] = $variableName;
168 1
                } else {
169
                    // Store original variable name
170 3
                    $metadata->originalVariables[$this->changeName($variableName)] = $variableName;
171
                    // Store view variable key - actual object name => full variable usage
172 3
                    $metadata->variables[$this->changeName($variableName)] = $variableText;
173
                }
174 3
            } elseif ($token[0] === T_DOC_COMMENT) { // Match doc block comments
175
                // Parse variable type and name
176 3
                if (preg_match('/@var\s+(?<type>[^ ]+)\s+(?<variable>[^*]+)/', $token[1], $matches)) {
177 3
                    $metadata->types[substr(trim($matches['variable']), 1)] = $matches['type'];
178 3
                }
179 3
            }
180 3
        }
181 3
        if (preg_match_all('/\$this->block\(\'(?<block>[^ ]+)\'/', $fileText, $matches)) {
182 2
            $metadata->blocks = $matches['block'];
183 2
        }
184
185 3
        if (preg_match('/\$this->extend\((?<class>[^ ]+\:\:class)\s*\,\s*\'(?<block>[^ ]+)\'\s*\)/', $fileText,
186 3
            $matches)) {
187 2
            $metadata->parentClass = $matches['class'];
188 2
            $metadata->parentBlock = $matches['block'];
189 2
        }
190
191 3
        return $metadata;
192
    }
193
194
    /**
195
     * Change variable name to camel caps format.
196
     *
197
     * @param string $variable
198
     *
199
     * @return string Changed variable name
200
     */
201 3
    public function changeName($variable)
202
    {
203 3
        return lcfirst(
204 3
            implode(
205 3
                '',
206 3
                array_map(
207 3
                    function ($element) {
208 3
                        return ucfirst($element);
209 3
                    },
210 3
                    explode('_', str_replace('-', '_', $variable))
211 3
                )
212 3
            )
213
0 ignored issues
show
Coding Style introduced by
Empty lines are not allowed in multi-line function calls
Loading history...
214 3
        );
215
    }
216
217
    /**
218
     * Generic class name and its name space generator.
219
     *
220
     * @param string $file Full path to view file
221
     * @param string $entryPath Entry path
222
     *
223
     * @return array Class name[0] and namespace[1]
224
     * @throws GeneratedViewPathHasReservedWord
225
     */
226 3
    protected function generateClassName($file, $entryPath)
227
    {
228
        // Get only file name as a class name with suffix
229 3
        $className = ucfirst($this->changeName(pathinfo($file, PATHINFO_FILENAME)). self::VIEW_CLASSNAME_SUFFIX);
230
231
        // Get namespace as part of file path relatively to entry path
232 3
        $nameSpace = strtolower(
233 3
            rtrim(
234 3
                ltrim(
235 3
                    str_replace(
236 3
                        '/',
237 3
                        '\\',
238 3
                        str_replace(array('-', '_'), '', str_replace($entryPath, '', pathinfo($file, PATHINFO_DIRNAME)))
239 3
                    ),
240
                    '\\'
241 3
                ),
242
                '\\'
243 3
            )
244 3
        );
245
246
        // Remove ignored parts from namespaces
247 3
        $nameSpace = str_replace($this->ignoreNamespace, '', $nameSpace);
248
249
        // Check generated namespaces
250 3
        foreach (static::$reservedWords as $reservedWord) {
251 3
            if (strpos($nameSpace, '\\' . $reservedWord) !== false) {
252 1
                throw new GeneratedViewPathHasReservedWord($file . '(' . $reservedWord . ')');
253
            }
254 3
        }
255
256
        // Return collection for further usage
257 3
        return array($className, rtrim($this->namespacePrefix . $nameSpace, '\\'));
258
    }
259
260
    /**
261
     * Create View class ancestor.
262
     *
263
     * @param Metadata $metadata View file metadata
264
     * @param string $path Entry path for generated classes and folders
265
     * @param null|callable $viewHandler View code handler
266
     */
267 2
    protected function generateViewClass(Metadata $metadata, $path, $viewHandler = null)
268
    {
269 2
        $metadataParentClass = eval('return ' . $metadata->parentClass . ';');
270
271
        // Read view file
272 2
        $viewCode = trim(file_get_contents($metadata->path));
273
274
        // If we have external handler - pass view code to it for conversion
275 2
        if (is_callable($viewHandler)) {
276
            $viewCode = call_user_func($viewHandler, $viewCode);
277
        }
278
279
        // Convert to string for defining
280 2
        $viewCode = '<<<\'EOT\'' . "\n" . $viewCode . "\n" . 'EOT';
281
282 2
        $parentClass = !isset($metadata->parentClass) ? $this->parentViewClass : $metadataParentClass;
283 2
        $this->generator
284 2
            ->defNamespace($metadata->namespace)
285 2
            ->multiComment(array('Class for view "' . $metadata->path . '" rendering'))
286 2
            ->defClass($metadata->className, '\\' . $parentClass)
287 2
            ->commentVar('string', 'Path to view file')
288 2
            ->defClassVar('$file', 'protected', $metadata->path)
289 2
            ->commentVar('string', 'Parent block name')
290 2
            ->defClassVar('$parentBlock', 'protected', $metadata->parentBlock)
291 2
            ->commentVar('array', 'Blocks list')
292 2
            ->defClassVar('$blocks', 'protected', $metadata->blocks)
293 2
            ->commentVar('string', 'View source code')
294 2
            ->defClassVar('$source', 'protected', $viewCode);
295
        //->commentVar('array', 'Collection of view variables')
296
        //->defClassVar('$variables', 'public static', array_keys($metadata->variables))
297
        //->commentVar('array', 'Collection of view variable types')
298
        //->defClassVar('$types', 'public static', $metadata->types)
299 2
        ;
300
301
        // Iterate all view variables
302 2
        foreach (array_keys($metadata->variables) as $name) {
303 2
            $type = array_key_exists($name, $metadata->types) ? $metadata->types[$name] : 'mixed';
304 2
            $static = array_key_exists($name, $metadata->static) ? ' static' : '';
305 2
            $this->generator
306 2
                ->commentVar($type, 'View variable')
307 2
                ->defClassVar('$' . $name, 'public' . $static);
308
309
            // Do not generate setters for static variables
310 2
            if ($static !== ' static') {
311 2
                $this->generator->text($this->generateViewVariableSetter(
312 2
                    $name,
313 2
                    $metadata->originalVariables[$name],
314
                    $type
315 2
                ));
316 2
            }
317 2
        }
318
319
        // Iterate namespace and create folder structure
320 2
        $path .= '/' . str_replace('\\', '/', $metadata->namespace);
321 2
        if (!is_dir($path)) {
322 1
            mkdir($path, 0777, true);
323 1
        }
324
325 2
        $newClassFile = $path . '/' . $metadata->className . '.php';
326 2
        file_put_contents(
327 2
            $newClassFile,
328 2
            '<?php' . $this->generator->endClass()->flush()
329 2
        );
330
331
        // Store path to generated class
332 2
        $metadata->generatedPath = $newClassFile;
333
334
        // Make generated cache files accessible
335 2
        chmod($newClassFile, 0777);
336 2
    }
337
338
    /**
339
     * Generate constructor for application class.
340
     *
341
     * @param string $variable View variable name
342
     * @param string $original Original view variable name
343
     * @param string $type Variable type
344
     *
345
     * @return string View variable setter method
346
     */
347 2
    protected function generateViewVariableSetter($variable, $original, $type = 'mixed')
348
    {
349
        // Define type hint
350 2
        $typeHint = strpos($type, '\\') !== false ? $type . ' ' : '';
351
352 2
        $class = "\n\t" . '/**';
353 2
        $class .= "\n\t" . ' * Setter for ' . $variable . ' view variable';
354 2
        $class .= "\n\t" . ' *';
355 2
        $class .= "\n\t" . ' * @param ' . $type . ' $value View variable value';
356 2
        $class .= "\n\t" . ' * @return $this Chaining';
357 2
        $class .= "\n\t" . ' */';
358 2
        $class .= "\n\t" . 'public function ' . $variable . '(' . $typeHint . '$value)';
359 2
        $class .= "\n\t" . '{';
360 2
        $class .= "\n\t\t" . 'return parent::set($value, \'' . $original . '\');';
361 2
        $class .= "\n\t" . '}' . "\n";
362
363 2
        return $class;
364
    }
365
366
    /** @return string Hash representing generator state */
367 1
    public function hash()
368
    {
369 1
        $hash = '';
370 1
        foreach ($this->files as $relativePath => $absolutePath) {
371 1
            $hash .= md5($relativePath . filemtime($absolutePath));
372 1
        }
373
374 1
        return md5($hash);
375
    }
376
}
377