ci4
1<?php
2
3declare(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
14namespace CodeIgniter\CLI;15
16use Config\Generators;17use Throwable;18
19/**
20* GeneratorTrait contains a collection of methods
21* to build the commands that generates a file.
22*/
23trait GeneratorTrait24{
25/**26* Component Name
27*
28* @var string
29*/
30protected $component;31
32/**33* File directory
34*
35* @var string
36*/
37protected $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*/
45protected ?string $templatePath = null;46
47/**48* View template name for fallback
49*
50* @var string
51*/
52protected $template;53
54/**55* Language string key for required class names.
56*
57* @var string
58*/
59protected $classNameLang = '';60
61/**62* Namespace to use for class.
63* Leave null to use the default namespace.
64*/
65protected ?string $namespace = null;66
67/**68* Whether to require class name.
69*
70* @internal
71*
72* @var bool
73*/
74private $hasClassName = true;75
76/**77* Whether to sort class imports.
78*
79* @internal
80*
81* @var bool
82*/
83private $sortImports = true;84
85/**86* Whether the `--suffix` option has any effect.
87*
88* @internal
89*
90* @var bool
91*/
92private $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*/
101private $params = [];102
103/**104* Execute the command.
105*
106* @param array<int|string, string|null> $params
107*
108* @deprecated use generateClass() instead
109*/
110protected function execute(array $params): void111{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*/
120protected function generateClass(array $params): void121{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.131if ($target === '') {132return;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*/
144protected function generateView(string $view, array $params): void145{146$this->params = $params;147
148$target = $this->buildPath($view);149
150// Check if path is empty.151if ($target === '') {152return;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*/
163private function generateFile(string $target, string $content): void164{165if ($this->getOption('namespace') === 'CodeIgniter') {166// @codeCoverageIgnoreStart167CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');168CLI::newLine();169
170if (171CLI::prompt(172'Are you sure you want to continue?',173['y', 'n'],174'required'175) === 'n'176) {177CLI::newLine();178CLI::write(lang('CLI.generator.cancelOperation'), 'yellow');179CLI::newLine();180
181return;182}183
184CLI::newLine();185// @codeCoverageIgnoreEnd186}187
188$isFile = is_file($target);189
190// Overwriting files unknowingly is a serious annoyance, So we'll check if191// we are duplicating things, If 'force' option is not supplied, we bail.192if (! $this->getOption('force') && $isFile) {193CLI::error(194lang('CLI.generator.fileExist', [clean_path($target)]),195'light_gray',196'red'197);198CLI::newLine();199
200return;201}202
203// Check if the directory to save the file is existing.204$dir = dirname($target);205
206if (! is_dir($dir)) {207mkdir($dir, 0755, true);208}209
210helper('filesystem');211
212// Build the class based on the details we have, We'll be getting our file213// contents from the template, and then we'll do the necessary replacements.214if (! write_file($target, $content)) {215// @codeCoverageIgnoreStart216CLI::error(217lang('CLI.generator.fileError', [clean_path($target)]),218'light_gray',219'red'220);221CLI::newLine();222
223return;224// @codeCoverageIgnoreEnd225}226
227if ($this->getOption('force') && $isFile) {228CLI::write(229lang('CLI.generator.fileOverwrite', [clean_path($target)]),230'yellow'231);232CLI::newLine();233
234return;235}236
237CLI::write(238lang('CLI.generator.fileCreate', [clean_path($target)]),239'green'240);241CLI::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*/
251protected function prepare(string $class): string252{253return $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*/
261protected function basename(string $filename): string262{263return basename($filename);264}265
266/**267* Parses the class name and checks if it is already qualified.
268*/
269protected function qualifyClassName(): string270{271$class = $this->normalizeInputClassName();272
273// Gets the namespace from input. Don't forget the ending backslash!274$namespace = $this->getNamespace() . '\\';275
276if (str_starts_with($class, $namespace)) {277return $class; // @codeCoverageIgnore278}279
280$directoryString = ($this->directory !== null) ? $this->directory . '\\' : '';281
282return $namespace . $directoryString . str_replace('/', '\\', $class);283}284
285/**286* Normalize input classname.
287*/
288private function normalizeInputClassName(): string289{290// Gets the class name from input.291$class = $this->params[0] ?? CLI::getSegment(2);292
293if ($class === null && $this->hasClassName) {294// @codeCoverageIgnoreStart295$nameLang = $this->classNameLang !== ''296? $this->classNameLang297: 'CLI.generator.className.default';298$class = CLI::prompt(lang($nameLang), null, 'required');299CLI::newLine();300// @codeCoverageIgnoreEnd301}302
303helper('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
312if (preg_match($pattern, $class, $matches) === 1) {313$class = $matches[1] . ucfirst($matches[2]);314}315
316if (317$this->enabledSuffixing && $this->getOption('suffix')318&& preg_match($pattern, $class) !== 1319) {320$class .= ucfirst($component);321}322
323// Trims input, normalize separators, and ensure that all paths are in Pascalcase.324return ltrim(325implode(326'\\',327array_map(328pascalize(...),329explode('\\', 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*/
342protected function renderTemplate(array $data = []): string343{344try {345$template = $this->templatePath ?? config(Generators::class)->views[$this->name];346
347return view($template, $data, ['debug' => false]);348} catch (Throwable $e) {349log_message('error', (string) $e);350
351return 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*/
369protected function parseTemplate(370string $class,371array $search = [],372array $replace = [],373array $data = []374): string {375// Retrieves the namespace part from the fully qualified class name.376$namespace = trim(377implode(378'\\',379array_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
390return 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*/
398protected function buildContent(string $class): string399{400$template = $this->prepare($class);401
402if (403$this->sortImports404&& preg_match(405'/(?P<imports>(?:^use [^;]+;$\n?)+)/m',406$template,407$match408)409) {410$imports = explode("\n", trim($match['imports']));411sort($imports);412
413return str_replace(trim($match['imports']), implode("\n", $imports), $template);414}415
416return $template;417}418
419/**420* Builds the file path from the class name.
421*
422* @param string $class namespaced classname or namespaced view.
423*/
424protected function buildPath(string $class): string425{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
431if (! $base = reset($base)) {432CLI::error(433lang('CLI.namespaceNotDefined', [$namespace]),434'light_gray',435'red'436);437CLI::newLine();438
439return '';440}441
442$realpath = realpath($base);443$base = ($realpath !== false) ? $realpath : $base;444
445$file = $base . DIRECTORY_SEPARATOR446. str_replace(447'\\',448DIRECTORY_SEPARATOR,449trim(str_replace($namespace . '\\', '', $class), '\\')450) . '.php';451
452return implode(453DIRECTORY_SEPARATOR,454array_slice(455explode(DIRECTORY_SEPARATOR, $file),4560,457-1458)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*/
467protected function getNamespace(): string468{469return $this->namespace ?? trim(470str_replace(471'/',472'\\',473$this->getOption('namespace') ?? APP_NAMESPACE474),475'\\'476);477}478
479/**480* Allows child generators to modify the internal `$hasClassName` flag.
481*
482* @return $this
483*/
484protected function setHasClassName(bool $hasClassName)485{486$this->hasClassName = $hasClassName;487
488return $this;489}490
491/**492* Allows child generators to modify the internal `$sortImports` flag.
493*
494* @return $this
495*/
496protected function setSortImports(bool $sortImports)497{498$this->sortImports = $sortImports;499
500return $this;501}502
503/**504* Allows child generators to modify the internal `$enabledSuffixing` flag.
505*
506* @return $this
507*/
508protected function setEnabledSuffixing(bool $enabledSuffixing)509{510$this->enabledSuffixing = $enabledSuffixing;511
512return $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*/
519protected function getOption(string $name): bool|string|null520{521if (! array_key_exists($name, $this->params)) {522return CLI::getOption($name);523}524
525return $this->params[$name] ?? true;526}527}
528