• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

codeigniter4 / CodeIgniter4 / 22300730863

23 Feb 2026 09:45AM UTC coverage: 86.638% (+0.08%) from 86.559%
22300730863

Pull #9970

github

web-flow
Merge 2f11d7c79 into f733c6ed9
Pull Request #9970: feat: Add Global Context feature

57 of 57 new or added lines in 4 files covered. (100.0%)

96 existing lines in 7 files now uncovered.

22305 of 25745 relevant lines covered (86.64%)

218.33 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.07
/system/CLI/GeneratorTrait.php
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
     * Generates a class file from an existing template.
105
     *
106
     * @param array<int|string, string|null> $params
107
     */
108
    protected function generateClass(array $params): void
109
    {
110
        $this->params = $params;
63✔
111

112
        // Get the fully qualified class name from the input.
113
        $class = $this->qualifyClassName();
63✔
114

115
        // Get the file path from class name.
116
        $target = $this->buildPath($class);
63✔
117

118
        // Check if path is empty.
119
        if ($target === '') {
63✔
120
            return;
1✔
121
        }
122

123
        $this->generateFile($target, $this->buildContent($class));
62✔
124
    }
125

126
    /**
127
     * Generate a view file from an existing template.
128
     *
129
     * @param string                         $view   namespaced view name that is generated
130
     * @param array<int|string, string|null> $params
131
     */
132
    protected function generateView(string $view, array $params): void
133
    {
134
        $this->params = $params;
3✔
135

136
        $target = $this->buildPath($view);
3✔
137

138
        // Check if path is empty.
139
        if ($target === '') {
3✔
UNCOV
140
            return;
×
141
        }
142

143
        $this->generateFile($target, $this->buildContent($view));
3✔
144
    }
145

146
    /**
147
     * Handles writing the file to disk, and all of the safety checks around that.
148
     *
149
     * @param string $target file path
150
     */
151
    private function generateFile(string $target, string $content): void
152
    {
153
        if ($this->getOption('namespace') === 'CodeIgniter') {
62✔
154
            // @codeCoverageIgnoreStart
UNCOV
155
            CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');
×
UNCOV
156
            CLI::newLine();
×
157

158
            if (
UNCOV
159
                CLI::prompt(
×
UNCOV
160
                    'Are you sure you want to continue?',
×
UNCOV
161
                    ['y', 'n'],
×
UNCOV
162
                    'required',
×
UNCOV
163
                ) === 'n'
×
164
            ) {
UNCOV
165
                CLI::newLine();
×
UNCOV
166
                CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');
×
167
                CLI::newLine();
×
168

UNCOV
169
                return;
×
170
            }
171

172
            CLI::newLine();
×
173
            // @codeCoverageIgnoreEnd
174
        }
175

176
        $isFile = is_file($target);
62✔
177

178
        // Overwriting files unknowingly is a serious annoyance, So we'll check if
179
        // we are duplicating things, If 'force' option is not supplied, we bail.
180
        if (! $this->getOption('force') && $isFile) {
62✔
181
            CLI::error(
2✔
182
                lang('CLI.generator.fileExist', [clean_path($target)]),
2✔
183
                'light_gray',
2✔
184
                'red',
2✔
185
            );
2✔
186
            CLI::newLine();
2✔
187

188
            return;
2✔
189
        }
190

191
        // Check if the directory to save the file is existing.
192
        $dir = dirname($target);
62✔
193

194
        if (! is_dir($dir)) {
62✔
195
            mkdir($dir, 0755, true);
25✔
196
        }
197

198
        helper('filesystem');
62✔
199

200
        // Build the class based on the details we have, We'll be getting our file
201
        // contents from the template, and then we'll do the necessary replacements.
202
        if (! write_file($target, $content)) {
62✔
203
            // @codeCoverageIgnoreStart
204
            CLI::error(
1✔
205
                lang('CLI.generator.fileError', [clean_path($target)]),
1✔
206
                'light_gray',
1✔
207
                'red',
1✔
208
            );
1✔
209
            CLI::newLine();
1✔
210

211
            return;
1✔
212
            // @codeCoverageIgnoreEnd
213
        }
214

215
        if ($this->getOption('force') && $isFile) {
61✔
216
            CLI::write(
3✔
217
                lang('CLI.generator.fileOverwrite', [clean_path($target)]),
3✔
218
                'yellow',
3✔
219
            );
3✔
220
            CLI::newLine();
3✔
221

222
            return;
3✔
223
        }
224

225
        CLI::write(
61✔
226
            lang('CLI.generator.fileCreate', [clean_path($target)]),
61✔
227
            'green',
61✔
228
        );
61✔
229
        CLI::newLine();
61✔
230
    }
