Skip to content

Commit

Permalink
Refactor Generators
Browse files Browse the repository at this point in the history
  • Loading branch information
mostafakhudair committed Jan 7, 2021
1 parent 659be22 commit 7d27f64
Show file tree
Hide file tree
Showing 47 changed files with 2,246 additions and 1,828 deletions.
16 changes: 8 additions & 8 deletions app/Config/Generators.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,13 @@ class Generators extends BaseConfig
* @var array<string, string>
*/
public $views = [
'make:command' => 'CodeIgniter\\Commands\\Generators\\Views\\command.tpl.php',
'make:controller' => 'CodeIgniter\\Commands\\Generators\\Views\\controller.tpl.php',
'make:entity' => 'CodeIgniter\\Commands\\Generators\\Views\\entity.tpl.php',
'make:filter' => 'CodeIgniter\\Commands\\Generators\\Views\\filter.tpl.php',
'make:migration' => 'CodeIgniter\\Commands\\Generators\\Views\\migration.tpl.php',
'make:model' => 'CodeIgniter\\Commands\\Generators\\Views\\model.tpl.php',
'make:seeder' => 'CodeIgniter\\Commands\\Generators\\Views\\seed.tpl.php',
'session:migration' => 'CodeIgniter\\Commands\\Generators\\Views\\session_migration.tpl.php',
'make:command' => 'CodeIgniter\Commands\Generators\Views\command.tpl.php',
'make:controller' => 'CodeIgniter\Commands\Generators\Views\controller.tpl.php',
'make:entity' => 'CodeIgniter\Commands\Generators\Views\entity.tpl.php',
'make:filter' => 'CodeIgniter\Commands\Generators\Views\filter.tpl.php',
'make:migration' => 'CodeIgniter\Commands\Generators\Views\migration.tpl.php',
'make:model' => 'CodeIgniter\Commands\Generators\Views\model.tpl.php',
'make:seeder' => 'CodeIgniter\Commands\Generators\Views\seeder.tpl.php',
'session:migration' => 'CodeIgniter\Commands\Generators\Views\session_migration.tpl.php',
];
}
325 changes: 325 additions & 0 deletions system/CLI/GeneratorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
<?php

/**
* This file is part of the CodeIgniter 4 framework.
*
* (c) CodeIgniter Foundation <[email protected]>
*
* 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 Config\Services;
use RuntimeException;
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 sort class imports.
*
* @internal
*
* @var boolean
*/
private $sortImports = true;

/**
* The params array for easy access by other methods.
*
* @internal
*
* @var array
*/
private $params = [];
private $nm = true;

/**
* Execute the command.
*
* @param array $params
*
* @return void
*/
protected function execute(array $params): void
{
$this->params = $params;

if ($this->getOption('namespace') === 'CodeIgniter' && $this->nm)
{
CLI::write(lang('CLI.generator.usingCINamespace'), 'yellow');
CLI::newLine();

// @codeCoverageIgnoreStart
if (CLI::prompt('Are you sure you want to continue?', ['y', 'n'], 'required') === 'n')
{
CLI::newLine();
CLI::write(lang('CLI.cancelOperation'), 'light_yellow');
CLI::newLine();

return;
}
// @codeCoverageIgnoreEnd

CLI::newLine();
}

$this->nm = false;

// Get the fully qualified class name from the input.
$class = $this->qualifyClassName();

// Get the file path from class name.
$path = $this->buildPath($class);

// Overwriting files unknowingly is a serious annoyance.
// So we'll check if we are duplicating things.
// If the 'force' option is not supplied, we bail.
if (! $this->getOption('force') && file_exists($path))
{
CLI::write(lang('CLI.generator.fileExist', [CLI::color(clean_path($path), '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)))
{
CLI::error(lang('CLI.generator.fileError', [clean_path($path)]), 'light_gray', 'red');
CLI::newLine();

return;
}

if ($this->getOption('force') && file_exists($path))
{
CLI::write(lang('CLI.generator.fileOverwrite', [CLI::color(clean_path($path), 'yellow')]));
CLI::newLine();

return;
}

CLI::write(lang('CLI.generator.fileCreate', [CLI::color(clean_path($path), 'light_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
*/
private function qualifyClassName(): string
{
// Gets the class name from input.
$class = $this->params[0] ?? CLI::getSegment(2) ?? '';

if (empty($class))
{
$class = CLI::prompt(lang('CLI.generator.className'), null, 'required'); // @codeCoverageIgnore
CLI::newLine();
}

helper('inflector');

$component = strtolower(singular($this->component));
$class = strtolower($class);
$class = strpos($class, $component) ? str_replace($component, ucfirst($component), $class) : $class;

if ($this->getOption('suffix') && ! stripos($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 root namespace from input.
$root = trim(str_replace('/', '\\', $this->getOption('namespace') ?? APP_NAMESPACE), '\\');

if (strncmp($class, $root, strlen($root)) === 0)
{
return $class;
}

return $root . '\\' . $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
*/
private 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
*/
private 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
*/
private 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
*/
private function buildPath(string $class): string
{
$root = 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($root);

if (! $base = reset($base))
{
throw new RuntimeException(lang('CLI.namespaceNotDefined', [$root]));
}

$base = realpath($base) ?: $base;
$file = $base . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, trim(str_replace($root, '', $class), '\\')) . '.php';

return implode(DIRECTORY_SEPARATOR, array_slice(explode(DIRECTORY_SEPARATOR, $file), 0, -1)) . DIRECTORY_SEPARATOR . $this->basename($file);
}

/**
* 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 boolean|mixed|null
*/
protected function getOption(string $name)
{
if (! array_key_exists($name, $this->params))
{
return CLI::getOption($name);
}

// If the param doesn't have a value, simply return TRUE
// so they know it was set, otherwise return the actual value.
return is_null($this->params[$name]) ? true : $this->params[$name];
}
}
Loading

0 comments on commit 7d27f64

Please sign in to comment.