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

AbstractGenerator::save()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 27
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 24.432

Importance

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