Test Failed
Pull Request — master (#58)
by Dmitriy
02:33
created

AbstractGenerator::hasErrors()   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
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
ccs 0
cts 0
cp 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\Required;
18
use Yiisoft\Validator\ValidatorInterface;
19
use Yiisoft\Yii\Gii\CodeFile;
20
use Yiisoft\Yii\Gii\Exception\InvalidConfigException;
21
use Yiisoft\Yii\Gii\GeneratorInterface;
22
use Yiisoft\Yii\Gii\GiiParametersProvider;
23
24
/**
25
 * This is the base class for all generator classes.
26
 *
27
 * A generator instance is responsible for taking user inputs, validating them,
28
 * and using them to generate the corresponding code based on a set of code template files.
29
 *
30
 * A generator class typically needs to implement the following methods:
31
 *
32
 * - {@see GeneratorInterface::getName()}: returns the name of the generator
33
 * - {@see GeneratorInterface::getDescription()}: returns the detailed description of the generator
34
 * - {@see GeneratorInterface::validate()}: returns generator validation result
35
 * - {@see GeneratorInterface::generate()}: generates the code based on the current user input and the specified code
36
 * template files. This is the place where main code generation code resides.
37
 */
38
abstract class AbstractGenerator implements GeneratorInterface, DataSetInterface
39
{
40
    private array $errors = [];
41
42
    /**
43
     * @var string the name of the code template that the user has selected.
44
     * The value of this property is internally managed by this class.
45
     */
46
    private string $template = 'default';
47
    private string $directory = 'src/Controller';
48
49
    public function __construct(
50
        protected Aliases $aliases,
51
        protected ValidatorInterface $validator,
52
        protected         GiiParametersProvider $parametersProvider,
53
    ) {
54
    }
55
56 7
    public function attributeLabels(): array
57
    {
58
        return [
59
            'enableI18N' => 'Enable I18N',
60
            'messageCategory' => 'Message Category',
61
        ];
62
    }
63
64
    /**
65
     * Returns a list of code template files that are required.
66
     * Derived classes usually should override this method if they require the existence of
67
     * certain template files.
68
     *
69
     * @return array list of code template files that are required. They should be file paths
70
     * relative to {@see getTemplatePath()}.
71
     */
72
    public function requiredTemplates(): array
73
    {
74
        return [];
75
    }
76
77
    /**
78
     * Returns the list of sticky attributes.
79
     * A sticky attribute will remember its value and will initialize the attribute with this value
80
     * when the generator is restarted.
81
     *
82
     * @return array list of sticky attributes
83
     */
84
    public function stickyAttributes(): array
85
    {
86
        return ['template', 'enableI18N', 'messageCategory'];
87
    }
88
89
    /**
90
     * Returns the list of hint messages.
91
     * The array keys are the attribute names, and the array values are the corresponding hint messages.
92
     * Hint messages will be displayed to end users when they are filling the form for the generator.
93
     *
94
     * @return array the list of hint messages
95
     */
96
    public function hints(): array
97
    {
98
        return [
99
            'enableI18N' => 'This indicates whether the generator should generate strings using <code>Yii::t()</code> method.
100
                Set this to <code>true</code> if you are planning to make your application translatable.',
101
            'messageCategory' => 'This is the category used by <code>Yii::t()</code> in case you enable I18N.',
102
        ];
103
    }
104
105
    /**
106
     * Returns the list of auto complete values.
107
     * The array keys are the attribute names, and the array values are the corresponding auto complete values.
108
     * Auto complete values can also be callable typed in order one want to make postponed data generation.
109
     *
110
     * @return array the list of auto complete values
111
     */
112
    public function autoCompleteData(): array
113
    {
114
        return [];
115
    }
116
117
    /**
118
     * Returns the root path to the default code template files.
119
     * The default implementation will return the "templates" subdirectory of the
120
     * directory containing the generator class file.
121
     *
122
     * @throws ReflectionException
123
     *
124
     * @return string the root path to the default code template files.
125
     */
126
    private function defaultTemplate(): string
127
    {
128
        $class = new ReflectionClass($this);
129
130
        return dirname($class->getFileName()) . '/default';
131
132
    }
133
134
    public function getDescription(): string
135
    {
136
        return '';
137
    }
138
139
    public function validate(AbstractGeneratorCommand $command): Result
140
    {
141
        return $this->validator->validate($command);
142
    }
143 1
144
    /**
145 1
     * Loads sticky attributes from an internal file and populates them into the generator.
146
     *
147 1
     * @internal
148
     */
149
    public function loadStickyAttributes(): void
150
    {
151
        $stickyAttributes = $this->stickyAttributes();
152
        $path = $this->getStickyDataFile();
153
        if (is_file($path)) {
154
            $result = Json::decode(file_get_contents($path));
155 3
            if (is_array($result)) {
156
                foreach ($stickyAttributes as $name) {
157 3
                    $method = 'set' . $name;
158
                    if (array_key_exists($name, $result) && method_exists($this, $method)) {
159 3
                        $this->$method($result[$name]);
160 3
                    }
161
                }
162
            }
163
        }
164
    }
165
166
    /**
167
     * Loads sticky attributes from an internal file and populates them into the generator.
168
     */
169
    public function load(array $data): void
170
    {
171
        foreach ($data as $name => $value) {
172
            $method = 'set' . $name;
173
            if (method_exists($this, $method)) {
174
                $this->$method($value);
175 3
            }
176
        }
177
    }
178
179 3
    /**
180 3
     * Saves sticky attributes into an internal file.
181
     */
182
    public function saveStickyAttributes(): void
183
    {
184
        $stickyAttributes = $this->stickyAttributes();
185
        $stickyAttributes[] = 'template';
186
        $values = [];
187
        foreach ($stickyAttributes as $name) {
188
            $method = 'get' . $name;
189
            if (method_exists($this, $method)) {
190
                $values[$name] = $this->$method();
191
            }
192
        }
193
        $path = $this->getStickyDataFile();
194
        if (!mkdir($concurrentDirectory = dirname($path), 0755, true) && !is_dir($concurrentDirectory)) {
195
            throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
196
        }
197
        file_put_contents($path, Json::encode($values));
198
    }
199
200
    protected function getStickyDataFile(): string
201
    {
202
        return sprintf('%s/gii/%s.json', $this->aliases->get('@runtime'), str_replace('\\', '-', static::class));
203
    }
204
205
    /**
206
     * Saves the generated code into files.
207
     *
208
     * @param CodeFile[] $files the code files to be saved
209
     * @param string[] $results this parameter receives a value from this method indicating the log messages
210 3
     * generated while saving the code files.
211
     *
212 3
     * @throws ReflectionException
213 3
     * @throws InvalidConfigException
214 3
     *
215 3
     * @return bool whether files are successfully saved without any error.
216
     */
217
    public function save(array $files, array $answers, array &$results): bool
218
    {
219
//        $results = ['Generating code using template "' . $this->getTemplatePath() . '"...'];
220
        $hasError = false;
221
        foreach ($files as $file) {
222
            $relativePath = $file->getRelativePath();
223
            if (!empty($answers[$file->getId()]) && $file->getOperation() !== CodeFile::OP_SKIP) {
224
                try {
225
                    $file->save();
226
                    $results[] = $file->getOperation() === CodeFile::OP_CREATE
227
                        ? " generated  $relativePath"
228
                        : " overwrote  $relativePath";
229
                } catch (Exception $e) {
230
                    $hasError = true;
231
                    $results[] = sprintf(
232
                        "   generating %s\n    - <span class=\"error\">%s</span>",
233
                        $relativePath,
234
                        $e->getMessage()
235
                    );
236
                }
237
            } else {
238
                $results[] = "   skipped    $relativePath";
239
            }
240
        }
241
        $results[] = 'done!';
242
243
        return !$hasError;
244
    }
245
246
    /**
247
     * @throws ReflectionException
248
     * @throws InvalidConfigException
249
     *
250
     * @return string the root path of the template files that are currently being used.
251
     */
252
    public function getTemplatePath(AbstractGeneratorCommand $command): string
253
    {
254
        $template = $command->getTemplate();
255
256
        if ($template === 'default') {
257
            return $this->defaultTemplate();
258
        }
259
260
        if (isset($this->parametersProvider->getTemplates()[$template])) {
261
            return $this->parametersProvider->getTemplates()[$template];
262
        }
263
264
        throw new InvalidConfigException("Unknown template: {$template}");
265
    }
266
267
    /**
268
     * Generates code using the specified code template and parameters.
269
     * Note that the code template will be used as a PHP file.
270
     *
271
     * @param string $template the code template file. This must be specified as a file path
272
     * relative to {@see getTemplatePath()}.
273
     * @param array $params list of parameters to be passed to the template file.
274
     *
275
     * @throws Throwable
276
     *
277
     * @return string the generated code
278
     */
279
    protected function render(AbstractGeneratorCommand $command, string $template, array $params = []): string
280
    {
281
        $file = sprintf(
282
            '%s/%s.php',
283
            $this->aliases->get($this->getTemplatePath($command)),
284
            $template
285
        );
286
287
        $renderer = function (): void {
288
            extract(func_get_arg(1));
289
            /** @psalm-suppress UnresolvableInclude */
290
            require func_get_arg(0);
291
        };
292
293 2
        $obInitialLevel = ob_get_level();
294
        ob_start();
295 2
        ob_implicit_flush(false);
296 1
        try {
297
            /** @psalm-suppress PossiblyInvalidFunctionCall */
298
            $renderer->bindTo($this)($file, array_merge($params, ['command'=>$command]));
299 1
            return ob_get_clean();
300 1
        } catch (Throwable $e) {
301
            while (ob_get_level() > $obInitialLevel) {
302
                if (!@ob_end_clean()) {
303
                    ob_clean();
304
                }
305
            }
306
            throw $e;
307
        }
308
    }
309
310
    /**
311
     * An inline validator that checks if the attribute value refers to an existing class name.
312
     *
313
     * @param string $value the attribute being validated
314
     */
315
    public function validateClass(string $value): Result
316
    {
317
        $result = new Result();
318 2
        if (!class_exists($value)) {
319
            $result->addError("Class '$value' does not exist or has syntax error.");
320 2
        }
321
322 2
        return $result;
323 2
    }
324
325 2
    /**
326
     * @param string $value the attribute to be validated
327
     *
328 2
     * @return bool whether the value is a reserved PHP keyword.
329 2
     */
330 2
    public function isReservedKeyword(string $value): bool
331
    {
332
        static $keywords = [
333 2
            '__class__',
334 2
            '__dir__',
335
            '__file__',
336
            '__function__',
337
            '__line__',
338
            '__method__',
339
            '__namespace__',
340
            '__trait__',
341
            'abstract',
342
            'and',
343
            'array',
344
            'as',
345
            'break',
346
            'case',
347
            'catch',
348
            'callable',
349
            'cfunction',
350
            'class',
351
            'clone',
352 3
            'const',
353
            'continue',
354
            'declare',
355 3
            'default',
356 3
            'die',
357 3
            'do',
358 3
            'echo',
359 1
            'else',
360
            'elseif',
361 2
            'empty',
362 1
            'enddeclare',
363
            'endfor',
364 1
            'endforeach',
365 1
            'endif',
366 1
            'endswitch',
367
            'endwhile',
368
            'eval',
369
            'exception',
370
            'exit',
371
            'extends',
372 2
            'final',
373
            'finally',
374
            'for',
375
            'foreach',
376
            'function',
377
            'global',
378
            'goto',
379
            'if',
380
            'implements',
381
            'include',
382
            'include_once',
383
            'instanceof',
384
            'insteadof',
385
            'interface',
386
            'isset',
387
            'list',
388
            'namespace',
389
            'new',
390
            'old_function',
391
            'or',
392
            'parent',
393
            'php_user_filter',
394
            'print',
395
            'private',
396 3
            'protected',
397
            'public',
398
            'require',
399 3
            'require_once',
400 3
            'return',
401 3
            'static',
402 3
            'switch',
403
            'this',
404
            'throw',
405
            'trait',
406
            'try',
407
            'unset',
408
            'use',
409
            'var',
410
            'while',
411
            'xor',
412
            'fn',
413
        ];
414 3
415
        return in_array(strtolower($value), $keywords, true);
416
    }
417
418
    /**
419
     * Generates a string depending on enableI18N property
420
     *
421
     * @param string $string the text be generated
422
     * @param array $placeholders the placeholders to use by `Yii::t()`
423
     */
424
    public function generateString(string $string = '', array $placeholders = []): string
425
    {
426
        $string = addslashes($string);
427
        if (!empty($placeholders)) {
428
            $phKeys = array_map(
429
                static fn ($word) => '{' . $word . '}',
430
                array_keys($placeholders)
431
            );
432
            $phValues = array_values($placeholders);
433
            $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
434
        } else {
435
            // No placeholders, just the given string
436
            $str = "'" . $string . "'";
437
        }
438
        return $str;
439
    }
440
441
    public function getAttributeValue(string $attribute): mixed
442
    {
443
        if (!$this->hasAttribute($attribute)) {
444
            throw new InvalidArgumentException(sprintf('There is no "%s" in %s.', $attribute, $this->getName()));
445
        }
446
        $method = 'get' . $attribute;
447
        return $this->$method();
448
    }
449
450
    public function hasAttribute(string $attribute): bool
451
    {
452
        $method = 'get' . $attribute;
453
        return method_exists($this, $method);
454
    }
455
456
    public function getErrors(): array
457
    {
458
        return $this->errors;
459
    }
460
461
    public function hasErrors(): bool
462
    {
463
        return $this->errors !== [];
464
    }
465
466
    public function getDirectory(): string
467
    {
468
        return $this->directory;
469
    }
470
471
    public function getData(): array
472
    {
473
        return [
474
            //            'templates' => $this->templates,
475
            'template' => $this->template,
476
        ];
477
    }
478
}
479