Passed
Pull Request — master (#33)
by Rustam
02:23
created

AbstractGenerator::getTemplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
c 0
b 0
f 0
cc 1
crap 1
rs 10
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\ResultSet;
18
use Yiisoft\Validator\Rule\Callback;
19
use Yiisoft\Validator\Rule\Required;
20
use Yiisoft\Validator\Validator;
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 template files.
40
 *   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
    protected Aliases $aliases;
57
    protected View $view;
58
59 6
    public function __construct(Aliases $aliases, View $view)
60
    {
61 6
        $this->aliases = $aliases;
62 6
        $this->view = $view;
63 6
    }
64
65
    public function attributeLabels(): array
66
    {
67
        return [
68
            'enableI18N' => 'Enable I18N',
69
            'messageCategory' => 'Message Category',
70
        ];
71
    }
72
73
    /**
74
     * Returns a list of code template files that are required.
75
     * Derived classes usually should override this method if they require the existence of
76
     * certain template files.
77
     *
78
     * @return array list of code template files that are required. They should be file paths
79
     * relative to {@see getTemplatePath()}.
80
     */
81
    public function requiredTemplates(): array
82
    {
83
        return [];
84
    }
85
86
    /**
87
     * Returns the list of sticky attributes.
88
     * A sticky attribute will remember its value and will initialize the attribute with this value
89
     * when the generator is restarted.
90
     *
91
     * @return array list of sticky attributes
92
     */
93
    public function stickyAttributes(): array
94
    {
95
        return ['template', 'enableI18N', 'messageCategory'];
96
    }
97
98
    /**
99
     * Returns the list of hint messages.
100
     * The array keys are the attribute names, and the array values are the corresponding hint messages.
101
     * Hint messages will be displayed to end users when they are filling the form for the generator.
102
     *
103
     * @return array the list of hint messages
104
     */
105
    public function hints(): array
106
    {
107
        return [
108
            'enableI18N' => 'This indicates whether the generator should generate strings using <code>Yii::t()</code> method.
109
                Set this to <code>true</code> if you are planning to make your application translatable.',
110
            'messageCategory' => 'This is the category used by <code>Yii::t()</code> in case you enable I18N.',
111
        ];
112
    }
113
114
    /**
115
     * Returns the list of auto complete values.
116
     * The array keys are the attribute names, and the array values are the corresponding auto complete values.
117
     * Auto complete values can also be callable typed in order one want to make postponed data generation.
118
     *
119
     * @return array the list of auto complete values
120
     */
121
    public function autoCompleteData(): array
122
    {
123
        return [];
124
    }
125
126
    /**
127
     * Returns the message to be displayed when the newly generated code is saved successfully.
128
     * Child classes may override this method to customize the message.
129
     *
130
     * @return string the message to be displayed when the newly generated code is saved successfully.
131
     */
132
    public function successMessage(): string
133
    {
134
        return 'The code has been generated successfully.';
135
    }
136
137
    /**
138
     * Returns the view file for the input form of the generator.
139
     * The default implementation will return the "form.php" file under the directory
140
     * that contains the generator class file.
141
     *
142
     * @throws ReflectionException
143
     *
144
     * @return string the view file for the input form of the generator.
145
     */
146
    public function formView(): string
147
    {
148
        $class = new ReflectionClass($this);
149
150
        return dirname($class->getFileName()) . '/form.php';
151
    }
152
153
    /**
154
     * Returns the root path to the default code template files.
155
     * The default implementation will return the "templates" subdirectory of the
156
     * directory containing the generator class file.
157
     *
158
     * @throws ReflectionException
159
     *
160
     * @return string the root path to the default code template files.
161
     */
162 1
    private function defaultTemplate(): string
163
    {
164 1
        $class = new ReflectionClass($this);
165
166 1
        return dirname($class->getFileName()) . '/default';
167
    }
168
169
    public function getDescription(): string
170
    {
171
        return '';
172
    }
173
174 3
    final public function validate(): ResultSet
175
    {
176 3
        $results = (new Validator($this->rules()))->validate($this);
177 3
        foreach ($results as $attribute => $resultItem) {
178 3
            if (!$resultItem->isValid()) {
179 1
                $this->errors[$attribute] = $resultItem->getErrors();
180
            }
181
        }
182 3
        return $results;
183
    }
184
185
    /**
186
     * Child classes should override this method like the following so that the parent
187
     * rules are included:
188
     *
189
     * ~~~
190
     * return array_merge(parent::rules(), [
191
     *     ...rules for the child class...
192
     * ]);
193
     * ~~~
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 3
    }
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 InvalidConfigException
277
     * @throws ReflectionException
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 InvalidConfigException
317
     * @throws ReflectionException
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 ViewNotFoundException
343
     * @throws Throwable
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->render($template, $params, $this);
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 self $dataSet
361
     *
362
     * @return Result
363
     */
364 3
    public function validateTemplate(string $value, self $dataSet): Result
365
    {
366 3
        $result = new Result();
367 3
        $templates = $dataSet->getTemplates();
368 3
        if ($templates === []) {
369 1
            return $result;
370
        }
371 2
        if (!isset($templates[$value])) {
372 1
            $result->addError('Invalid template selection.');
373
        } else {
374 1
            $templatePath = $templates[$value];
375 1
            foreach ($dataSet->requiredTemplates() as $template) {
376 1
                if (!is_file($dataSet->aliases->get($templatePath . '/' . $template))) {
377
                    $result->addError("Unable to find the required code template file '$template'.");
378
                }
379
            }
380
        }
381
382 2
        return $result;
383
    }
384
385
    /**
386
     * An inline validator that checks if the attribute value refers to an existing class name.
387
     *
388
     * @param string $value the attribute being validated
389
     *
390
     * @return Result
391
     */
392
    public function validateClass(string $value): Result
393
    {
394
        $result = new Result();
395
        if (!class_exists($value)) {
396
            $result->addError("Class '$value' does not exist or has syntax error.");
397
        }
398
399
        return $result;
400
    }
401
402
    /**
403
     * An inline validator that checks if the attribute value refers to a valid namespaced class name.
404
     * The validator will check if the directory containing the new class file exist or not.
405
     *
406
     * @param string $value being validated
407
     * @param self $dataSet
408
     *
409
     * @return Result
410
     */
411 2
    public function validateNewClass(string $value, self $dataSet): Result
412
    {
413 2
        $result = new Result();
414 2
        $class = ltrim($value, '\\');
415 2
        if (($pos = strrpos($class, '\\')) !== false) {
416
            $ns = substr($class, 0, $pos);
417
            try {
418
                $path = $dataSet->aliases->get('@' . str_replace('\\', '/', $ns));
419
                if (!is_dir($path)) {
420
                    $result->addError("Please make sure the directory containing this class exists: $path");
421
                }
422
            } catch (InvalidArgumentException $exception) {
423
                $result->addError("The class namespace is invalid: $ns");
424
            }
425
        }
426
427 2
        return $result;
428
    }
429
430
    /**
431
     * @param string $value the attribute to be validated
432
     *
433
     * @return bool whether the value is a reserved PHP keyword.
434
     */
435
    public function isReservedKeyword(string $value): bool
436
    {
437
        static $keywords = [
438
            '__class__',
439
            '__dir__',
440
            '__file__',
441
            '__function__',
442
            '__line__',
443
            '__method__',
444
            '__namespace__',
445
            '__trait__',
446
            'abstract',
447
            'and',
448
            'array',
449
            'as',
450
            'break',
451
            'case',
452
            'catch',
453
            'callable',
454
            'cfunction',
455
            'class',
456
            'clone',
457
            'const',
458
            'continue',
459
            'declare',
460
            'default',
461
            'die',
462
            'do',
463
            'echo',
464
            'else',
465
            'elseif',
466
            'empty',
467
            'enddeclare',
468
            'endfor',
469
            'endforeach',
470
            'endif',
471
            'endswitch',
472
            'endwhile',
473
            'eval',
474
            'exception',
475
            'exit',
476
            'extends',
477
            'final',
478
            'finally',
479
            'for',
480
            'foreach',
481
            'function',
482
            'global',
483
            'goto',
484
            'if',
485
            'implements',
486
            'include',
487
            'include_once',
488
            'instanceof',
489
            'insteadof',
490
            'interface',
491
            'isset',
492
            'list',
493
            'namespace',
494
            'new',
495
            'old_function',
496
            'or',
497
            'parent',
498
            'php_user_filter',
499
            'print',
500
            'private',
501
            'protected',
502
            'public',
503
            'require',
504
            'require_once',
505
            'return',
506
            'static',
507
            'switch',
508
            'this',
509
            'throw',
510
            'trait',
511
            'try',
512
            'unset',
513
            'use',
514
            'var',
515
            'while',
516
            'xor',
517
            'fn',
518
        ];
519
520
        return in_array(strtolower($value), $keywords, true);
521
    }
522
523
    /**
524
     * Generates a string depending on enableI18N property
525
     *
526
     * @param string $string the text be generated
527
     * @param array $placeholders the placeholders to use by `Yii::t()`
528
     *
529
     * @return string
530
     */
531
    public function generateString(string $string = '', array $placeholders = []): string
532
    {
533
        $string = addslashes($string);
534
        if (!empty($placeholders)) {
535
            $phKeys = array_map(
536
                fn ($word) => '{' . $word . '}',
537
                array_keys($placeholders)
538
            );
539
            $phValues = array_values($placeholders);
540
            $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
541
        } else {
542
            // No placeholders, just the given string
543
            $str = "'" . $string . "'";
544
        }
545
        return $str;
546
    }
547
548
    /**
549
     * @param string $attribute
550
     *
551
     * @return mixed
552
     */
553 3
    public function getAttributeValue(string $attribute)
554
    {
555 3
        if (!$this->hasAttribute($attribute)) {
556
            throw new InvalidArgumentException(sprintf('There is no "%s" in %s.', $attribute, $this->getName()));
557
        }
558 3
        $method = 'get' . $attribute;
559 3
        return $this->$method();
560
    }
561
562 3
    public function hasAttribute(string $attribute): bool
563
    {
564 3
        $method = 'get' . $attribute;
565 3
        return method_exists($this, $method);
566
    }
567
568 1
    public function getErrors(): array
569
    {
570 1
        return $this->errors;
571
    }
572
573 3
    public function hasErrors(): bool
574
    {
575 3
        return $this->errors !== [];
576
    }
577
578 3
    public function getTemplates(): array
579
    {
580 3
        return $this->templates;
581
    }
582
583 2
    public function setTemplates(array $templates): void
584
    {
585 2
        $this->templates = $templates;
586 2
    }
587
588 3
    public function getTemplate(): string
589
    {
590 3
        return $this->template;
591
    }
592
593 3
    public function setTemplate(string $template): void
594
    {
595 3
        $this->template = $template;
596 3
    }
597
598 2
    public function getDirectory(): ?string
599
    {
600 2
        return $this->directory;
601
    }
602
603
    public function setDirectory(string $directory): void
604
    {
605
        $this->directory = $directory;
606
    }
607
}
608