Passed
Pull Request — master (#56)
by Rustam
02:34
created

AbstractGenerator   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 552
Duplicated Lines 0 %

Test Coverage

Coverage 43.48%

Importance

Changes 6
Bugs 1 Features 0
Metric Value
wmc 65
eloc 228
c 6
b 1
f 0
dl 0
loc 552
ccs 70
cts 161
cp 0.4348
rs 3.2

34 Methods

Rating   Name   Duplication   Size   Complexity  
A stickyAttributes() 0 3 1
A autoCompleteData() 0 3 1
A hints() 0 6 1
A successMessage() 0 3 1
A requiredTemplates() 0 3 1
A attributeLabels() 0 5 1
A __construct() 0 4 1
A getTemplate() 0 3 1
A rules() 0 6 1
A defaultTemplate() 0 5 1
A validateNewClass() 0 19 4
A saveStickyAttributes() 0 16 5
A validateClass() 0 8 2
A loadStickyAttributes() 0 11 6
A setTemplates() 0 3 1
A generateString() 0 15 2
A getStickyDataFile() 0 3 1
B save() 0 27 6
A getDirectory() 0 3 1
A hasAttribute() 0 4 1
A getTemplates() 0 3 1
B isReservedKeyword() 0 86 1
A render() 0 24 4
A getTemplatePath() 0 11 3
A setTemplate() 0 3 1
A getData() 0 5 1
A validate() 0 6 1
A getAttributeValue() 0 7 2
A setDirectory() 0 3 1
A getErrors() 0 3 1
A validateTemplate() 0 21 5
A getDescription() 0 3 1
A hasErrors() 0 3 1
A load() 0 6 3

How to fix   Complexity   

Complex Class

Complex classes like AbstractGenerator often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractGenerator, and based on these observations, apply Extract Interface, too.

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