Test Failed
Pull Request — master (#58)
by Alexander
04:54 queued 02:29
created

AbstractGenerator::save()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 6
eloc 19
nc 6
nop 3
dl 0
loc 27
ccs 0
cts 13
cp 0
crap 42
rs 9.0111
c 1
b 0
f 0
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
    public function getDescription(): string
134
    {
135
        return '';
136
    }
137
138
    public function validate(AbstractGeneratorCommand $command): Result
139
    {
140
        return $this->validator->validate($command);
141
    }
142
143 1
    /**
144
     * Loads sticky attributes from an internal file and populates them into the generator.
145 1
     *
146
     * @internal
147 1
     */
148
    public function loadStickyAttributes(): void
149
    {
150
        $stickyAttributes = $this->stickyAttributes();
151
        $path = $this->getStickyDataFile();
152
        if (is_file($path)) {
153
            $result = Json::decode(file_get_contents($path));
154
            if (is_array($result)) {
155 3
                foreach ($stickyAttributes as $name) {
156
                    $method = 'set' . $name;
157 3
                    if (array_key_exists($name, $result) && method_exists($this, $method)) {
158
                        $this->$method($result[$name]);
159 3
                    }
160 3
                }
161
            }
162
        }
163
    }
164
165
    /**
166
     * Loads sticky attributes from an internal file and populates them into the generator.
167
     */
168
    public function load(array $data): void
169
    {
170
        foreach ($data as $name => $value) {
171
            $method = 'set' . $name;
172
            if (method_exists($this, $method)) {
173
                $this->$method($value);
174
            }
175 3
        }
176
    }
177
178
    /**
179 3
     * Saves sticky attributes into an internal file.
180 3
     */
181
    public function saveStickyAttributes(): void
182
    {
183
        $stickyAttributes = $this->stickyAttributes();
184
        $stickyAttributes[] = 'template';
185
        $values = [];
186
        foreach ($stickyAttributes as $name) {
187
            $method = 'get' . $name;
188
            if (method_exists($this, $method)) {
189
                $values[$name] = $this->$method();
190
            }
191
        }
192
        $path = $this->getStickyDataFile();
193
        if (!mkdir($concurrentDirectory = dirname($path), 0755, true) && !is_dir($concurrentDirectory)) {
194
            throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory));
195
        }
196
        file_put_contents($path, Json::encode($values));
197
    }
198
199
    protected function getStickyDataFile(): string
200
    {
201
        return sprintf('%s/gii/%s.json', $this->aliases->get('@runtime'), str_replace('\\', '-', static::class));
202
    }
203
204
    /**
205
     * Saves the generated code into files.
206
     *
207
     * @param CodeFile[] $files the code files to be saved
208
     * @param string[] $results this parameter receives a value from this method indicating the log messages
209
     * generated while saving the code files.
210 3
     *
211
     * @throws ReflectionException
212 3
     * @throws InvalidConfigException
213 3
     *
214 3
     * @return bool whether files are successfully saved without any error.
215 3
     */
216
    public function save(array $files, array $answers, array &$results): bool
217
    {
218
//        $results = ['Generating code using template "' . $this->getTemplatePath() . '"...'];
219
        $hasError = false;
220
        foreach ($files as $file) {
221
            $relativePath = $file->getRelativePath();
222
            if (!empty($answers[$file->getId()]) && $file->getOperation() !== CodeFile::OP_SKIP) {
223
                try {
224
                    $file->save();
225
                    $results[] = $file->getOperation() === CodeFile::OP_CREATE
226
                        ? " generated  $relativePath"
227
                        : " overwrote  $relativePath";
228
                } catch (Exception $e) {
229
                    $hasError = true;
230
                    $results[] = sprintf(
231
                        "   generating %s\n    - <span class=\"error\">%s</span>",
232
                        $relativePath,
233
                        $e->getMessage()
234
                    );
235
                }
236
            } else {
237
                $results[] = "   skipped    $relativePath";
238
            }
239
        }
240
        $results[] = 'done!';
241
242
        return !$hasError;
243
    }
244
245
    /**
246
     * @throws ReflectionException
247
     * @throws InvalidConfigException
248
     *
249
     * @return string the root path of the template files that are currently being used.
250
     */
251
    public function getTemplatePath(AbstractGeneratorCommand $command): string
252
    {
253
        $template = $command->getTemplate();
254
255
        if ($template === 'default') {
256
            return $this->defaultTemplate();
257
        }
258
259
        if (isset($this->parametersProvider->getTemplates()[$template])) {
260
            return $this->parametersProvider->getTemplates()[$template];
261
        }
262
263
        throw new InvalidConfigException("Unknown template: {$template}");
264
    }
265
266
    /**
267
     * Generates code using the specified code template and parameters.
268
     * Note that the code template will be used as a PHP file.
269
     *
270
     * @param string $template the code template file. This must be specified as a file path
271
     * relative to {@see getTemplatePath()}.
272
     * @param array $params list of parameters to be passed to the template file.
273
     *
274
     * @throws Throwable
275
     *
276
     * @return string the generated code
277
     */
278
    protected function render(AbstractGeneratorCommand $command, string $template, array $params = []): string
