* * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\CLI; use Config\Generators; use Throwable; /** * GeneratorTrait contains a collection of methods * to build the commands that generates a file. */ trait GeneratorTrait { /** * Component Name * * @var string */ protected $component; /** * File directory * * @var string */ protected $directory; /** * (Optional) View template path * * We use special namespaced paths like: * `CodeIgniter\Commands\Generators\Views\cell.tpl.php`. */ protected ?string $templatePath = null; /** * View template name for fallback * * @var string */ protected $template; /** * Language string key for required class names. * * @var string */ protected $classNameLang = ''; /** * Namespace to use for class. * Leave null to use the default namespace. */ protected ?string $namespace = null; /** * Whether to require class name. * * @internal * * @var bool */ private $hasClassName = true; /** * Whether to sort class imports. * * @internal * * @var bool */ private $sortImports = true; /** * Whether the `--suffix` option has any effect. * * @internal * * @var bool */ private $enabledSuffixing = true; /** * The params array for easy access by other methods. * * @internal * * @var array */ private $params = []; /** * Execute the command. * * @param array $params * * @deprecated use generateClass() instead */ protected function execute(array $params): void { $this->generateClass($params); } /** * Generates a class file from an existing template. * * @param array $params */ protected function generateClass(array $params): void { $this->params = $params; // Get the fully qualified class name from the input. $class = $this->qualifyClassName(); // Get the file path from class name. $target = $this->buildPath($class); // Check if path is empty. if ($target === '') { return; } $this->generateFile($target, $this->buildContent($class)); } /** * Generate a view file from an existing template. * * @param string $view namespaced view name that is generated * @param array $params */ protected function generateView(string $view, array $params): void { $this->params = $params; $target = $this->buildPath($view); // Check if path is empty. if ($target === '') { return; } $this->generateFile($target, $this->buildContent($view)); } /** * Handles writing the file to disk, and all of the safety checks around that. * * @param string $target file path */ private function generateFile(string $target, string $content): void { if ($this->getOption('namespace') === 'CodeIgniter') { // @codeCoverageIgnoreStart CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow'); CLI::newLine(); if ( CLI::prompt( 'Are you sure you want to continue?', ['y', 'n'], 'required' ) === 'n' ) { CLI::newLine(); CLI::write(lang('CLI.generator.cancelOperation'), 'yellow'); CLI::newLine(); return; } CLI::newLine(); // @codeCoverageIgnoreEnd } $isFile = is_file($target); // Overwriting files unknowingly is a serious annoyance, So we'll check if // we are duplicating things, If 'force' option is not supplied, we bail. if (! $this->getOption('force') && $isFile) { CLI::error( lang('CLI.generator.fileExist', [clean_path($target)]), 'light_gray', 'red' ); CLI::newLine(); return; } // Check if the directory to save the file is existing. $dir = dirname($target); if (! is_dir($dir)) { mkdir($dir, 0755, true); } helper('filesystem'); // Build the class based on the details we have, We'll be getting our file // contents from the template, and then we'll do the necessary replacements. if (! write_file($target, $content)) { // @codeCoverageIgnoreStart CLI::error( lang('CLI.generator.fileError', [clean_path($target)]), 'light_gray', 'red' ); CLI::newLine(); return; // @codeCoverageIgnoreEnd } if ($this->getOption('force') && $isFile) { CLI::write( lang('CLI.generator.fileOverwrite', [clean_path($target)]), 'yellow' ); CLI::newLine(); return; } CLI::write( lang('CLI.generator.fileCreate', [clean_path($target)]), 'green' ); CLI::newLine(); } /** * Prepare options and do the necessary replacements. * * @param string $class namespaced classname or namespaced view. * * @return string generated file content */ protected function prepare(string $class): string { return $this->parseTemplate($class); } /** * Change file basename before saving. * * Useful for components where the file name has a date. */ protected function basename(string $filename): string { return basename($filename); } /** * Parses the class name and checks if it is already qualified. */ protected function qualifyClassName(): string { $class = $this->normalizeInputClassName(); // Gets the namespace from input. Don't forget the ending backslash! $namespace = $this->getNamespace() . '\\'; if (str_starts_with($class, $namespace)) { return $class; // @codeCoverageIgnore } $directoryString = ($this->directory !== null) ? $this->directory . '\\' : ''; return $namespace . $directoryString . str_replace('/', '\\', $class); } /** * Normalize input classname. */ private function normalizeInputClassName(): string { // Gets the class name from input. $class = $this->params[0] ?? CLI::getSegment(2); if ($class === null && $this->hasClassName) { // @codeCoverageIgnoreStart $nameLang = $this->classNameLang !== '' ? $this->classNameLang : 'CLI.generator.className.default'; $class = CLI::prompt(lang($nameLang), null, 'required'); CLI::newLine(); // @codeCoverageIgnoreEnd } helper('inflector'); $component = singular($this->component); /** * @see https://regex101.com/r/a5KNCR/2 */ $pattern = sprintf('/([a-z][a-z0-9_\/\\\\]+)(%s)$/i', $component); if (preg_match($pattern, $class, $matches) === 1) { $class = $matches[1] . ucfirst($matches[2]); } if ( $this->enabledSuffixing && $this->getOption('suffix') && preg_match($pattern, $class) !== 1 ) { $class .= ucfirst($component); } // Trims input, normalize separators, and ensure that all paths are in Pascalcase. return ltrim( implode( '\\', array_map( 'pascalize', explode('\\', str_replace('/', '\\', trim($class))) ) ), '\\/' ); } /** * Gets the generator view as defined in the `Config\Generators::$views`, * with fallback to `$template` when the defined view does not exist. * * @param array $data */ protected function renderTemplate(array $data = []): string { try { $template = $this->templatePath ?? config(Generators::class)->views[$this->name]; return view($template, $data, ['debug' => false]); } catch (Throwable $e) { log_message('error', (string) $e); return view( "CodeIgniter\\Commands\\Generators\\Views\\{$this->template}", $data, ['debug' => false] ); } } /** * Performs pseudo-variables contained within view file. * * @param string $class namespaced classname or namespaced view. * @param list $search * @param list $replace * @param array $data * * @return string generated file content */ protected function parseTemplate( string $class, array $search = [], array $replace = [], array $data = [] ): string { // Retrieves the namespace part from the fully qualified class name. $namespace = trim( implode( '\\', array_slice(explode('\\', $class), 0, -1) ), '\\' ); $search[] = '<@php'; $search[] = '{namespace}'; $search[] = '{class}'; $replace[] = 'renderTemplate($data)); } /** * Builds the contents for class being generated, doing all * the replacements necessary, and alphabetically sorts the * imports for a given template. */ protected function buildContent(string $class): string { $template = $this->prepare($class); if ( $this->sortImports && preg_match( '/(?P(?:^use [^;]+;$\n?)+)/m', $template, $match ) ) { $imports = explode("\n", trim($match['imports'])); sort($imports); return str_replace(trim($match['imports']), implode("\n", $imports), $template); } return $template; } /** * Builds the file path from the class name. * * @param string $class namespaced classname or namespaced view. */ protected function buildPath(string $class): string { $namespace = $this->getNamespace(); // Check if the namespace is actually defined and we are not just typing gibberish. $base = service('autoloader')->getNamespace($namespace); if (! $base = reset($base)) { CLI::error( lang('CLI.namespaceNotDefined', [$namespace]), 'light_gray', 'red' ); CLI::newLine(); return ''; } $realpath = realpath($base); $base = ($realpath !== false) ? $realpath : $base; $file = $base . DIRECTORY_SEPARATOR . str_replace( '\\', DIRECTORY_SEPARATOR, trim(str_replace($namespace . '\\', '', $class), '\\') ) . '.php'; return implode( DIRECTORY_SEPARATOR, array_slice( explode(DIRECTORY_SEPARATOR, $file), 0, -1 ) ) . DIRECTORY_SEPARATOR . $this->basename($file); } /** * Gets the namespace from the command-line option, * or the default namespace if the option is not set. * Can be overridden by directly setting $this->namespace. */ protected function getNamespace(): string { return $this->namespace ?? trim( str_replace( '/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE ), '\\' ); } /** * Allows child generators to modify the internal `$hasClassName` flag. * * @return $this */ protected function setHasClassName(bool $hasClassName) { $this->hasClassName = $hasClassName; return $this; } /** * Allows child generators to modify the internal `$sortImports` flag. * * @return $this */ protected function setSortImports(bool $sortImports) { $this->sortImports = $sortImports; return $this; } /** * Allows child generators to modify the internal `$enabledSuffixing` flag. * * @return $this */ protected function setEnabledSuffixing(bool $enabledSuffixing) { $this->enabledSuffixing = $enabledSuffixing; return $this; } /** * Gets a single command-line option. Returns TRUE if the option exists, * but doesn't have a value, and is simply acting as a flag. */ protected function getOption(string $name): bool|string|null { if (! array_key_exists($name, $this->params)) { return CLI::getOption($name); } return $this->params[$name] ?? true; } }