ci4

Форк
0
/
GeneratorTrait.php 
527 строк · 13.6 Кб
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\CLI;
15

16
use Config\Generators;
17
use Throwable;
18

19
/**
20
 * GeneratorTrait contains a collection of methods
21
 * to build the commands that generates a file.
22
 */
23
trait GeneratorTrait
24
{
25
    /**
26
     * Component Name
27
     *
28
     * @var string
29
     */
30
    protected $component;
31

32
    /**
33
     * File directory
34
     *
35
     * @var string
36
     */
37
    protected $directory;
38

39
    /**
40
     * (Optional) View template path
41
     *
42
     * We use special namespaced paths like:
43
     *      `CodeIgniter\Commands\Generators\Views\cell.tpl.php`.
44
     */
45
    protected ?string $templatePath = null;
46

47
    /**
48
     * View template name for fallback
49
     *
50
     * @var string
51
     */
52
    protected $template;
53

54
    /**
55
     * Language string key for required class names.
56
     *
57
     * @var string
58
     */
59
    protected $classNameLang = '';
60

61
    /**
62
     * Namespace to use for class.
63
     * Leave null to use the default namespace.
64
     */
65
    protected ?string $namespace = null;
66

67
    /**
68
     * Whether to require class name.
69
     *
70
     * @internal
71
     *
72
     * @var bool
73
     */
74
    private $hasClassName = true;
75

76
    /**
77
     * Whether to sort class imports.
78
     *
79
     * @internal
80
     *
81
     * @var bool
82
     */
83
    private $sortImports = true;
84

85
    /**
86
     * Whether the `--suffix` option has any effect.
87
     *
88
     * @internal
89
     *
90
     * @var bool
91
     */
92
    private $enabledSuffixing = true;
93

94
    /**
95
     * The params array for easy access by other methods.
96
     *
97
     * @internal
98
     *
99
     * @var array<int|string, string|null>
100
     */
101
    private $params = [];
102

103
    /**
104
     * Execute the command.
105
     *
106
     * @param array<int|string, string|null> $params
107
     *
108
     * @deprecated use generateClass() instead
109
     */
110
    protected function execute(array $params): void
111
    {
112
        $this->generateClass($params);
113
    }
114

115
    /**
116
     * Generates a class file from an existing template.
117
     *
118
     * @param array<int|string, string|null> $params
119
     */
120
    protected function generateClass(array $params): void
121
    {
122
        $this->params = $params;
123

124
        // Get the fully qualified class name from the input.
125
        $class = $this->qualifyClassName();
126

127
        // Get the file path from class name.
128
        $target = $this->buildPath($class);
129

130
        // Check if path is empty.
131
        if ($target === '') {
132
            return;
133
        }
134

135
        $this->generateFile($target, $this->buildContent($class));
136
    }
137

138
    /**
139
     * Generate a view file from an existing template.
140
     *
141
     * @param string                         $view   namespaced view name that is generated
142
     * @param array<int|string, string|null> $params
143
     */
144
    protected function generateView(string $view, array $params): void
145
    {
146
        $this->params = $params;
147

148
        $target = $this->buildPath($view);
149

150
        // Check if path is empty.
151
        if ($target === '') {
152
            return;
153
        }
154

155
        $this->generateFile($target, $this->buildContent($view));
156
    }
157

158
    /**
159
     * Handles writing the file to disk, and all of the safety checks around that.
160
     *
161
     * @param string $target file path
162
     */
163
    private function generateFile(string $target, string $content): void
164
    {
165
        if ($this->getOption('namespace') === 'CodeIgniter') {
166
            // @codeCoverageIgnoreStart
167
            CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');
168
            CLI::newLine();
169

170
            if (
171
                CLI::prompt(
172
                    'Are you sure you want to continue?',
173
                    ['y', 'n'],
174
                    'required'
175
                ) === 'n'
176
            ) {
177
                CLI::newLine();
178
                CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');
179
                CLI::newLine();
180

181
                return;
182
            }
183

184
            CLI::newLine();
185
            // @codeCoverageIgnoreEnd
186
        }
187

188
        $isFile = is_file($target);
189

190
        // Overwriting files unknowingly is a serious annoyance, So we'll check if
191
        // we are duplicating things, If 'force' option is not supplied, we bail.
192
        if (! $this->getOption('force') && $isFile) {
193
            CLI::error(
194
                lang('CLI.generator.fileExist', [clean_path($target)]),
195
                'light_gray',
196
                'red'
197
            );
198
            CLI::newLine();
199

200
            return;
201
        }
202

203
        // Check if the directory to save the file is existing.
204
        $dir = dirname($target);
205

206
        if (! is_dir($dir)) {
207
            mkdir($dir, 0755, true);
208
        }
209

210
        helper('filesystem');
211

212
        // Build the class based on the details we have, We'll be getting our file
213
        // contents from the template, and then we'll do the necessary replacements.
214
        if (! write_file($target, $content)) {
215
            // @codeCoverageIgnoreStart
216
            CLI::error(
217
                lang('CLI.generator.fileError', [clean_path($target)]),
218
                'light_gray',
219
                'red'
220
            );
221
            CLI::newLine();
222

223
            return;
224
            // @codeCoverageIgnoreEnd
225
        }
226

227
        if ($this->getOption('force') && $isFile) {
228
            CLI::write(
229
                lang('CLI.generator.fileOverwrite', [clean_path($target)]),
230
                'yellow'
231
            );
232
            CLI::newLine();
233

234
            return;
235
        }
236

237
        CLI::write(
238
            lang('CLI.generator.fileCreate', [clean_path($target)]),
239
            'green'
240
        );
241
        CLI::newLine();
242
    }
243

244
    /**
245
     * Prepare options and do the necessary replacements.
246
     *
247
     * @param string $class namespaced classname or namespaced view.
248
     *
249
     * @return string generated file content
250
     */
251
    protected function prepare(string $class): string
252
    {
253
        return $this->parseTemplate($class);
254
    }
255

256
    /**
257
     * Change file basename before saving.
258
     *
259
     * Useful for components where the file name has a date.
260
     */
261
    protected function basename(string $filename): string
262
    {
263
        return basename($filename);
264
    }
265

266
    /**
267
     * Parses the class name and checks if it is already qualified.
268
     */
269
    protected function qualifyClassName(): string
270
    {
271
        $class = $this->normalizeInputClassName();
272

273
        // Gets the namespace from input. Don't forget the ending backslash!
274
        $namespace = $this->getNamespace() . '\\';
275

276
        if (str_starts_with($class, $namespace)) {
277
            return $class; // @codeCoverageIgnore
278
        }
279

280
        $directoryString = ($this->directory !== null) ? $this->directory . '\\' : '';
281

282
        return $namespace . $directoryString . str_replace('/', '\\', $class);
283
    }
284

285
    /**
286
     * Normalize input classname.
287
     */
288
    private function normalizeInputClassName(): string
289
    {
290
        // Gets the class name from input.
291
        $class = $this->params[0] ?? CLI::getSegment(2);
292

293
        if ($class === null && $this->hasClassName) {
294
            // @codeCoverageIgnoreStart
295
            $nameLang = $this->classNameLang !== ''
296
                ? $this->classNameLang
297
                : 'CLI.generator.className.default';
298
            $class = CLI::prompt(lang($nameLang), null, 'required');
299
            CLI::newLine();
300
            // @codeCoverageIgnoreEnd
301
        }
302

303
        helper('inflector');
304

305
        $component = singular($this->component);
306

307
        /**
308
         * @see https://regex101.com/r/a5KNCR/2
309
         */
310
        $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)$/i', $component);
311

312
        if (preg_match($pattern, $class, $matches) === 1) {
313
            $class = $matches[1] . ucfirst($matches[2]);
314
        }
315

316
        if (
317
            $this->enabledSuffixing && $this->getOption('suffix')
318
            && preg_match($pattern, $class) !== 1
319
        ) {
320
            $class .= ucfirst($component);
321
        }
322

323
        // Trims input, normalize separators, and ensure that all paths are in Pascalcase.
324
        return ltrim(
325
            implode(
326
                '\\',
327
                array_map(
328
                    pascalize(...),
329
                    explode('\\', str_replace('/', '\\', trim($class)))
330
                )
331
            ),
332
            '\\/'
333
        );
334
    }