279
    {
280
        $file = sprintf(
281
            '%s/%s.php',
282
            $this->aliases->get($this->getTemplatePath($command)),
283
            $template
284
        );
285
286
        $renderer = function (): void {
287
            extract(func_get_arg(1));
288
            /** @psalm-suppress UnresolvableInclude */
289
            require func_get_arg(0);
290
        };
291
292
        $obInitialLevel = ob_get_level();
293 2
        ob_start();
294
        ob_implicit_flush(false);
295 2
        try {
296 1
            /** @psalm-suppress PossiblyInvalidFunctionCall */
297
            $renderer->bindTo($this)($file, array_merge($params, ['command' => $command]));
298
            return ob_get_clean();
299 1
        } catch (Throwable $e) {
300 1
            while (ob_get_level() > $obInitialLevel) {
301
                if (!@ob_end_clean()) {
302
                    ob_clean();
303
                }
304
            }
305
            throw $e;
306
        }
307
    }
308
309
    /**
310
     * An inline validator that checks if the attribute value refers to an existing class name.
311
     *
312
     * @param string $value the attribute being validated
313
     */
314
    public function validateClass(string $value): Result
315
    {
316
        $result = new Result();
317
        if (!class_exists($value)) {
318 2
            $result->addError("Class '$value' does not exist or has syntax error.");
319
        }
320 2
321
        return $result;
322 2
    }
323 2
324
    /**
325 2
     * @param string $value the attribute to be validated
326
     *
327
     * @return bool whether the value is a reserved PHP keyword.
328 2
     */
329 2
    public function isReservedKeyword(string $value): bool
330 2
    {
331
        static $keywords = [
332
            '__class__',
333 2
            '__dir__',
334 2
            '__file__',
335
            '__function__',
336
            '__line__',
337
            '__method__',
338
            '__namespace__',
339
            '__trait__',
340
            'abstract',
341
            'and',
342
            'array',
343
            'as',
344
            'break',
345
            'case',
346
            'catch',
347
            'callable',
348
            'cfunction',
349
            'class',
350
            'clone',
351
            'const',
352 3
            'continue',
353
            'declare',
354
            'default',
355 3
            'die',
356 3
            'do',
357 3
            'echo',
358 3
            'else',
359 1
            'elseif',
360
            'empty',
361 2
            'enddeclare',
362 1
            'endfor',
363
            'endforeach',
364 1
            'endif',
365 1
            'endswitch',
366 1
            'endwhile',
367
            'eval',
368
            'exception',
369
            'exit',
370
            'extends',
371
            'final',
372 2
            'finally',
373
            'for',
374
            'foreach',
375
            'function',
376
            'global',
377
            'goto',
378
            'if',
379
            'implements',
380
            'include',
381
            'include_once',
382
            'instanceof',
383
            'insteadof',
384
            'interface',
385
            'isset',
386
            'list',
387
            'namespace',
388
            'new',
389
            'old_function',
390
            'or',
391
            'parent',
392
            'php_user_filter',
393
            'print',
394
            'private',
395
            'protected',
396 3
            'public',
397
            'require',
398
            'require_once',
399 3
            'return',
400 3
            'static',
401 3
            'switch',
402 3
            'this',
403
            'throw',
404
            'trait',
405
            'try',
406
            'unset',
407
            'use',
408
            'var',
409
            'while',
410
            'xor',
411
            'fn',
412
        ];
413
414 3
        return in_array(strtolower($value), $keywords, true);
415
    }
416
417
    /**
418
     * Generates a string depending on enableI18N property
419
     *
420
     * @param string $string the text be generated
421
     * @param array $placeholders the placeholders to use by `Yii::t()`
422
     */
423
    public function generateString(string $string = '', array $placeholders = []): string
424
    {
425
        $string = addslashes($string);
426
        if (!empty($placeholders)) {
427
            $phKeys = array_map(
428
                static fn ($word) => '{' . $word . '}',
429
                array_keys($placeholders)
430
            );
431
            $phValues = array_values($placeholders);
432
            $str = "'" . str_replace($phKeys, $phValues, $string) . "'";
433
        } else {
434
            // No placeholders, just the given string
435
            $str = "'" . $string . "'";
436
        }
437
        return $str;
438
    }
439
440
    public function getAttributeValue(string $attribute): mixed
441
    {
442
        if (!$this->hasAttribute($attribute)) {
443
            throw new InvalidArgumentException(sprintf('There is no "%s" in %s.', $attribute, $this->getName()));
444
        }
445
        $method = 'get' . $attribute;
446
        return $this->$method();
447
    }
448
449
    public function hasAttribute(string $attribute): bool
450
    {
451
        $method = 'get' . $attribute;
452
        return method_exists($this, $method);
453
    }
454
455
    public function getErrors(): array
456
    {
457
        return $this->errors;
458
    }
459
460
    public function hasErrors(): bool
461
    {
462
        return $this->errors !== [];
463
    }
464
465
    public function getDirectory(): string
466
    {
467
        return $this->directory;
468
    }
469
470
    public function getData(): array
471
    {
472
        return [
473
            //            'templates' => $this->templates,
474
            'template' => $this->template,
475
        ];
476
    }
477
}
478