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

codeigniter4 / CodeIgniter4 / 8677009716

13 Apr 2024 11:45PM UTC coverage: 84.44% (-2.2%) from 86.607%
8677009716

push

github

web-flow
Merge pull request #8776 from kenjis/fix-findQualifiedNameFromPath-Cannot-declare-class-v3

fix: Cannot declare class CodeIgniter\Config\Services, because the name is already in use

0 of 3 new or added lines in 1 file covered. (0.0%)

478 existing lines in 72 files now uncovered.

20318 of 24062 relevant lines covered (84.44%)

188.23 hits per line

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

88.1
/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
100
     */
101
    private $params = [];
102

103
    /**
104
     * Execute the command.
105
     *
106
     * @deprecated use generateClass() instead
107
     */
108
    protected function execute(array $params): void
109
    {
UNCOV
110
        $this->generateClass($params);
×
111
    }
112

113
    /**
114
     * Generates a class file from an existing template.
115
     */
116
    protected function generateClass(array $params): void
117
    {
118
        $this->params = $params;
55✔
119

120
        // Get the fully qualified class name from the input.
121
        $class = $this->qualifyClassName();
55✔
122

123
        // Get the file path from class name.
124
        $target = $this->buildPath($class);
55✔
125

126
        // Check if path is empty.
127
        if ($target === '') {
55✔
128
            return;
1✔
129
        }
130

131
        $this->generateFile($target, $this->buildContent($class));
54✔
132
    }
133

134
    /**
135
     * Generate a view file from an existing template.
136
     *
137
     * @param string $view namespaced view name that is generated
138
     */
139
    protected function generateView(string $view, array $params): void
140
    {
141
        $this->params = $params;
3✔
142

143
        $target = $this->buildPath($view);
3✔
144

145
        // Check if path is empty.
146
        if ($target === '') {
3✔
147
            return;
×
148
        }
149

150
        $this->generateFile($target, $this->buildContent($view));
3✔
151
    }
152

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

165
            if (
UNCOV
166
                CLI::prompt(
×
UNCOV
167
                    'Are you sure you want to continue?',
×
UNCOV
168
                    ['y', 'n'],
×
UNCOV
169
                    'required'
×
UNCOV
170
                ) === 'n'
×
171
            ) {
UNCOV
172
                CLI::newLine();
×
UNCOV
173
                CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');
×
UNCOV
174
                CLI::newLine();
×
175

UNCOV
176
                return;
×
177
            }
178

UNCOV
179
            CLI::newLine();
×
180
            // @codeCoverageIgnoreEnd
181
        }
182

183
        $isFile = is_file($target);
54✔
184

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

195
            return;
1✔
196
        }
197

198
        // Check if the directory to save the file is existing.
199
        $dir = dirname($target);
54✔
200

201
        if (! is_dir($dir)) {
54✔
202
            mkdir($dir, 0755, true);
22✔
203
        }
204

205
        helper('filesystem');
54✔
206

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

218
            return;
1✔
219
            // @codeCoverageIgnoreEnd
220
        }
221

222
        if ($this->getOption('force') && $isFile) {
53✔
223
            CLI::write(
2✔
224
                lang('CLI.generator.fileOverwrite', [clean_path($target)]),
2✔
225
                'yellow'
2✔
226
            );
2✔
227
            CLI::newLine();
2✔
228

229
            return;
2✔
230
        }
231

232
        CLI::write(
53✔
233
            lang('CLI.generator.fileCreate', [clean_path($target)]),
53✔
234
            'green'
53✔
235
        );
53✔
236
        CLI::newLine();
53✔
237
    }
238

239
    /**
240
     * Prepare options and do the necessary replacements.
241
     *
242
     * @param string $class namespaced classname or namespaced view.
243
     *
244
     * @return string generated file content
245
     */
246
    protected function prepare(string $class): string
247
    {
248
        return $this->parseTemplate($class);
24✔
249
    }
250

251
    /**
252
     * Change file basename before saving.
253
     *
254
     * Useful for components where the file name has a date.
255
     */
256
    protected function basename(string $filename): string
257
    {
258
        return basename($filename);
49✔
259
    }
260

261
    /**
262
     * Parses the class name and checks if it is already qualified.
263
     */
264
    protected function qualifyClassName(): string
265
    {
266
        $class = $this->normalizeInputClassName();
55✔
267

268
        // Gets the namespace from input. Don't forget the ending backslash!
269
        $namespace = $this->getNamespace() . '\\';
55✔
270

271
        if (str_starts_with($class, $namespace)) {
55✔
UNCOV
272
            return $class; // @codeCoverageIgnore
×
273
        }
274

275
        $directoryString = ($this->directory !== null) ? $this->directory . '\\' : '';
55✔
276

277
        return $namespace . $directoryString . str_replace('/', '\\', $class);
55✔
278
    }