335

336
    /**
337
     * Gets the generator view as defined in the `Config\Generators::$views`,
338
     * with fallback to `$template` when the defined view does not exist.
339
     *
340
     * @param array<string, mixed> $data
341
     */
342
    protected function renderTemplate(array $data = []): string
343
    {
344
        try {
345
            $template = $this->templatePath ?? config(Generators::class)->views[$this->name];
346

347
            return view($template, $data, ['debug' => false]);
348
        } catch (Throwable $e) {
349
            log_message('error', (string) $e);
350

351
            return view(
352
                "CodeIgniter\\Commands\\Generators\\Views\\{$this->template}",
353
                $data,
354
                ['debug' => false]
355
            );
356
        }
357
    }
358

359
    /**
360
     * Performs pseudo-variables contained within view file.
361
     *
362
     * @param string                          $class   namespaced classname or namespaced view.
363
     * @param list<string>                    $search
364
     * @param list<string>                    $replace
365
     * @param array<string, bool|string|null> $data
366
     *
367
     * @return string generated file content
368
     */
369
    protected function parseTemplate(
370
        string $class,
371
        array $search = [],
372
        array $replace = [],
373
        array $data = []
374
    ): string {
375
        // Retrieves the namespace part from the fully qualified class name.
376
        $namespace = trim(
377
            implode(
378
                '\\',
379
                array_slice(explode('\\', $class), 0, -1)
380
            ),
381
            '\\'
382
        );
383
        $search[]  = '<@php';
384
        $search[]  = '{namespace}';
385
        $search[]  = '{class}';
386
        $replace[] = '<?php';
387
        $replace[] = $namespace;
388
        $replace[] = str_replace($namespace . '\\', '', $class);
389

390
        return str_replace($search, $replace, $this->renderTemplate($data));
391
    }
