This commit is contained in:
Markus
2022-04-28 09:40:10 +02:00
commit 795794f992
9586 changed files with 1146991 additions and 0 deletions

219
system/CLI/BaseCommand.php Normal file
View File

@@ -0,0 +1,219 @@
<?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 Psr\Log\LoggerInterface;
use ReflectionException;
use Throwable;
/**
* BaseCommand is the base class used in creating CLI commands.
*
* @property array $arguments
* @property Commands $commands
* @property string $description
* @property string $group
* @property LoggerInterface $logger
* @property string $name
* @property array $options
* @property string $usage
*/
abstract class BaseCommand
{
/**
* The group the command is lumped under
* when listing commands.
*
* @var string
*/
protected $group;
/**
* The Command's name
*
* @var string
*/
protected $name;
/**
* the Command's usage description
*
* @var string
*/
protected $usage;
/**
* the Command's short description
*
* @var string
*/
protected $description;
/**
* the Command's options description
*
* @var array
*/
protected $options = [];
/**
* the Command's Arguments description
*
* @var array
*/
protected $arguments = [];
/**
* The Logger to use for a command
*
* @var LoggerInterface
*/
protected $logger;
/**
* Instance of Commands so
* commands can call other commands.
*
* @var Commands
*/
protected $commands;
public function __construct(LoggerInterface $logger, Commands $commands)
{
$this->logger = $logger;
$this->commands = $commands;
}
/**
* Actually execute a command.
*
* @param array<string, mixed> $params
*/
abstract public function run(array $params);
/**
* Can be used by a command to run other commands.
*
* @throws ReflectionException
*
* @return mixed
*/
protected function call(string $command, array $params = [])
{
return $this->commands->run($command, $params);
}
/**
* A simple method to display an error with line/file, in child commands.
*/
protected function showError(Throwable $e)
{
$exception = $e;
$message = $e->getMessage();
require APPPATH . 'Views/errors/cli/error_exception.php';
}
/**
* Show Help includes (Usage, Arguments, Description, Options).
*/
public function showHelp()
{
CLI::write(lang('CLI.helpUsage'), 'yellow');
if (! empty($this->usage)) {
$usage = $this->usage;
} else {
$usage = $this->name;
if (! empty($this->arguments)) {
$usage .= ' [arguments]';
}
}
CLI::write($this->setPad($usage, 0, 0, 2));
if (! empty($this->description)) {
CLI::newLine();
CLI::write(lang('CLI.helpDescription'), 'yellow');
CLI::write($this->setPad($this->description, 0, 0, 2));
}
if (! empty($this->arguments)) {
CLI::newLine();
CLI::write(lang('CLI.helpArguments'), 'yellow');
$length = max(array_map('strlen', array_keys($this->arguments)));
foreach ($this->arguments as $argument => $description) {
CLI::write(CLI::color($this->setPad($argument, $length, 2, 2), 'green') . $description);
}
}
if (! empty($this->options)) {
CLI::newLine();
CLI::write(lang('CLI.helpOptions'), 'yellow');
$length = max(array_map('strlen', array_keys($this->options)));
foreach ($this->options as $option => $description) {
CLI::write(CLI::color($this->setPad($option, $length, 2, 2), 'green') . $description);
}
}
}
/**
* Pads our string out so that all titles are the same length to nicely line up descriptions.
*
* @param int $extra How many extra spaces to add at the end
*/
public function setPad(string $item, int $max, int $extra = 2, int $indent = 0): string
{
$max += $extra + $indent;
return str_pad(str_repeat(' ', $indent) . $item, $max);
}
/**
* Get pad for $key => $value array output
*
* @deprecated Use setPad() instead.
*
* @codeCoverageIgnore
*/
public function getPad(array $array, int $pad): int
{
$max = 0;
foreach (array_keys($array) as $key) {
$max = max($max, strlen($key));
}
return $max + $pad;
}
/**
* Makes it simple to access our protected properties.
*
* @return mixed
*/
public function __get(string $key)
{
return $this->{$key} ?? null;
}
/**
* Makes it simple to check our protected properties.
*/
public function __isset(string $key): bool
{
return isset($this->{$key});
}
}

1000
system/CLI/CLI.php Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,80 @@
<?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 CodeIgniter\Controller;
use Config\Services;
use ReflectionException;
/**
* Command runner
*/
class CommandRunner extends Controller
{
/**
* Instance of class managing the collection of commands
*
* @var Commands
*/
protected $commands;
/**
* Constructor
*/
public function __construct()
{
$this->commands = Services::commands();
}
/**
* We map all un-routed CLI methods through this function
* so we have the chance to look for a Command first.
*
* @param string $method
* @param array ...$params
*
* @throws ReflectionException
*
* @return mixed
*/
public function _remap($method, ...$params)
{
// The first param is usually empty, so scrap it.
if (empty($params[0])) {
array_shift($params);
}
return $this->index($params);
}
/**
* Default command.
*
* @throws ReflectionException
*
* @return mixed
*/
public function index(array $params)
{
$command = array_shift($params) ?? 'list';
return $this->commands->run($command, $params);
}
/**
* Allows access to the current commands that have been found.
*/
public function getCommands(): array
{
return $this->commands->getCommands();
}
}

