Completed
Push — master ( 27437d...683447 )
by Nikita
04:20
created

Generator   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 26
Bugs 3 Features 16
Metric Value
wmc 36
c 26
b 3
f 16
lcom 1
cbo 3
dl 0
loc 321
ccs 147
cts 147
cp 1
rs 8.8

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 12 1
A changeName() 0 12 1
B scan() 0 19 5
C analyze() 0 56 12
B generateClassName() 0 28 3
A generate() 0 14 3
A hash() 0 9 2
B generateViewClass() 0 56 7
A generateViewVariableSetter() 0 18 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
    protected $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
    )
0 ignored issues
show
Coding Style introduced by
There must be a single space between the closing parenthesis and the opening brace of a multi-line function declaration; found newline
Loading history...
74
    {
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
     * Change variable name to camel caps format.
83
     *
84
     * @param string $variable
85
     *
86
     * @return string Changed variable name
87
     */
88 3
    public function changeName($variable)
89
    {
90 3
        return lcfirst(
91 3
            implode(
92 3
                '',
93 3
                array_map(
94
                    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...
95 3
                    explode('_', $variable)
96 3
                )
97 3
            )
98 3
        );
99
    }
100
101
    /**
102
     * Recursively scan path for files with specified extensions.
103
     *
104
     * @param string $source     Entry point path
105
     * @param string $path       Entry path for scanning
106
     * @param array  $extensions Collection of file extensions without dot
107
     */
108 4
    public function scan($source, array $extensions = array(View::DEFAULT_EXT), $path = null)
109
    {
110 4
        $this->entryPath = $source;
111
112 4
        $path = isset($path) ? $path : $source;
113
114
        // Recursively go deeper into inner folders for scanning
115 4
        $folders  = glob($path.'/*', GLOB_ONLYDIR);
116 4
        foreach ($folders as $folder) {
117 3
            $this->scan($source, $extensions, $folder);
118 4
        }
119
120
        // Iterate file extensions
121 4
        foreach ($extensions as $extension) {
122 4
            foreach (glob(rtrim($path, '/') . '/*.'.$extension) as $file) {
123 4
                $this->files[str_replace($source, '', $file)] = $file;
124 4
            }
125 4
        }
126 4
    }
127
128
    /**
129
     * Analyze view file and create its metadata.
130
     *
131
     * @param string $file Path to view file
132
     *
133
     * @return Metadata View file metadata
134
     */
135 3
    public function analyze($file)
136
    {
137 3
        $metadata = new Metadata();
138 3
        $fileText = file_get_contents($file);
139
        // Use PHP tokenizer to find variables
140 3
        foreach ($tokens = token_get_all($fileText) as $idx => $token) {
141 3
            if (!is_string($token) && $token[0] === T_VARIABLE) {
142
                // Store variable
143 3
                $variableText = $token[1];
144
                // Store variable name
145 3
                $variableName = ltrim($token[1], '$');
146
147
                // Ignore static variables
148 3
                if (isset($tokens[$idx-1]) && $tokens[$idx-1][0] === T_DOUBLE_COLON) {
149 1
                    $metadata->static[$variableName] = $variableText;
150 1
                    continue;
151
                }
152
153
                // If next token is object operator
154 3
                if ($tokens[$idx + 1][0] === T_OBJECT_OPERATOR) {
155
                    // Ignore $this
156 3
                    if ($variableName === 'this') {
157 3
                        continue;
158
                    }
159
160
                    // And two more tokens
161 1
                    $variableText .= $tokens[$idx + 1][1] . $tokens[$idx + 2][1];
162
163
                    // Store object variable
164 1
                    $metadata->variables[$this->changeName($variableName)] = $variableText;
165
                    // Store view variable key - actual object name => full variable usage
166 1
                    $metadata->originalVariables[$this->changeName($variableName)] = $variableName;
167 1
                } else {
168
                    // Store original variable name
169 3
                    $metadata->originalVariables[$this->changeName($variableName)] = $variableName;
170
                    // Store view variable key - actual object name => full variable usage
171 3
                    $metadata->variables[$this->changeName($variableName)] = $variableText;
172
                }
173 3
            } elseif ($token[0] === T_DOC_COMMENT) { // Match doc block comments
174
                // Parse variable type and name
175 3
                if (preg_match('/@var\s+(?<type>[^ ]+)\s+(?<variable>[^*]+)/', $token[1], $matches)) {
176 3
                    $metadata->types[substr(trim($matches['variable']), 1)] = $matches['type'];
177 3
                }
178 3
            }
179 3
        }
180 3
        if (preg_match_all('/\$this->block\(\'(?<block>[^ ]+)\'/', $fileText, $matches)) {
181 2
            $metadata->blocks = $matches['block'];
182 2
        }
183
184 3
        if (preg_match('/\$this->extend\((?<class>[^ ]+\:\:class)\s*\,\s*\'(?<block>[^ ]+)\'\s*\)/', $fileText, $matches)) {
185 2
            $metadata->parentClass = $matches['class'];
186 2
            $metadata->parentBlock = $matches['block'];
187 2
        }
188
189 3
        return $metadata;
190
    }
191
192
    /**
193
     * Generic class name and its name space generator.
194
     *
195
     * @param string $file      Full path to view file
196
     * @param string $entryPath Entry path
197
     *
198
     * @return array Class name[0] and namespace[1]
199
     * @throws GeneratedViewPathHasReservedWord
200
     */
201 3
    protected function generateClassName($file, $entryPath)
202
    {
203
        // Get only file name as a class name with suffix
204 3
        $className = ucfirst(pathinfo($file, PATHINFO_FILENAME) . self::VIEW_CLASSNAME_SUFFIX);
205
206
        // Get namespace as part of file path relatively to entry path
207 3
        $nameSpace = rtrim(ltrim(
208 3
            str_replace(
209 3
                '/',
210 3
                '\\',
211 3
                str_replace($entryPath, '', pathinfo($file, PATHINFO_DIRNAME))
212 3
            ),
213
            '\\'
214 3
        ), '\\');
215
216
        // Remove ignored parts from namespaces
217 3
        $nameSpace = str_replace($this->ignoreNamespace, '', $nameSpace);
218
219
        // Check generated namespaces
220 3
        foreach (static::$reservedWords as $reservedWord) {
221 3
            if (strpos($nameSpace, '\\' . $reservedWord) !== false) {
222 1
                throw new GeneratedViewPathHasReservedWord($file.'('.$reservedWord.')');
223
            }
224 3
        }
225
226
        // Return collection for further usage
227 3
        return array($className, rtrim($this->namespacePrefix . $nameSpace, '\\'));
228
    }
229
230
    /**
231
     * Generate view classes.
232
     *
233
     * @param string $path Entry path for generated classes and folders
234
     */
235 3
    public function generate($path = __DIR__)
236
    {
237 3
        foreach ($this->files as $relativePath => $absolutePath) {
238 3
            $this->metadata[$absolutePath] = $this->analyze($absolutePath);
239 3
            $this->metadata[$absolutePath]->path = $absolutePath;
240 3
            list($this->metadata[$absolutePath]->className,
241 3
                $this->metadata[$absolutePath]->namespace) = $this->generateClassName($absolutePath, $this->entryPath);
242 3
        }
243
244 2
        foreach ($this->metadata as $metadata) {
245 2
            $this->generateViewClass($metadata, $path);
246 2
            $this->generateViewClass($metadata, $path);
247 2
        }
248 2
    }
249
250
    /** @return string Hash representing generator state */
251 1
    public function hash()
252
    {
253 1
        $hash = '';
254 1
        foreach ($this->files as $relativePath => $absolutePath) {
255 1
            $hash .= md5($relativePath.filemtime($absolutePath));
256 1
        }
257
258 1
        return md5($hash);
259
    }
260
261
    /**
262
     * Create View class ancestor.
263
     *
264
     * @param Metadata $metadata View file metadata
265
     * @param string   $path Entry path for generated classes and folders
266
     */
267 2
    protected function generateViewClass(Metadata $metadata, $path)
268
    {
269 2
        $metadataParentClass = eval('return '.$metadata->parentClass.';');
270
271 2
        $parentClass = !isset($metadata->parentClass)?$this->parentViewClass:$metadataParentClass;
272 2
        $this->generator
0 ignored issues
show
Bug introduced by
The method defnamespace() cannot be called from this context as it is declared private in class samsonphp\generator\Generator.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
273 2
            ->defNamespace($metadata->namespace)
274 2
            ->multiComment(array('Class for view "'.$metadata->path.'" rendering'))
275 2
            ->defClass($metadata->className, '\\' . $parentClass)
276 2
            ->commentVar('string', 'Path to view file')
277 2
            ->defClassVar('$file', 'protected', $metadata->path)
278 2
            ->commentVar('string', 'Parent block name')
279 2
            ->defClassVar('$parentBlock', 'protected', $metadata->parentBlock)
280 2
            ->commentVar('array', 'Blocks list')
281 2
            ->defClassVar('$blocks', 'protected', $metadata->blocks)
0 ignored issues
show
Documentation introduced by
$metadata->blocks is of type array, but the function expects a string|null.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282 2
            ->commentVar('string', 'View source code')
283 2
            ->defClassVar('$source', 'protected', '<<<\'EOT\'' . "\n" . file_get_contents($metadata->path) . "\n" . 'EOT');
284
            //->commentVar('array', 'Collection of view variables')
285
            //->defClassVar('$variables', 'public static', array_keys($metadata->variables))
286
            //->commentVar('array', 'Collection of view variable types')
287
            //->defClassVar('$types', 'public static', $metadata->types)
288 2
        ;
289
290
        // Iterate all view variables
291 2
        foreach (array_keys($metadata->variables) as $name) {
292 2
            $type = array_key_exists($name, $metadata->types) ? $metadata->types[$name] : 'mixed';
293 2
            $static = array_key_exists($name, $metadata->static) ? ' static' : '';
294 2
            $this->generator
295 2
                ->commentVar($type, 'View variable')
296 2
                ->defClassVar('$'.$name, 'public'.$static);
297
298
            // Do not generate setters for static variables
299 2
            if ($static !== ' static') {
300 2
                $this->generator->text($this->generateViewVariableSetter(
301 2
                    $name,
302 2
                    $metadata->originalVariables[$name],
303
                    $type
304 2
                ));
305 2
            }
306 2
        }
307
308
        // Iterate namespace and create folder structure
309 2
        $path .= '/'.str_replace('\\', '/', $metadata->namespace);
310 2
        if (!is_dir($path)) {
311 1
            mkdir($path, 0777, true);
312 1
        }
313
314 2
        $newClassFile = $path.'/'.$metadata->className.'.php';
315 2
        file_put_contents(
316 2
            $newClassFile,
317 2
            '<?php'.$this->generator->endClass()->flush()
318 2
        );
319
320
        // Make generated cache files accessible
321 2
        chmod($newClassFile, 0777);
322 2
    }
323
324
    /**
325
     * Generate constructor for application class.
326
     *
327
     * @param string $variable View variable name
328
     * @param string $original Original view variable name
329
     * @param string $type Variable type
330
     *
331
     * @return string View variable setter method
332
     */
333 2
    protected function generateViewVariableSetter($variable, $original, $type = 'mixed')
334
    {
335
        // Define type hint
336 2
        $typeHint = strpos($type, '\\') !== false ? $type.' ' : '';
337
338 2
        $class = "\n\t" . '/**';
339 2
        $class .= "\n\t" . ' * Setter for ' . $variable . ' view variable';
340 2
        $class .= "\n\t" . ' *';
341 2
        $class .= "\n\t" . ' * @param '.$type.' $value View variable value';
342 2
        $class .= "\n\t" . ' * @return $this Chaining';
343 2
        $class .= "\n\t" . ' */';
344 2
        $class .= "\n\t" . 'public function ' . $variable . '('.$typeHint.'$value)';
345 2
        $class .= "\n\t" . '{';
346 2
        $class .= "\n\t\t" . 'return parent::set($value, \'' . $original . '\');';
347 2
        $class .= "\n\t" . '}' . "\n";
348
349 2
        return $class;
350
    }
351
}
352