392

393
    /**
394
     * Builds the contents for class being generated, doing all
395
     * the replacements necessary, and alphabetically sorts the
396
     * imports for a given template.
397
     */
398
    protected function buildContent(string $class): string
399
    {
400
        $template = $this->prepare($class);
401

402
        if (
403
            $this->sortImports
404
            && preg_match(
405
                '/(?P<imports>(?:^use [^;]+;$\n?)+)/m',
406
                $template,
407
                $match
408
            )
409
        ) {
410
            $imports = explode("\n", trim($match['imports']));
411
            sort($imports);
412

413
            return str_replace(trim($match['imports']), implode("\n", $imports), $template);
414
        }
415

416
        return $template;
417
    }
418

419
    /**
420
     * Builds the file path from the class name.
421
     *
422
     * @param string $class namespaced classname or namespaced view.
423
     */
424
    protected function buildPath(string $class): string
425
    {
426
        $namespace = $this->getNamespace();
427

428
        // Check if the namespace is actually defined and we are not just typing gibberish.
429
        $base = service('autoloader')->getNamespace($namespace);
430

431
        if (! $base = reset($base)) {
432
            CLI::error(
433
                lang('CLI.namespaceNotDefined', [$namespace]),
434
                'light_gray',
435
                'red'
436
            );
437
            CLI::newLine();
438

439
            return '';
440
        }
441

442
        $realpath = realpath($base);
443
        $base     = ($realpath !== false) ? $realpath : $base;
444

445
        $file = $base . DIRECTORY_SEPARATOR
446
            . str_replace(
447
                '\\',
448
                DIRECTORY_SEPARATOR,
449
                trim(str_replace($namespace . '\\', '', $class), '\\')
450
            ) . '.php';
451

452
        return implode(
453
            DIRECTORY_SEPARATOR,
454
            array_slice(
455
                explode(DIRECTORY_SEPARATOR, $file),
456
                0,
457
                -1
458
            )
459
        ) . DIRECTORY_SEPARATOR . $this->basename($file);
460
    }
461

462
    /**
463
     * Gets the namespace from the command-line option,
464
     * or the default namespace if the option is not set.
465
     * Can be overridden by directly setting $this->namespace.
466
     */
467
    protected function getNamespace(): string
468
    {
469
        return $this->namespace ?? trim(
470
            str_replace(
471
                '/',
472
                '\\',
473
                $this->getOption('namespace') ?? APP_NAMESPACE
474
            ),
475
            '\\'
476
        );
477
    }
478

479
    /**
480
     * Allows child generators to modify the internal `$hasClassName` flag.
481
     *
482
     * @return $this
483
     */
484
    protected function setHasClassName(bool $hasClassName)
485
    {
486
        $this->hasClassName = $hasClassName;
487

488
        return $this;
489
    }
490

491
    /**
492
     * Allows child generators to modify the internal `$sortImports` flag.
493
     *
494
     * @return $this
495
     */
496
    protected function setSortImports(bool $sortImports)
497
    {
498
        $this->sortImports = $sortImports;
499

500
        return $this;
501
    }
502

503
    /**
504
     * Allows child generators to modify the internal `$enabledSuffixing` flag.
505
     *
506
     * @return $this
507
     */
508
    protected function setEnabledSuffixing(bool $enabledSuffixing)
509
    {
510
        $this->enabledSuffixing = $enabledSuffixing;
511

512
        return $this;
513
    }
514

515
    /**
516
     * Gets a single command-line option. Returns TRUE if the option exists,
517
     * but doesn't have a value, and is simply acting as a flag.
518
     */
519
    protected function getOption(string $name): bool|string|null
520
    {
521
        if (! array_key_exists($name, $this->params)) {
522
            return CLI::getOption($name);
523
        }
524

525
        return $this->params[$name] ?? true;
526
    }
527
}
528

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.