Completed
Push — master ( ef826c...eeb9bb )
by Vitaly
02:08
created

Generator::analyze()   D

Complexity

Conditions 10
Paths 10

Size

Total Lines 46
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 10.0512

Importance

Changes 8
Bugs 1 Features 5
Metric Value
c 8
b 1
f 5
dl 0
loc 46
ccs 23
cts 25
cp 0.92
rs 4.983
cc 10
eloc 21
nc 10
nop 1
crap 10.0512

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
    /**
58
     * Generator constructor.
59
     *
60
     * @param \samsonphp\generator\Generator $generator
61
     * @param string                         $namespacePrefix
62
     * @param array                          $ignoreNamespace
63
     */
64 3
    public function __construct(\samsonphp\generator\Generator $generator, $namespacePrefix, array $ignoreNamespace = array())
65
    {
66 3
        $this->generator = $generator;
67 3
        $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...
68 3
        $this->namespacePrefix = rtrim(ltrim($namespacePrefix, '\\'), '\\').'\\';
69 3
    }
70
71
    /**
72
     * Change variable name to camel caps format.
73
     *
74
     * @param string $variable
75
     *
76
     * @return string Changed variable name
77
     */
78 1
    public function changeName($variable)
79
    {
80 1
        return lcfirst(
81 1
            implode(
82 1
                '',
83 1
                array_map(
84
                    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...
85 1
                    explode('_', $variable)
86 1
                )
87 1
            )
88 1
        );
89
    }
90
91
    /**
92
     * Recursively scan path for files with specified extensions.
93
     *
94
     * @param string $source     Entry point path
95
     * @param string $path       Entry path for scanning
96
     * @param array  $extensions Collection of file extensions without dot
97
     */
98 3
    public function scan($source, array $extensions = array(View::DEFAULT_EXT), $path = null)
99
    {
100 3
        $this->entryPath = $source;
101
102 3
        $path = isset($path) ? $path : $source;
103
104
        // Recursively go deeper into inner folders for scanning
105 3
        $folders  = glob($path.'/*', GLOB_ONLYDIR);
106 3
        foreach ($folders as $folder) {
107 3
            $this->scan($source, $extensions, $folder);
108 3
        }
109
110
        // Iterate file extensions
111 3
        foreach ($extensions as $extension) {
112 3
            foreach (glob(rtrim($path, '/') . '/*.'.$extension) as $file) {
113 3
                $this->files[str_replace($source, '', $file)] = $file;
114 3
            }
115 3
        }
116 3
    }
117
118
    /**
119
     * Analyze view file and create its metadata.
120
     *
121
     * @param string $file Path to view file
122
     *
123
     * @return Metadata View file metadata
124
     */
125 2
    public function analyze($file)
126
    {
127 2
        $metadata = new Metadata();
128
        // Use PHP tokenizer to find variables
129 2
        foreach ($tokens = token_get_all(file_get_contents($file)) as $idx => $token) {
130 2
            if (!is_string($token) && $token[0] === T_VARIABLE) {
131
                // Store variable
132 1
                $variableText = $token[1];
133
                // Store variable name
134 1
                $variableName = ltrim($token[1], '$');
135
136
                // Ignore static variables
137 1
                if (isset($tokens[$idx-1]) && $tokens[$idx-1][0] === T_DOUBLE_COLON) {
138
                    $metadata->static[$variableName] = $variableText;
139
                }
140
141
                // If next token is object operator
142 1
                if ($tokens[$idx + 1][0] === T_OBJECT_OPERATOR) {
143
                    // Ignore $this
144 1
                    if ($variableName === 'this') {
145 1
                        continue;
146
                    }
147
148
                    // And two more tokens
149 1
                    $variableText .= $tokens[$idx + 1][1] . $tokens[$idx + 2][1];
150
151
                    // Store object variable
152 1
                    $metadata->variables[$this->changeName($variableName)] = $variableText;
153
                    // Store view variable key - actual object name => full variable usage
154 1
                    $metadata->originalVariables[$this->changeName($variableName)] = $variableName;
155 1
                } else {
156
                    // Store original variable name
157 1
                    $metadata->originalVariables[$this->changeName($variableName)] = $variableName;
158
                    // Store view variable key - actual object name => full variable usage
159 1
                    $metadata->variables[$this->changeName($variableName)] = $variableText;
160
                }
161 2
            } elseif ($token[0] === T_DOC_COMMENT) { // Match doc block comments
162
                // Parse variable type and name
163 2
                if (preg_match('/@var\s+(?<type>[^ ]+)\s+(?<variable>[^*]+)/', $token[1], $matches)) {
164 1
                    $metadata->types[substr(trim($matches['variable']), 1)] = $matches['type'];
165 1
                }
166 2
            }
167 2
        }
168
169 2
        return $metadata;
170
    }
171
172
    /**
173
     * Generic class name and its name space generator.
174
     *
175
     * @param string $file      Full path to view file
176
     * @param string $entryPath Entry path
177
     *
178
     * @return array Class name[0] and namespace[1]
179
     * @throws GeneratedViewPathHasReservedWord
180
     */
181 2
    protected function generateClassName($file, $entryPath)
