<?php /** * This file is part of CodeIgniter 4 framework. * * (c) CodeIgniter Foundation <admin@codeigniter.com> * * For the full copyright and license information, please view * the LICENSE file that was distributed with this source code. */ namespace CodeIgniter\CLI; use Config\Services; 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; /** * View template name * * @var string */ protected $template; /** * Language string key for required class names. * * @var string */ protected $classNameLang = ''; /** * 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. */ protected function execute(array $params): void { $this->params = $params; 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 } // Get the fully qualified class name from the input. $class = $this->qualifyClassName(); // Get the file path from class name. $path = $this->buildPath($class); // Check if path is empty. if (empty($path)) { return; } $isFile = is_file($path); // 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($path)]), 'light_gray', 'red'); CLI::newLine(); return; } // Check if the directory to save the file is existing. $dir = dirname($path); 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($path, $this->buildContent($class))) { // @codeCoverageIgnoreStart CLI::error(lang('CLI.generator.fileError', [clean_path($path)]), 'light_gray', 'red'); CLI::newLine(); return; // @codeCoverageIgnoreEnd } if ($this->getOption('force') && $isFile) { CLI::write(lang('CLI.generator.fileOverwrite', [clean_path($path)]), 'yellow'); CLI::newLine(); return; } CLI::write(lang('CLI.generator.fileCreate', [clean_path($path)]), 'green'); CLI::newLine(); } /** * Prepare options and do the necessary replacements. */ 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 { // Gets the class name from input. $class = $this->params[0] ?? CLI::getSegment(2); if ($class === null && $this->hasClassName) { // @codeCoverageIgnoreStart $nameLang = $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/1 */ $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') && ! strripos($class, $component)) { $class .= ucfirst($component); } // Trims input, normalize separators, and ensure that all paths are in Pascalcase. $class = ltrim(implode('\\', array_map('pascalize', explode('\\', str_replace('/', '\\', trim($class))))), '\\/'); // Gets the namespace from input. $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\'); if (strncmp($class, $namespace, strlen($namespace)) === 0) { return $class; // @codeCoverageIgnore } return $namespace . '\\' . $this->directory . '\\' . str_replace('/', '\\', $class); } /** * Gets the generator view as defined in the `Config\Generators::$views`, * with fallback to `$template` when the defined view does not exist. */ protected function renderTemplate(array $data = []): string { try { return view(config('Generators')->views[$this->name], $data, ['debug' => false]); } catch (Throwable $e) { log_message('error', $e->getMessage()); return view("CodeIgniter\\Commands\\Generators\\Views\\{$this->template}", $data, ['debug' => false]); } } /** * Performs pseudo-variables contained within view file. */ 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[] = '<?php'; $replace[] = $namespace; $replace[] = str_replace($namespace . '\\', '', $class); return str_replace($search, $replace, $this->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<imports>(?:^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. */ protected function buildPath(string $class): string { $namespace = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\'); // Check if the namespace is actually defined and we are not just typing gibberish. $base = Services::autoloader()->getNamespace($namespace); if (! $base = reset($base)) { CLI::error(lang('CLI.namespaceNotDefined', [$namespace]), 'light_gray', 'red'); CLI::newLine(); return ''; } $base = realpath($base) ?: $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); } /** * 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. * * @return mixed */ protected function getOption(string $name) { if (! array_key_exists($name, $this->params)) { return CLI::getOption($name); } return $this->params[$name] ?? true; } }