<?php /** * This file is part of the 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; /** * Whether to require class name. * * @internal * * @var boolean */ private $hasClassName = true; /** * Whether to sort class imports. * * @internal * * @var boolean */ private $sortImports = true; /** * The params array for easy access by other methods. * * @internal * * @var array */ private $params = []; /** * Execute the command. * * @param array $params * * @return void */ 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. * * @param string $class * * @return string */ 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. * * @param string $filename * * @return string */ protected function basename(string $filename): string { return basename($filename); } /** * Parses the class name and checks if it is already qualified. * * @return string */ protected function qualifyClassName(): string { // Gets the class name from input. $class = $this->params[0] ?? CLI::getSegment(2); if (is_null($class) && $this->hasClassName) { // @codeCoverageIgnoreStart $class = CLI::prompt(lang('CLI.generator.className'), null, 'required'); CLI::newLine(); // @codeCoverageIgnoreEnd } helper('inflector'); $component = strtolower(singular($this->component)); $class = strtolower($class); $class = strpos($class, $component) !== false ? str_replace($component, ucfirst($component), $class) : $class; if ($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. * * @param array $data Data to be passed to the view. * * @return string */ 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. * * @param string $class * @param array $search * @param array $replace * @param array $data * * @return string */ 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)), '\\'); array_push($search, '<@php', '{namespace}', '{class}'); array_push($replace, '<?php', $namespace, 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. * * @param string $class * * @return string */ 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. * * @param string $class * * @return string */ 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)) { // @codeCoverageIgnoreStart CLI::error(lang('CLI.namespaceNotDefined', [$namespace]), 'light_gray', 'red'); CLI::newLine(); return ''; // @codeCoverageIgnoreEnd } $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. * * @param boolean $hasClassName * * @return $this */ protected function setHasClassName(bool $hasClassName) { $this->hasClassName = $hasClassName; return $this; } /** * Allows child generators to modify the internal `$sortImports` flag. * * @param boolean $sortImports * * @return $this */ protected function setSortImports(bool $sortImports) { $this->sortImports = $sortImports; 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. * * @param string $name * * @return mixed */ protected function getOption(string $name) { if (! array_key_exists($name, $this->params)) { return CLI::getOption($name); } return is_null($this->params[$name]) ? true : $this->params[$name]; } }