Completed
Push — master ( 7179be...d0f261 )
by Vitaly
02:05
created

Generator   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 246
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 100%

Importance

Changes 16
Bugs 2 Features 9
Metric Value
wmc 24
c 16
b 2
f 9
lcom 1
cbo 3
dl 0
loc 246
ccs 109
cts 109
cp 1
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A changeName() 0 12 1
B scan() 0 19 5
B analyze() 0 23 5
B generateClassName() 0 28 3
A generate() 0 13 3
A hash() 0 9 2
B generateViewClass() 0 30 3
A generateViewVariableSetter() 0 15 1
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
 *
27
 * @package samsonframework\view
28
 */
29
class Generator
30
{
31
    /** string All generated view classes will end with this suffix */
32
    const VIEW_CLASSNAME_SUFFIX = 'View';
33
34
    /** @var array Collection of PHP reserved words */
35
    protected static $reservedWords = array('list');
36
37
    /** @var Metadata[] Collection of view metadata */
38
    protected $metadata = array();
39
40
    /** @var \samsonphp\generator\Generator */
41
    protected $generator;
42
43
    /** @var string Generated classes namespace prefix */
44
    protected $namespacePrefix;
45
46
    /** @var string Collection of namespace parts to be ignored in generated namespaces */
47
    protected $ignoreNamespace = array();
48
49
    /** @var array Collection of view files */
50
    protected $files;
51
52
    /** @var string Scanning entry path */
53
    protected $entryPath;
54
55
    /**
56
     * Generator constructor.
57
     *
58
     * @param \samsonphp\generator\Generator $generator
59
     * @param string                         $namespacePrefix
60
     * @param array                          $ignoreNamespace
61
     */
62 3
    public function __construct(\samsonphp\generator\Generator $generator, $namespacePrefix, array $ignoreNamespace = array())
63
    {
64 3
        $this->generator = $generator;
65 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...
66 3
        $this->namespacePrefix = rtrim(ltrim($namespacePrefix, '\\'), '\\').'\\';
67 3
    }
68
69
    /**
70
     * Change variable name to camel caps format.
71
     *
72
     * @param string $variable
73
     *
74
     * @return string Changed variable name
75
     */
76 1
    public function changeName($variable)
77
    {
78 1
        return lcfirst(
79 1
            implode(
80 1
                '',
81 1
                array_map(
82
                    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...
83 1
                    explode('_', $variable)
84 1
                )
85 1
            )
86 1
        );
87
    }
88
89
    /**
90
     * Recursively scan path for files with specified extensions.
91
     *
92
     * @param string $source     Entry point path
93
     * @param string $path       Entry path for scanning
94
     * @param array  $extensions Collection of file extensions without dot
95
     */
96 3
    public function scan($source, array $extensions = array(View::DEFAULT_EXT), $path = null)
97
    {
98 3
        $this->entryPath = $source;
99
100 3
        $path = isset($path) ? $path : $source;
101
102
        // Recursively go deeper into inner folders for scanning
103 3
        $folders  = glob($path.'/*', GLOB_ONLYDIR);
104 3
        foreach ($folders as $folder) {
105 3
            $this->scan($source, $extensions, $folder);
106 3
        }
107
108
        // Iterate file extensions
109 3
        foreach ($extensions as $extension) {
110 3
            foreach (glob(rtrim($path, '/') . '/*.'.$extension) as $file) {
111 3
                $this->files[str_replace($source, '', $file)] = $file;
112 3
            }
113 3
        }
114 3
    }
115
116
    /**
117
     * Analyze view file and create its metadata.
118
     *
119
     * @param string $file Path to view file
120
     *
121
     * @return Metadata View file metadata
122
     */
123 2
    public function analyze($file)
124
    {
125 2
        $metadata = new Metadata();
126
        // Use PHP tokenizer to find variables
127 2
        foreach ($tokens = token_get_all(file_get_contents($file)) as $idx => $token) {
128 2
            if (!is_string($token) && $token[0] === T_VARIABLE) {
129
                // Store variable
130 1
                $variableText = $token[1];
131
                // Store variable name
132 1
                $variableName = ltrim($token[1], '$');
133
                // If next token is object operator
134 1
                if ($tokens[$idx + 1][0] === T_OBJECT_OPERATOR) {
135 1
                    $variableName = $tokens[$idx + 2][1];
136
                    // And two more tokens
137 1
                    $variableText .= $tokens[$idx + 1][1] . $variableName;
138 1
                }
139
                // Store view variable key - actual object name => full varaible usage
140 1
                $metadata->variables[$this->changeName($variableName)] = $variableText;
141 1
            }
142 2
        }
143
144 2
        return $metadata;
145
    }
146
147
    /**
148
     * Generic class name and its name space generator.
149
     *
150
     * @param string $file      Full path to view file
151
     * @param string $entryPath Entry path
152
     *
153
     * @return array Class name[0] and namespace[1]
154
     * @throws GeneratedViewPathHasReservedWord
155
     */
156 2
    protected function generateClassName($file, $entryPath)
157
    {
158
        // Get only file name as a class name with suffix
159 2
        $className = ucfirst(pathinfo($file, PATHINFO_FILENAME) . self::VIEW_CLASSNAME_SUFFIX);
160
161
        // Get namespace as part of file path relatively to entry path
162 2
        $nameSpace = rtrim(ltrim(
163 2
            str_replace(
164 2
                '/',
165 2
                '\\',
166 2
                str_replace($entryPath, '', pathinfo($file, PATHINFO_DIRNAME))
167 2
            ),
168
            '\\'
169 2
        ), '\\');
170
171
        // Remove ignored parts from namespaces
172 2
        $nameSpace = str_replace($this->ignoreNamespace, '', $nameSpace);
173
174
        // Check generated namespaces
175 2
        foreach (static::$reservedWords as $reservedWord) {
176 2
            if (strpos($nameSpace, '\\' . $reservedWord) !== false) {
177 1
                throw new GeneratedViewPathHasReservedWord($file.'('.$reservedWord.')');
178
            }
179 1
        }
180
181
        // Return collection for further usage
182 1
        return array($className, rtrim($this->namespacePrefix . $nameSpace, '\\'));
183
    }
184
185
    /**
186
     * Generate view classes.
187
     *
188
     * @param string $path Entry path for generated classes and folders
189
     */
190 2
    public function generate($path = __DIR__)
191
    {
192 2
        foreach ($this->files as $relativePath => $absolutePath) {
193 2
            $this->metadata[$absolutePath] = $this->analyze($absolutePath);
194 2
            $this->metadata[$absolutePath]->path = $relativePath;
0 ignored issues
show
Documentation Bug introduced by
It seems like $relativePath can also be of type integer. However, the property $path is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
195 1
            list($this->metadata[$absolutePath]->className,
196 2
                $this->metadata[$absolutePath]->namespace) = $this->generateClassName($absolutePath, $this->entryPath);
197 1
        }
198
199 1
        foreach ($this->metadata as $metadata) {
200 1
            $this->generateViewClass($metadata, $path);
201 1
        }
202 1
    }
203
204
    /** @return string Hash representing generator state */
205 1
    public function hash()
206
    {
207 1
        $hash = '';
208 1
        foreach ($this->files as $relativePath => $absolutePath) {
209 1
            $hash .= $relativePath.filemtime($absolutePath);
210 1
        }
211
212 1
        return md5($hash);
213
    }
214
215
    /**
216
     * Create View class ancestor.
217
     *
218
     * @param Metadata $metadata View file metadata
219
     * @param string   $path Entry path for generated classes and folders
220
     */
221 1
    protected function generateViewClass(Metadata $metadata, $path)
222
    {
223 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...
224 1
            ->defNamespace($metadata->namespace)
225 1
            ->multiComment(array('Class for view "'.$metadata->path.'" rendering'))
226 1
            ->defClass($metadata->className, '\\' . View::class)
227 1
            ->commentVar('string', 'Path to view file')
228 1
            ->defClassVar('$path', 'protected', $metadata->path)
229 1
            ->commentVar('array', 'Collection of view variables')
230 1
            ->defClassVar('$variables', 'public static', array_keys($metadata->variables));
0 ignored issues
show
Documentation introduced by
array_keys($metadata->variables) is of type array<integer,integer|string>, 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...
231
232
        // Iterate all view variables
233 1
        foreach (array_keys($metadata->variables) as $name) {
234 1
            $this->generator
235 1
                ->commentVar('mixed', 'View variable')
236 1
                ->defClassVar('$'.$name, 'public')
237 1
                ->text($this->generateViewVariableSetter($name));
238 1
        }
239
240
        // Iterate namespace and create folder structure
241 1
        $path .= '/'.str_replace('\\', '/', $metadata->namespace);
242 1
        if (!is_dir($path)) {
243 1
            mkdir($path, 0775, true);
244 1
        }
245
246 1
        file_put_contents(
247 1
            $path.'/'.$metadata->className.'.php',
248 1
            '<?php'.$this->generator->endClass()->flush()
249 1
        );
250 1
    }
251
252
    /**
253
     * Generate constructor for application class.
254
     *
255
     * @param string $variable View variable name
256
     *
257
     * @return string View variable setter method
258
     */
259 1
    protected function generateViewVariableSetter($variable)
260
    {
261 1
        $class = "\n\t" . '/**';
262 1
        $class .= "\n\t" . ' * Setter for ' . $variable . ' view variable';
263 1
        $class .= "\n\t" . ' *';
264 1
        $class .= "\n\t" . ' * @param mixed $value View variable value';
265 1
        $class .= "\n\t" . ' * @return $this Chaining';
266 1
        $class .= "\n\t" . ' */';
267 1
        $class .= "\n\t" . 'public function ' . $variable . '($value)';
268 1
        $class .= "\n\t" . '{';
269 1
        $class .= "\n\t\t" . 'return parent::set($value, \'' . $variable . '\');';
270 1
        $class .= "\n\t" . '}' . "\n";
271
272 1
        return $class;
273
    }
274
}
275