Test Failed
Pull Request — master (#58)
by Dmitriy
02:33
created

AbstractGenerator::isReservedKeyword()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 86
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 1.0895

Importance

Changes 0
Metric Value
cc 1
eloc 82
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 86
ccs 21
cts 38
cp 0.5526
crap 1.0895
rs 8.3927

How to fix   Long Method   

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
declare(strict_types=1);
4
5
namespace Yiisoft\Yii\Gii\Generator;
6
7
use Exception;
8
use InvalidArgumentException;
9
use ReflectionClass;
10
use ReflectionException;
11
use RuntimeException;
12
use Throwable;
13
use Yiisoft\Aliases\Aliases;
14
use Yiisoft\Json\Json;
15
use Yiisoft\Validator\DataSetInterface;
16
use Yiisoft\Validator\Result;
17
use Yiisoft\Validator\Rule\Required;
18
use Yiisoft\Validator\ValidatorInterface;
19
use Yiisoft\Yii\Gii\CodeFile;
20
use Yiisoft\Yii\Gii\Exception\InvalidConfigException;
21
use Yiisoft\Yii\Gii\GeneratorInterface;
22
use Yiisoft\Yii\Gii\GiiParametersProvider;
23
24
/**
25
 * This is the base class for all generator classes.
26
 *
27
 * A generator instance is responsible for taking user inputs, validating them,
28
 * and using them to generate the corresponding code based on a set of code template files.
29
 *
30
 * A generator class typically needs to implement the following methods:
31
 *
32
 * - {@see GeneratorInterface::getName()}: returns the name of the generator
33
 * - {@see GeneratorInterface::getDescription()}: returns the detailed description of the generator
34
 * - {@see GeneratorInterface::validate()}: returns generator validation result
35
 * - {@see GeneratorInterface::generate()}: generates the code based on the current user input and the specified code
36
 * template files. This is the place where main code generation code resides.
37
 */
38
abstract class AbstractGenerator implements GeneratorInterface, DataSetInterface
39
{
40
    private array $errors = [];
41
42
    /**
43
     * @var string the name of the code template that the user has selected.
44
     * The value of this property is internally managed by this class.
45
     */
46
    private string $template = 'default';
47
    private string $directory = 'src/Controller';
48
49
    public function __construct(
50
        protected Aliases $aliases,
51
        protected ValidatorInterface $validator,
52
        protected         GiiParametersProvider $parametersProvider,
53
    ) {
54
    }
55
56 7
    public function attributeLabels(): array
57
    {
58
        return [
59
            'enableI18N' => 'Enable I18N',
60
            'messageCategory' => 'Message Category',
61
        ];
62
    }
63
64
    /**
65
     * Returns a list of code template files that are required.
66
     * Derived classes usually should override this method if they require the existence of
67
     * certain template files.
68
     *
69
     * @return array list of code template files that are required. They should be file paths
70
     * relative to {@see getTemplatePath()}.
71
     */
72
    public function requiredTemplates(): array
73
    {
74
        return [];
75
    }
76
77
    /**
78
     * Returns the list of sticky attributes.
79
     * A sticky attribute will remember its value and will initialize the attribute with this value
80
     * when the generator is restarted.
81
     *
82
     * @return array list of sticky attributes
83
     */
84
    public function stickyAttributes(): array
85
    {
86
        return ['template', 'enableI18N', 'messageCategory'];
87
    }
88
89
    /**
90
     * Returns the list of hint messages.
91
     * The array keys are the attribute names, and the array values are the corresponding hint messages.
92
     * Hint messages will be displayed to end users when they are filling the form for the generator.
93
     *
94
     * @return array the list of hint messages
95
     */
96
    public function hints(): array
97
    {
98
        return [
99
            'enableI18N' => 'This indicates whether the generator should generate strings using <code>Yii::t()</code> method.
100
                Set this to <code>true</code> if you are planning to make your application translatable.',
101
            'messageCategory' => 'This is the category used by <code>Yii::t()</code> in case you enable I18N.',
102
        ];
103
    }
104
105
    /**
106
     * Returns the list of auto complete values.
107
     * The array keys are the attribute names, and the array values are the corresponding auto complete values.
108
     * Auto complete values can also be callable typed in order one want to make postponed data generation.
109
     *
110
     * @return array the list of auto complete values
111
     */
112
    public function autoCompleteData(): array
113
    {
114
        return [];
115
    }
116
117
    /**
118
     * Returns the root path to the default code template files.
119
     * The default implementation will return the "templates" subdirectory of the
120
     * directory containing the generator class file.
121
     *
122
     * @throws ReflectionException
123
     *
124
     * @return string the root path to the default code template files.
125
     */
126
    private function defaultTemplate(): string
127
    {
128
        $class = new ReflectionClass($this);
129
130
        return dirname($class->getFileName()) . '/default';
131
132
    }
133
134
    public function getDescription(): string
135
    {
136
        return '';
137
    }
138
139
    public function validate(AbstractGeneratorCommand $command): Result
140
    {
141
        return $this->validator->validate($command);
142
    }
143 1
144
    /**
145 1
     * Loads sticky attributes from an internal file and populates them into the generator.
146
     *
147 1
     * @internal
148
     */
149
    public function loadStickyAttributes(): void
150
    {
151
        $stickyAttributes = $this->stickyAttributes();
152
        $path = $this->getStickyDataFile();
153
        if (is_file($path)) {
154
            $result = Json::decode(file_get_contents($path));
155 3
            if (is_array($result)) {
156
                foreach ($stickyAttributes as $name) {
157 3
                    $method = 'set' . $name;
158
                    if (array_key_exists($name, $result) && method_exists($this, $method)) {
159 3
                        $this->$method($result[$name]);
160 3
                    }
161
                }
162
            }
163
        }
164
    }
165
166
    /**
167
     * Loads sticky attributes from an internal file and populates them into the generator.
168
     */
169
    public function load(array $data): void
170
    {
171
        foreach ($data as $name => $value) {
172
            $method = 'set' . $name;
173
            if (method_exists($this, $method)) {
174
                $this->$method($value);
175 3
            }
176
        }
177
    }
178
179 3
    /**
180 3
     * Saves sticky attributes into an internal file.
181
     */
182
    public function saveStickyAttributes(): void
183
    {
184
        $stickyAttributes = $this->stickyAttributes();
185
        $stickyAttributes[] = 'template';
186
        $values = [];
187
        foreach ($stickyAttributes as $name) {
188
            $method = 'get' . $name;
189
            if (method_exists($this, $method)) {
190
                $values[$name] = $this->$method();
191
            }
192
        }
193
        $path = $this->getStickyDataFile();
194
        if (!mkdir($concurrentDirectory = dirname($path), 0755, true) && !is_dir($concurrentDirectory)) {
195
            throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
196
        }
197
        file_put_contents($path, Json::encode($values));
198
    }
199
200
    protected function getStickyDataFile(): string
201
    {
202
        return sprintf('%s/gii/%s.json', $this->aliases->get('@runtime'), str_replace('\\', '-', static::class));
203
    }
204
205
    /**
206
     * Saves the generated code into files.
207
     *
208
     * @param CodeFile[] $files the code files to be saved
209
     * @param string[] $results this parameter receives a value from this method indicating the log messages
210 3
     * generated while saving the code files.
211
     *
212 3
     * @throws ReflectionException
213 3
     * @throws InvalidConfigException
214 3
     *
215 3
     * @return bool whether files are successfully saved without any error.
216
     */
217
    public function save(array $files, array $answers, array &$results): bool
218
    {
219
//        $results = ['Generating code using template "' . $this->getTemplatePath() . '"...'];
220
        $hasError = false;
221
        foreach ($files as $file) {
222
            $relativePath = $file->getRelativePath();
223
            if (!empty($answers[$file->getId()]) && $file->getOperation() !== CodeFile::OP_SKIP) {
224
                try {
225
                    $file->save();
226
                    $results[] = $file->getOperation() === CodeFile::OP_CREATE
227
                        ? " generated  $relativePath"
228
                        : " overwrote  $relativePath";
229
                } catch (Exception $e) {
230
                    $hasError = true;
231
                    $results[] = sprintf(
232
                        "   generating %s\n    - <span class=\"error\">%s</span>",
233
                        $relativePath,
234
                        $e->getMessage()
235
                    );
236
                }
237
            } else {
238
                $results[] = "   skipped    $relativePath";
239
            }
240
        }
241
        $results[] = 'done!';
242
243
        return !$hasError;
244
    }
245
246
    /**
247
     * @throws ReflectionException
248
     * @throws InvalidConfigException
249
     *
250
     * @return string the root path of the template files that are currently being used.
251
     */
252
    public function getTemplatePath(AbstractGeneratorCommand $command): string
253
    {
254
        $template = $command->getTemplate();
255
256
        if ($template === 'default') {
257
            return $this->defaultTemplate();
258
        }
259
260
        if (isset($this->parametersProvider->getTemplates()[$template])) {
261
            return $this->parametersProvider->getTemplates()[$template];
262
        }
263
264
        throw new InvalidConfigException("Unknown template: {$template}");
265
    }
266
267
    /**
268
     * Generates code using the specified code template and parameters.
269
     * Note that the code template will be used as a PHP file.
270
     *
271
     * @param string $template the code template file. This must be specified as a file path
272
     * relative to {@see getTemplatePath()}.
273
     * @param array $params list of parameters to be passed to the template file.
274
     *
275
     * @throws Throwable
276
     *
277
     * @return string the generated code
278
     */
279
    protected function render(AbstractGeneratorCommand $command, string $template, array $params = []): string
280
    {
281
        $file = sprintf(
282
            '%s/%s.php',
283
            $this->aliases->get($this->getTemplatePath($command)),
284
            $template
285
        );
286
287
        $renderer = function (): void {
288
            extract(func_get_arg(1));
289
            /** @psalm-suppress UnresolvableInclude */
290
            require func_get_arg(0);
291
        };
292
293 2
        $obInitialLevel = ob_get_level();
294
        ob_start();
295 2
        ob_implicit_flush(false);
296 1
        try {
297
            /** @psalm-suppress PossiblyInvalidFunctionCall */
298
            $renderer->bindTo($this)($file, array_merge($params, ['command'=>$command]));
299 1
            return ob_get_clean();
300 1
        } catch (Throwable $e) {
301
            while (ob_get_level() > $obInitialLevel) {
302
                if (!@ob_end_clean()) {
303
                    ob_clean();
304
                }
305
            }
306
            throw $e;
307
        }
308
    }
309
310
    /**
311
     * An inline validator that checks if the attribute value refers to an existing class name.
312
     *
313
     * @param string $value the attribute being validated
314
     */
315
    public function validateClass(string $value): Result
316
    {
317
        $result = new Result();
318 2
        if (!class_exists($value)) {
319
            $result->addError("Class '$value' does not exist or has syntax error.");
320 2
        }
321
322 2
        return $result;
323 2
    }
324
325 2
    /**
326
     * @param string $value the attribute to be validated
327
     *
328 2
     * @return bool whether the value is a reserved PHP keyword.
329 2
     */
330 2
    public function isReservedKeyword(string $value): bool
331
    {
332
        static $keywords = [
333 2
            '__class__',
334 2
            '__dir__',
335
            '__file__',
336
            '__function__',
337
            '__line__',
338
            '__method__',
339
            '__namespace__',
340
            '__trait__',
341
            'abstract',
342
            'and',
343
            'array',
344
            'as',
345
            'break',
346
            'case',
347
            'catch',
348
            'callable',
349
            'cfunction',
350
            'class',
351
            'clone',
352 3
            'const',
353
            'continue',
354
            'declare',
355 3
            'default',
356 3
            'die',
357 3
            'do',
358 3
            'echo',
359 1
            'else',
360
            'elseif',
361 2
            'empty',
362 1
            'enddeclare',
363
            'endfor',
364 1
            'endforeach',
365 1
            'endif',
366 1
            'endswitch',
367
            'endwhile',
368
            'eval',
369
            'exception',
370
            'exit',
371
            'extends',
372 2
            'final',
373
            'finally',
374
            'for',
375
            'foreach',
376
            'function',
377
            'global',
378
            'goto',
379
            'if',
380
            'implements',
381
            'include',
382
            'include_once',
383
            'instanceof',
384
            'insteadof',
385
            'interface',
386
            'isset',
387
            'list',
388
            'namespace',
389
            'new',
390
            'old_function',
391
            'or',
392
            'parent',
393
            'php_user_filter',
394
            'print',
395
            'private',
396 3
            'protected',
397
            'public',
398
            'require',
399 3
            'require_once',
400 3
            'return',
401 3
            'static',
402 3
            'switch',
403
            'this',
404
            'throw',
405
            'trait',
406
            'try',
407
            'unset',
408
            'use',
409
            'var',
410
            'while',
411
            'xor',
412
            'fn',
413
        ];
414 3
415
        return in_array(strtolower($value), $keywords, true);
416
    }
417
418
    /**
419
     * Generates a string depending on enableI18N property
420
     *
421
     * @param string $string the text be generated
422
     * @param array $placeholders the placeholders to use by `Yii::t()`
423
     */
424
    public function generateString(string $string = '', array $placeholders = []): string
425
    {
426
        $string = addslashes($string);
427
        if (!empty($placeholders)) {
428
            $phKeys = array_map(
429
                static fn ($word) => '{' . $word . '}',
430
                array_keys($placeholders)
431
            );
432
            $phValues = array_values($placeholders);
433
            $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
434
        } else {
435
            // No placeholders, just the given string
436
            $str = "'" . $string . "'";
437
        }
438
        return $str;
439
    }
440
441
    public function getAttributeValue(string $attribute): mixed
442
    {
443
        if (!$this->hasAttribute($attribute)) {
444
            throw new InvalidArgumentException(sprintf('There is no "%s" in %s.', $attribute, $this->getName()));
445
        }
446
        $method = 'get' . $attribute;
447
        return $this->$method();
448
    }
449
450
    public function hasAttribute(string $attribute): bool
451
    {
452
        $method = 'get' . $attribute;
453
        return method_exists($this, $method);
454
    }
455
456
    public function getErrors(): array
457
    {
458
        return $this->errors;
459
    }
460
461
    public function hasErrors(): bool
462
    {
463
        return $this->errors !== [];
464
    }
465
466
    public function getDirectory(): string
467
    {
468
        return $this->directory;
469
    }
470
471
    public function getData(): array
472
    {
473
        return [
474
            //            'templates' => $this->templates,
475
            'template' => $this->template,
476
        ];
477
    }
478
}
479