231

232
    /**
233
     * Prepare options and do the necessary replacements.
234
     *
235
     * @param string $class namespaced classname or namespaced view.
236
     *
237
     * @return string generated file content
238
     */
239
    protected function prepare(string $class): string
240
    {
241
        return $this->parseTemplate($class);
28✔
242
    }
243

244
    /**
245
     * Change file basename before saving.
246
     *
247
     * Useful for components where the file name has a date.
248
     */
249
    protected function basename(string $filename): string
250
    {
251
        return basename($filename);
57✔
252
    }
253

254
    /**
255
     * Parses the class name and checks if it is already qualified.
256
     */
257
    protected function qualifyClassName(): string
258
    {
259
        $class = $this->normalizeInputClassName();
63✔
260

261
        // Gets the namespace from input. Don't forget the ending backslash!
262
        $namespace = $this->getNamespace() . '\\';
63✔
263

264
        if (str_starts_with($class, $namespace)) {
63✔
UNCOV
265
            return $class; // @codeCoverageIgnore
×
266
        }
267

268
        $directoryString = ($this->directory !== null) ? $this->directory . '\\' : '';
63✔
269

270
        return $namespace . $directoryString . str_replace('/', '\\', $class);
63✔
271
    }
272

273
    private function normalizeInputClassName(): string
274
    {
275
        // Gets the class name from input.
276
        $class = $this->params[0] ?? CLI::getSegment(2);
63✔
277

278
        if ($class === null && $this->hasClassName) {
63✔
279
            $nameField = $this->classNameLang !== ''
1✔
280
                ? $this->classNameLang
1✔
UNCOV
281
                : 'CLI.generator.className.default';
×
282
            $class = CLI::prompt(lang($nameField), null, 'required');
1✔
283

284
            // Reassign the class name to the params array in case
285
            // the class name is requested again
286
            $this->params[0] = $class;
1✔
287
            CLI::newLine();
1✔
288
        }
289

290
        helper('inflector');
63✔
291

292
        $component = singular($this->component);
63✔
293

294
        /**
295
         * @see https://regex101.com/r/a5KNCR/2
296
         */
297
        $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)$/i', $component);
63✔
298

299
        if (preg_match($pattern, $class, $matches) === 1) {
63✔
300
            $class = $matches[1] . ucfirst($matches[2]);
3✔
301
        }
302

303
        if (
304
            $this->enabledSuffixing && $this->getOption('suffix')
63✔
305
            && preg_match($pattern, $class) !== 1
63✔
306
        ) {
307
            $class .= ucfirst($component);
18✔
308
        }
309

310
        // Trims input, normalize separators, and ensure that all paths are in Pascalcase.
311
        return ltrim(
63✔
312
            implode(
63✔
313
                '\\',
63✔
314
                array_map(
63✔
315
                    pascalize(...),
63✔
316
                    explode('\\', str_replace('/', '\\', trim($class))),
63✔
317
                ),
63✔
318
            ),
63✔
319
            '\\/',
63✔
320
        );
63✔
321
    }
322

323
    /**
324
     * Gets the generator view as defined in the `Config\Generators::$views`,
325
     * with fallback to `$template` when the defined view does not exist.
326
     *
327
     * @param array<string, mixed> $data
328
     */
329
    protected function renderTemplate(array $data = []): string
330
    {
331
        try {
332
            $template = $this->templatePath ?? config(Generators::class)->views[$this->name];
59✔
333

334
            return view($template, $data, ['debug' => false]);
49✔
335
        } catch (Throwable $e) {
10✔
336
            log_message('error', (string) $e);
10✔
337

338
            return view(
10✔
339
                "CodeIgniter\\Commands\\Generators\\Views\\{$this->template}",
10✔
340
                $data,
10✔
341
                ['debug' => false],
10✔
342
            );
10✔
343
        }
344
    }
345

346
    /**
347
     * Performs pseudo-variables contained within view file.
348
     *
349
     * @param string                          $class   namespaced classname or namespaced view.
350
     * @param list<string>                    $search
351
     * @param list<string>                    $replace
352
     * @param array<string, bool|string|null> $data
353
     *
354
     * @return string generated file content
355
     */
