Passed
Pull Request — master (#52)
by Wilmer
02:40
created

AbstractGenerator::getData()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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