Completed
Push — master ( 302a83...8019db )
by Vitaly
02:14
created

Generator   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 340
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 98.7%

Importance

Changes 27
Bugs 4 Features 16
Metric Value
wmc 37
c 27
b 4
f 16
lcom 1
cbo 3
dl 0
loc 340
ccs 152
cts 154
cp 0.987
rs 8.6

9 Methods

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