182
    {
183
        // Get only file name as a class name with suffix
184 2
        $className = ucfirst(pathinfo($file, PATHINFO_FILENAME) . self::VIEW_CLASSNAME_SUFFIX);
185
186
        // Get namespace as part of file path relatively to entry path
187 2
        $nameSpace = rtrim(ltrim(
188 2
            str_replace(
189 2
                '/',
190 2
                '\\',
191 2
                str_replace($entryPath, '', pathinfo($file, PATHINFO_DIRNAME))
192 2
            ),
193
            '\\'
194 2
        ), '\\');
195
196
        // Remove ignored parts from namespaces
197 2
        $nameSpace = str_replace($this->ignoreNamespace, '', $nameSpace);
198
199
        // Check generated namespaces
200 2
        foreach (static::$reservedWords as $reservedWord) {
201 2
            if (strpos($nameSpace, '\\' . $reservedWord) !== false) {
202 1
                throw new GeneratedViewPathHasReservedWord($file.'('.$reservedWord.')');
203
            }
204 1
        }
205
206
        // Return collection for further usage
207 1
        return array($className, rtrim($this->namespacePrefix . $nameSpace, '\\'));
208
    }
209
210
    /**
211
     * Generate view classes.
212
     *
213
     * @param string $path Entry path for generated classes and folders
214
     */
215 2
    public function generate($path = __DIR__)
216
    {
217 2
        foreach ($this->files as $relativePath => $absolutePath) {
218 2
            $this->metadata[$absolutePath] = $this->analyze($absolutePath);
219 2
            $this->metadata[$absolutePath]->path = $absolutePath;
220 1
            list($this->metadata[$absolutePath]->className,
221 2
                $this->metadata[$absolutePath]->namespace) = $this->generateClassName($absolutePath, $this->entryPath);
222 1
        }
223
224 1
        foreach ($this->metadata as $metadata) {
225 1
            $this->generateViewClass($metadata, $path);
226 1
        }
227 1
    }
228
229
    /** @return string Hash representing generator state */
230 1
    public function hash()
231
    {
232 1
        $hash = '';
233 1
        foreach ($this->files as $relativePath => $absolutePath) {
234 1
            $hash .= md5($relativePath.filemtime($absolutePath));
235 1
        }
236
237 1
        return md5($hash);
238
    }
239
240
    /**
241
     * Create View class ancestor.
242
     *
243
     * @param Metadata $metadata View file metadata
244
     * @param string   $path Entry path for generated classes and folders
245
     */
246 1
    protected function generateViewClass(Metadata $metadata, $path)
247
    {
248 1
        $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...
249 1
            ->defNamespace($metadata->namespace)
250 1
            ->multiComment(array('Class for view "'.$metadata->path.'" rendering'))
251 1
            ->defClass($metadata->className, '\\' . View::class)
252 1
            ->commentVar('string', 'Path to view file')
253 1
            ->defClassVar('$file', 'protected', $metadata->path)
254
            //->commentVar('array', 'Collection of view variables')
255
            //->defClassVar('$variables', 'public static', array_keys($metadata->variables))
256
            //->commentVar('array', 'Collection of view variable types')
257
            //->defClassVar('$types', 'public static', $metadata->types)
258
        ;
259
260
        // Iterate all view variables
261 1
        foreach (array_keys($metadata->variables) as $name) {
262 1
            $type = array_key_exists($name, $metadata->types) ? $metadata->types[$name] : 'mixed';
263 1
            $static = array_key_exists($name, $metadata->static) ? ' static' : '';
264 1
            $this->generator
265 1
                ->commentVar($type, 'View variable')
266 1
                ->defClassVar('$'.$name, 'public'.$static);
267
268
            // Do not generate setters for static variables
269 1
            if ($static !== 'static') {
270 1
                $this->generator->text($this->generateViewVariableSetter(
271 1
                    $name,
272 1
                    $metadata->originalVariables[$name],
273
                    $type
274 1
                ));
275 1
            }
276 1
        }
277
278
        // Iterate namespace and create folder structure
279 1
        $path .= '/'.str_replace('\\', '/', $metadata->namespace);
280 1
        if (!is_dir($path)) {
281 1
            mkdir($path, 0775, true);
282 1
        }
283
284 1
        file_put_contents(
285 1
            $path.'/'.$metadata->className.'.php',
286 1
            '<?php'.$this->generator->endClass()->flush()
287 1
        );
288 1
    }
289
290
    /**
291
     * Generate constructor for application class.
292
     *
293
     * @param string $variable View variable name
294
     * @param string $original Original view variable name
295
     * @param string $type Variable type
296
     *
297
     * @return string View variable setter method
298
     */
299 1
    protected function generateViewVariableSetter($variable, $original, $type = 'mixed')
300
    {
301
        // Define type hint
302 1
        $typeHint = $type !== 'mixed' && $type !== 'string' ? $type.' ' : '';
303
304 1
        $class = "\n\t" . '/**';
305 1
        $class .= "\n\t" . ' * Setter for ' . $variable . ' view variable';
306 1
        $class .= "\n\t" . ' *';
307 1
        $class .= "\n\t" . ' * @param '.$type.' $value View variable value';
308 1
        $class .= "\n\t" . ' * @return $this Chaining';
309 1
        $class .= "\n\t" . ' */';
310 1
        $class .= "\n\t" . 'public function ' . $variable . '('.$typeHint.'$value)';
311 1
        $class .= "\n\t" . '{';
312 1
        $class .= "\n\t\t" . 'return parent::set($value, \'' . $original . '\');';
313 1
        $class .= "\n\t" . '}' . "\n";
314
315 1
        return $class;
316
    }
317
}
318