356
    protected function parseTemplate(
357
        string $class,
358
        array $search = [],
359
        array $replace = [],
360
        array $data = [],
361
    ): string {
362
        // Retrieves the namespace part from the fully qualified class name.
363
        $namespace = trim(
59✔
364
            implode(
59✔
365
                '\\',
59✔
366
                array_slice(explode('\\', $class), 0, -1),
59✔
367
            ),
59✔
368
            '\\',
59✔
369
        );
59✔
370
        $search[]  = '<@php';
59✔
371
        $search[]  = '{namespace}';
59✔
372
        $search[]  = '{class}';
59✔
373
        $replace[] = '<?php';
59✔
374
        $replace[] = $namespace;
59✔
375
        $replace[] = str_replace($namespace . '\\', '', $class);
59✔
376

377
        return str_replace($search, $replace, $this->renderTemplate($data));
59✔
378
    }
379

380
    /**
381
     * Builds the contents for class being generated, doing all
382
     * the replacements necessary, and alphabetically sorts the
383
     * imports for a given template.
384
     */
385
    protected function buildContent(string $class): string
386
    {
387
        $template = $this->prepare($class);
62✔
388

389
        if (
390
            $this->sortImports
62✔
391
            && preg_match(
62✔
392
                '/(?P<imports>(?:^use [^;]+;$\n?)+)/m',
62✔
393
                $template,
62✔
394
                $match,
62✔
395
            )
62✔
396
        ) {
397
            $imports = explode("\n", trim($match['imports']));
59✔
398
            sort($imports);
59✔
399

400
            return str_replace(trim($match['imports']), implode("\n", $imports), $template);
59✔
401
        }
402

403
        return $template;
6✔
404
    }
405

406
    /**
407
     * Builds the file path from the class name.
408
     *
409
     * @param string $class namespaced classname or namespaced view.
410
     */
411
    protected function buildPath(string $class): string
412
    {
413
        $namespace = $this->getNamespace();
58✔
414

415
        // Check if the namespace is actually defined and we are not just typing gibberish.
416
        $base = service('autoloader')->getNamespace($namespace);
58✔
417

418
        if (! $base = reset($base)) {
58✔
419
            CLI::error(
1✔
420
                lang('CLI.namespaceNotDefined', [$namespace]),
1✔
421
                'light_gray',
1✔
422
                'red',
1✔
423
            );
1✔
424
            CLI::newLine();
1✔
425

426
            return '';
1✔
427
        }
428

429
        $realpath = realpath($base);
57✔
430
        $base     = ($realpath !== false) ? $realpath : $base;
57✔
431

432
        $file = $base . DIRECTORY_SEPARATOR
57✔
433
            . str_replace(
57✔
434
                '\\',
57✔
435
                DIRECTORY_SEPARATOR,
57✔
436
                trim(str_replace($namespace . '\\', '', $class), '\\'),
57✔
437
            ) . '.php';
57✔
438

439
        return implode(
57✔
440
            DIRECTORY_SEPARATOR,
57✔
441
            array_slice(
57✔
442
                explode(DIRECTORY_SEPARATOR, $file),
57✔
443
                0,
57✔
444
                -1,
57✔
445
            ),
57✔
446
        ) . DIRECTORY_SEPARATOR . $this->basename($file);
57✔
447
    }
448

449
    /**
450
     * Gets the namespace from the command-line option,
451
     * or the default namespace if the option is not set.
452
     * Can be overridden by directly setting $this->namespace.
453
     */
454
    protected function getNamespace(): string
455
    {
456
        return $this->namespace ?? trim(
58✔
457
            str_replace(
58✔
458
                '/',
58✔
459
                '\\',
58✔
460
                $this->getOption('namespace') ?? APP_NAMESPACE,
58✔
461
            ),
58✔
462
            '\\',
58✔
463
        );
58✔
464
    }
465

466
    /**
467
     * Allows child generators to modify the internal `$hasClassName` flag.
468
     *
469
     * @return $this
470
     */
471
    protected function setHasClassName(bool $hasClassName)
472
    {
473
        $this->hasClassName = $hasClassName;
3✔
474

475
        return $this;
3✔
476
    }
477

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

487
        return $this;
3✔
488
    }
489

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

499
        return $this;
1✔
500
    }
501

502
    /**
503
     * Gets a single command-line option. Returns TRUE if the option exists,
504
     * but doesn't have a value, and is simply acting as a flag.
505
     */
506
    protected function getOption(string $name): bool|string|null
507
    {
508
        if (! array_key_exists($name, $this->params)) {
63✔
509
            return CLI::getOption($name);
63✔
510
        }
511

512
        return $this->params[$name] ?? true;
42✔
513
    }
514
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc