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

AbstractGenerator::render()   A

Complexity

Conditions 4
Paths 7

Size

Total Lines 24
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4.5923

Importance

Changes 0
Metric Value
eloc 15
c 0
b 0
f 0
dl 0
loc 24
ccs 10
cts 15
cp 0.6667
rs 9.7666
cc 4
nc 7
nop 2
crap 4.5923
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 view file for the input form of the generator.
136
     * The default implementation will return the "form.php" file under the directory
137
     * that contains the generator class file.
138
     *
139
     * @throws ReflectionException
140
     *
141
     * @return string the view file for the input form of the generator.
142
     */
143
    public function formView(): string
144
    {
145
        $class = new ReflectionClass($this);
146
147
        return dirname($class->getFileName()) . '/form.php';
148
    }
149
150
    /**
151
     * Returns the root path to the default code template files.
152
     * The default implementation will return the "templates" subdirectory of the
153
     * directory containing the generator class file.
154
     *
155
     * @throws ReflectionException
156
     *
157
     * @return string the root path to the default code template files.
158
     */
159 1
    private function defaultTemplate(): string
160
    {
161 1
        $class = new ReflectionClass($this);
162
163 1
        return dirname($class->getFileName()) . '/default';
164
    }
165
166
    public function getDescription(): string
167
    {
168
        return '';
169
    }
170
171 3
    final public function validate(): Result
172
    {
173 3
        $result = $this->validator->validate($this, $this->rules());
174
175 3
        $this->errors = $result->getErrorMessagesIndexedByAttribute();
176 3
        return $result;
177
    }
178
179
    /**
180
     * Child classes should override this method like the following so that the parent
181
     * rules are included:
182
     *
183
     * ```php
184
     * return array_merge(parent::rules(), [
185
     *     ...rules for the child class...
186
     * ]);
187
     * ```
188
     *
189
     * @return Closure[]|Closure[][]|RuleInterface[]|RuleInterface[][]
190
     */
191 3
    public function rules(): array
192
    {
193
        return [
194
            'template' => [
195 3
                new Required(message: 'A code template must be selected.'),
196 3
                new Callback([$this, 'validateTemplate']),
197
            ],
198
        ];
199
    }
200
201
    /**
202
     * Loads sticky attributes from an internal file and populates them into the generator.
203
     *
204
     * @internal
205
     */
206
    public function loadStickyAttributes(): void
207
    {
208
        $stickyAttributes = $this->stickyAttributes();
209
        $path = $this->getStickyDataFile();
210
        if (is_file($path)) {
211
            $result = Json::decode(file_get_contents($path));
212
            if (is_array($result)) {
213
                foreach ($stickyAttributes as $name) {
214
                    $method = 'set' . $name;
215
                    if (array_key_exists($name, $result) && method_exists($this, $method)) {
216
                        $this->$method($result[$name]);
217
                    }
218
                }
219
            }
220
        }
221
    }
222
223
    /**
224
     * Loads sticky attributes from an internal file and populates them into the generator.
225
     */
226 3
    public function load(array $data): void
227
    {
228 3
        foreach ($data as $name => $value) {
229 3
            $method = 'set' . $name;
230 3
            if (method_exists($this, $method)) {
231 3
                $this->$method($value);
232
            }
233
        }
234
    }
235
236
    /**
237
     * Saves sticky attributes into an internal file.
238
     */
239
    public function saveStickyAttributes(): void
240
    {
241
        $stickyAttributes = $this->stickyAttributes();
242
        $stickyAttributes[] = 'template';
243
        $values = [];
244
        foreach ($stickyAttributes as $name) {
245
            $method = 'get' . $name;
246
            if (method_exists($this, $method)) {
247
                $values[$name] = $this->$method();
248
            }
249
        }
250
        $path = $this->getStickyDataFile();
251
        if (!mkdir($concurrentDirectory = dirname($path), 0755, true) && !is_dir($concurrentDirectory)) {
252
            throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
253
        }
254
        file_put_contents($path, Json::encode($values));
255
    }
256
257
    protected function getStickyDataFile(): string
258
    {
259
        return sprintf('%s/gii/%s.json', $this->aliases->get('@runtime'), str_replace('\\', '-', static::class));
260
    }
261
262
    /**
263
     * Saves the generated code into files.
264
     *
265
     * @param CodeFile[] $files the code files to be saved
266
     * @param string[] $results this parameter receives a value from this method indicating the log messages
267
     * generated while saving the code files.
268
     *
269
     * @throws ReflectionException
270
     * @throws InvalidConfigException
271
     *
272
     * @return bool whether files are successfully saved without any error.
273
     */
274
    public function save(array $files, array $answers, array &$results): bool
275
    {
276
        $results = ['Generating code using template "' . $this->getTemplatePath() . '"...'];
277
        $hasError = false;
278
        foreach ($files as $file) {
279
            $relativePath = $file->getRelativePath();
280
            if (!empty($answers[$file->getId()]) && $file->getOperation() !== CodeFile::OP_SKIP) {
281
                try {
282
                    $file->save();
283
                    $results[] = $file->getOperation() === CodeFile::OP_CREATE
284
                        ? " generated  $relativePath"
285
                        : " overwrote  $relativePath";
286
                } catch (Exception $e) {
287
                    $hasError = true;
288
                    $results[] = sprintf(
289
                        "   generating %s\n    - <span class=\"error\">%s</span>",
290
                        $relativePath,
291
                        $e->getMessage()
292
                    );
293
                }
294
            } else {
295
                $results[] = "   skipped    $relativePath";
296
            }
297
        }
298
        $results[] = 'done!';
299
300
        return !$hasError;
301
    }
302
303
    /**
304
     * @throws ReflectionException
305
     * @throws InvalidConfigException
306
     *
307
     * @return string the root path of the template files that are currently being used.
308
     */
309 2
    public function getTemplatePath(): string
310
    {
311 2
        if ($this->template === 'default') {
312 1
            return $this->defaultTemplate();
313
        }
314
315 1
        if (isset($this->templates[$this->template])) {
316 1
            return $this->templates[$this->template];
317
        }
318
319
        throw new InvalidConfigException("Unknown template: {$this->template}");
320
    }
321
322
    /**
323
     * Generates code using the specified code template and parameters.
324
     * Note that the code template will be used as a PHP file.
325
     *
326
     * @param string $template the code template file. This must be specified as a file path
327
     * relative to {@see getTemplatePath()}.
328
     * @param array $params list of parameters to be passed to the template file.
329
     *
330
     * @throws Throwable
331
     *
332
     * @return string the generated code
333
     */
334 2
    public function render(string $template, array $params = []): string
335
    {
336 2
        $file = sprintf('%s/%s.php', $this->aliases->get($this->getTemplatePath()), $template);
337
338 2
        $renderer = function (): void {
339 2
            extract(func_get_arg(1));
340
            /** @psalm-suppress UnresolvableInclude */
341 2
            require func_get_arg(0);
342
        };
343
344 2
        $obInitialLevel = ob_get_level();
345 2
        ob_start();
346 2
        ob_implicit_flush(false);
347
        try {
348
            /** @psalm-suppress PossiblyInvalidFunctionCall */
349 2
            $renderer->bindTo($this)($file, $params);
350 2
            return ob_get_clean();
351
        } catch (Throwable $e) {
352
            while (ob_get_level() > $obInitialLevel) {
353
                if (!@ob_end_clean()) {
354
                    ob_clean();
355
                }
356
            }
357
            throw $e;
358
        }
359
    }
360
361
    /**
362
     * Validates the template selection.
363
     * This method validates whether the user selects an existing template
364
     * and the template contains all required template files as specified in {@see requiredTemplates()}.
365
     *
366
     * @param string $value
367
     */
368 3
    public function validateTemplate(mixed $value, Callback $rule, ValidationContext $validationContext): Result
369
    {
370
        /** @var self $dataSet */
371 3
        $dataSet = $validationContext->getDataSet();
372 3
        $result = new Result();
373 3
        $templates = $dataSet->getTemplates();
374 3
        if ($templates === []) {
375 1
            return $result;
376
        }
377 2
        if (!isset($templates[$value])) {
378 1
            $result->addError('Invalid template selection.');
379
        } else {
380 1
            $templatePath = $templates[$value];
381 1
            foreach ($dataSet->requiredTemplates() as $template) {
382 1
                if (!is_file($dataSet->aliases->get($templatePath . '/' . $template))) {
383
                    $result->addError("Unable to find the required code template file '$template'.");
384
                }
385
            }
386
        }
387
388 2
        return $result;
389
    }
390
391
    /**
392
     * An inline validator that checks if the attribute value refers to an existing class name.
393
     *
394
     * @param string $value the attribute being validated
395
     */
396
    public function validateClass(string $value): Result
397
    {
398
        $result = new Result();
399
        if (!class_exists($value)) {
400
            $result->addError("Class '$value' does not exist or has syntax error.");
401
        }
402
403
        return $result;
404
    }
405
406
    /**
407
     * An inline validator that checks if the attribute value refers to a valid namespaced class name.
408
     * The validator will check if the directory containing the new class file exist or not.
409
     *
410
     * @param mixed $value being validated
411
     */
412 3
    public function validateNewClass(mixed $value, Callback $rule, ValidationContext $validationContext): Result
413
    {
414
        /** @var self $dataSet */
415 3
        $dataSet = $validationContext->getDataSet();
416 3
        $result = new Result();
417 3
        $class = ltrim($value, '\\');
418 3
        if (($pos = strrpos($class, '\\')) !== false) {
419
            $ns = substr($class, 0, $pos);
420
            try {
421
                $path = $dataSet->aliases->get('@' . str_replace('\\', '/', $ns));
422
                if (!is_dir($path)) {
423
                    $result->addError("Please make sure the directory containing this class exists: $path");
424
                }
425
            } catch (InvalidArgumentException) {
426
                $result->addError("The class namespace is invalid: $ns");
427
            }
428
        }
429
430 3
        return $result;
431
    }
432
433
    /**
434
     * @param string $value the attribute to be validated
435
     *
436
     * @return bool whether the value is a reserved PHP keyword.
437
     */
438
    public function isReservedKeyword(string $value): bool
439
    {
440
        static $keywords = [
441
            '__class__',
442
            '__dir__',
443
            '__file__',
444
            '__function__',
445
            '__line__',
446
            '__method__',
447
            '__namespace__',
448
            '__trait__',
449
            'abstract',
450
            'and',
451
            'array',
452
            'as',
453
            'break',
454
            'case',
455
            'catch',
456
            'callable',
457
            'cfunction',
458
            'class',
459
            'clone',
460
            'const',
461
            'continue',
462
            'declare',
463
            'default',
464
            'die',
465
            'do',
466
            'echo',
467
            'else',
468
            'elseif',
469
            'empty',
470
            'enddeclare',
471
            'endfor',
472
            'endforeach',
473
            'endif',
474
            'endswitch',
475
            'endwhile',
476
            'eval',
477
            'exception',
478
            'exit',
479
            'extends',
480
            'final',
481
            'finally',
482
            'for',
483
            'foreach',
484
            'function',
485
            'global',
486
            'goto',
487
            'if',
488
            'implements',
489
            'include',
490
            'include_once',
491
            'instanceof',
492
            'insteadof',
493
            'interface',
494
            'isset',
495
            'list',
496
            'namespace',
497
            'new',
498
            'old_function',
499
            'or',
500
            'parent',
501
            'php_user_filter',
502
            'print',
503
            'private',
504
            'protected',
505
            'public',
506
            'require',
507
            'require_once',
508
            'return',
509
            'static',
510
            'switch',
511
            'this',
512
            'throw',
513
            'trait',
514
            'try',
515
            'unset',
516
            'use',
517
            'var',
518
            'while',
519
            'xor',
520
            'fn',
521
        ];
522
523
        return in_array(strtolower($value), $keywords, true);
524
    }
525
526
    /**
527
     * Generates a string depending on enableI18N property
528
     *
529
     * @param string $string the text be generated
530
     * @param array $placeholders the placeholders to use by `Yii::t()`
531
     */
532
    public function generateString(string $string = '', array $placeholders = []): string
533
    {
534
        $string = addslashes($string);
535
        if (!empty($placeholders)) {
536
            $phKeys = array_map(
537
                static fn ($word) => '{' . $word . '}',
538
                array_keys($placeholders)
539
            );
540
            $phValues = array_values($placeholders);
541
            $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
542
        } else {
543
            // No placeholders, just the given string
544
            $str = "'" . $string . "'";
545
        }
546
        return $str;
547
    }
548
549 3
    public function getAttributeValue(string $attribute): mixed
550
    {
551 3
        if (!$this->hasAttribute($attribute)) {
552
            throw new InvalidArgumentException(sprintf('There is no "%s" in %s.', $attribute, $this->getName()));
553
        }
554 3
        $method = 'get' . $attribute;
555 3
        return $this->$method();
556
    }
557
558 3
    public function hasAttribute(string $attribute): bool
559
    {
560 3
        $method = 'get' . $attribute;
561 3
        return method_exists($this, $method);
562
    }
563
564 1
    public function getErrors(): array
565
    {
566 1
        return $this->errors;
567
    }
568
569 3
    public function hasErrors(): bool
570
    {
571 3
        return $this->errors !== [];
572
    }
573
574 3
    public function getTemplates(): array
575
    {
576 3
        return $this->templates;
577
    }
578
579 2
    public function setTemplates(array $templates): void
580
    {
581 2
        $this->templates = $templates;
582
    }
583
584 3
    public function getTemplate(): string
585
    {
586 3
        return $this->template;
587
    }
588
589 3
    public function setTemplate(string $template): void
590
    {
591 3
        $this->template = $template;
592
    }
593
594 2
    public function getDirectory(): ?string
595
    {
596 2
        return $this->directory;
597
    }
598
599
    public function setDirectory(string $directory): void
600
    {
601
        $this->directory = $directory;
602
    }
603
604
    public function getData(): array
605
    {
606
        return [
607
            'templates' => $this->templates,
608
            'template' => $this->template,
609
        ];
610
    }
611
}
612