279

280
    /**
281
     * Normalize input classname.
282
     */
283
    private function normalizeInputClassName(): string
284
    {
285
        // Gets the class name from input.
286
        $class = $this->params[0] ?? CLI::getSegment(2);
55✔
287

288
        if ($class === null && $this->hasClassName) {
55✔
289
            // @codeCoverageIgnoreStart
UNCOV
290
            $nameLang = $this->classNameLang !== ''
×
UNCOV
291
                ? $this->classNameLang
×
UNCOV
292
                : 'CLI.generator.className.default';
×
UNCOV
293
            $class = CLI::prompt(lang($nameLang), null, 'required');
×
UNCOV
294
            CLI::newLine();
×
295
            // @codeCoverageIgnoreEnd
296
        }
297

298
        helper('inflector');
55✔
299

300
        $component = singular($this->component);
55✔
301

302
        /**
303
         * @see https://regex101.com/r/a5KNCR/2
304
         */
305
        $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)$/i', $component);
55✔
306

307
        if (preg_match($pattern, $class, $matches) === 1) {
55✔
308
            $class = $matches[1] . ucfirst($matches[2]);
2✔
309
        }
310

311
        if (
312
            $this->enabledSuffixing && $this->getOption('suffix')
55✔
313
            && preg_match($pattern, $class) !== 1
55✔
314
        ) {
315
            $class .= ucfirst($component);
13✔
316
        }
317

318
        // Trims input, normalize separators, and ensure that all paths are in Pascalcase.
319
        return ltrim(
55✔
320
            implode(
55✔
321
                '\\',
55✔
322
                array_map(
55✔
323
                    'pascalize',
55✔
324
                    explode('\\', str_replace('/', '\\', trim($class)))
55✔
325
                )
55✔
326
            ),
55✔
327
            '\\/'
55✔
328
        );
55✔
329
    }
330

331
    /**
332
     * Gets the generator view as defined in the `Config\Generators::$views`,
333
     * with fallback to `$template` when the defined view does not exist.
334
     */
335
    protected function renderTemplate(array $data = []): string
336
    {
337
        try {
338
            $template = $this->templatePath ?? config(Generators::class)->views[$this->name];
51✔
339

340
            return view($template, $data, ['debug' => false]);
49✔
341
        } catch (Throwable $e) {
2✔
342
            log_message('error', (string) $e);
2✔
343

344
            return view(
2✔
345
                "CodeIgniter\\Commands\\Generators\\Views\\{$this->template}",
2✔
346
                $data,
2✔
347
                ['debug' => false]
2✔
348
            );
2✔
349
        }
350
    }
351

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

380
        return str_replace($search, $replace, $this->renderTemplate($data));
51✔
381
    }
382

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

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

403
            return str_replace(trim($match['imports']), implode("\n", $imports), $template);
51✔
404
        }
405

406
        return $template;
6✔
407
    }
408

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

418
        // Check if the namespace is actually defined and we are not just typing gibberish.
419
        $base = service('autoloader')->getNamespace($namespace);
54✔
420

421
        if (! $base = reset($base)) {
54✔
422
            CLI::error(
1✔
423
                lang('CLI.namespaceNotDefined', [$namespace]),
1✔
424
                'light_gray',
1✔
425
                'red'
1✔
426
            );
1✔
427
            CLI::newLine();
1✔
428

429
            return '';
1✔
430
        }
431

432
        $realpath = realpath($base);
53✔
433
        $base     = ($realpath !== false) ? $realpath : $base;
53✔
434

435
        $file = $base . DIRECTORY_SEPARATOR
53✔
436
            . str_replace(
53✔
437
                '\\',
53✔
438
                DIRECTORY_SEPARATOR,
53✔
439
                trim(str_replace($namespace . '\\', '', $class), '\\')
53✔
440
            ) . '.php';
53✔
441

442
        return implode(
53✔
443
            DIRECTORY_SEPARATOR,
53✔
444
            array_slice(
53✔
445
                explode(DIRECTORY_SEPARATOR, $file),
53✔
446
                0,
53✔
447
                -1
53✔
448
            )
53✔
449
        ) . DIRECTORY_SEPARATOR . $this->basename($file);
53✔
450
    }
451

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

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

478
        return $this;
3✔
479
    }
480

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

490
        return $this;
3✔
491
    }
492

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

502
        return $this;
1✔
503
    }
504

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

515
        return $this->params[$name] ?? true;
35✔
516
    }
517
}
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