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