Passed
Push — master ( 85fdee...7ab89d )
by Alexander
08:44
created

AbstractGenerator::setDirectory()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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