181
system/CLI/Commands.php Normal file
View File

@@ -0,0 +1,181 @@
<?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 CodeIgniter\Autoloader\FileLocator;
use CodeIgniter\Log\Logger;
use ReflectionClass;
use ReflectionException;
/**
* Core functionality for running, listing, etc commands.
*/
class Commands
{
/**
* The found commands.
*
* @var array
*/
protected $commands = [];
/**
* Logger instance.
*
* @var Logger
*/
protected $logger;
/**
* Constructor
*
* @param Logger|null $logger
*/
public function __construct($logger = null)
{
$this->logger = $logger ?? service('logger');
$this->discoverCommands();
}
/**
* Runs a command given
*/
public function run(string $command, array $params)
{
if (! $this->verifyCommand($command, $this->commands)) {
return;
}
// The file would have already been loaded during the
// createCommandList function...
$className = $this->commands[$command]['class'];
$class = new $className($this->logger, $this);
return $class->run($params);
}
/**
* Provide access to the list of commands.
*
* @return array
*/
public function getCommands()
{
return $this->commands;
}
/**
* Discovers all commands in the framework and within user code,
* and collects instances of them to work with.
*/
public function discoverCommands()
{
if ($this->commands !== []) {
return;
}
/** @var FileLocator $locator */
$locator = service('locator');
$files = $locator->listFiles('Commands/');
// If no matching command files were found, bail
// This should never happen in unit testing.
if ($files === []) {
return; // @codeCoverageIgnore
}
// Loop over each file checking to see if a command with that
// alias exists in the class.
foreach ($files as $file) {
$className = $locator->findQualifiedNameFromPath($file);
if (empty($className) || ! class_exists($className)) {
continue;
}
try {
$class = new ReflectionClass($className);
if (! $class->isInstantiable() || ! $class->isSubclassOf(BaseCommand::class)) {
continue;
}
/** @var BaseCommand $class */
$class = new $className($this->logger, $this);
if (isset($class->group)) {
$this->commands[$class->name] = [
'class' => $className,
'file' => $file,
'group' => $class->group,
'description' => $class->description,
];
}
unset($class);
} catch (ReflectionException $e) {
$this->logger->error($e->getMessage());
}
}
asort($this->commands);
}
/**
* Verifies if the command being sought is found
* in the commands list.
*/
public function verifyCommand(string $command, array $commands): bool
{
if (isset($commands[$command])) {
return true;
}
$message = lang('CLI.commandNotFound', [$command]);
if ($alternatives = $this->getCommandAlternatives($command, $commands)) {
if (count($alternatives) === 1) {
$message .= "\n\n" . lang('CLI.altCommandSingular') . "\n ";
} else {
$message .= "\n\n" . lang('CLI.altCommandPlural') . "\n ";
}
$message .= implode("\n ", $alternatives);
}
CLI::error($message);
CLI::newLine();
return false;
}
/**
* Finds alternative of `$name` among collection
* of commands.
*/
protected function getCommandAlternatives(string $name, array $collection): array
{
$alternatives = [];
foreach (array_keys($collection) as $commandName) {
$lev = levenshtein($name, $commandName);
if ($lev <= strlen($commandName) / 3 || strpos($commandName, $name) !== false) {
$alternatives[$commandName] = $lev;
}
}
ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE);
return array_keys($alternatives);
}
}

63
system/CLI/Console.php Normal file
View File

@@ -0,0 +1,63 @@
<?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 CodeIgniter\CodeIgniter;
use Exception;
/**
* Console
*/
class Console
{
/**
* Main CodeIgniter instance.
*
* @var CodeIgniter
*/
protected $app;
public function __construct(CodeIgniter $app)
{
$this->app = $app;
}
/**
* Runs the current command discovered on the CLI.
*
* @throws Exception
*
* @return mixed
*/
public function run(bool $useSafeOutput = false)
{
$path = CLI::getURI() ?: 'list';
// Set the path for the application to route to.
$this->app->setPath("ci{$path}");
return $this->app->useSafeOutput($useSafeOutput)->run();
}
/**
* Displays basic information about the Console.
*/
public function showHeader(bool $suppress = false)
{
if ($suppress) {
return;
}
CLI::write(sprintf('CodeIgniter v%s Command Line Tool - Server Time: %s UTC%s', CodeIgniter::CI_VERSION, date('Y-m-d H:i:s'), date('P')), 'green');
CLI::newLine();
}
}

View File

@@ -0,0 +1,34 @@
<?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\Exceptions;
use CodeIgniter\Exceptions\DebugTraceableTrait;
use RuntimeException;
/**
* CLIException
*/
class CLIException extends RuntimeException
{
use DebugTraceableTrait;
/**
* Thrown when `$color` specified for `$type` is not within the
* allowed list of colors.
*
* @return CLIException
*/
public static function forInvalidColor(string $type, string $color)
{
return new static(lang('CLI.invalidColor', [$type, $color]));
}
}

View File

@@ -0,0 +1,352 @@
<?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. Don't forget the ending backslash!
$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;
}
}