Completed
Push — master ( 8019db...83d806 )
by Vitaly
02:13
created

Generator   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 338
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 98.68%

Importance

Changes 28
Bugs 4 Features 17
Metric Value
wmc 37
c 28
b 4
f 17
lcom 1
cbo 3
dl 0
loc 338
ccs 150
cts 152
cp 0.9868
rs 8.6

9 Methods

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