diff --git a/application/Views/errors/cli/production.php b/application/Views/errors/cli/production.php new file mode 100644 index 000000000000..f04d517d2145 --- /dev/null +++ b/application/Views/errors/cli/production.php @@ -0,0 +1,5 @@ +namespaces as $namespace => $nsPath) + { + $fullPath = rtrim($nsPath, '/') .'/'. $path; + + if (! is_dir($fullPath)) continue; + + $tempFiles = get_filenames($fullPath, true); + + if (! count($tempFiles)) + { + continue; + } + + $files = array_merge($files, $tempFiles); + } + + return $files; + } + /** * Checks the application folder to see if the file can be found. * Only for use with filenames that DO NOT include namespacing. diff --git a/system/CLI/BaseCommand.php b/system/CLI/BaseCommand.php index da0f97a1d8c0..47258a4bf4a7 100644 --- a/system/CLI/BaseCommand.php +++ b/system/CLI/BaseCommand.php @@ -2,64 +2,89 @@ use Psr\Log\LoggerInterface; -class BaseCommand +/** + * Class BaseCommand + * + * @property $group + * @property $name + * @property $description + * + * @package CodeIgniter\CLI + */ +abstract class BaseCommand { /** - * List of aliases and the method - * in this class they point to. - * Allows for more flexible naming of - * CLI commands. + * The group the command is lumped under + * when listing commands. * - * @var array + * @var string */ - protected $tasks = []; + protected $group; + + /** + * The Command's name + * + * @var string + */ + protected $name; + + /** + * the Command's short description + * + * @var string + */ + protected $description; /** * @var \Psr\Log\LoggerInterface */ protected $logger; + /** + * Instance of the CommandRunner controller + * so commands can call other commands. + * + * @var \CodeIgniter\CLI\CommandRunner + */ + protected $commands; + //-------------------------------------------------------------------- - public function __construct(LoggerInterface $logger) + public function __construct(LoggerInterface $logger, CommandRunner $commands) { $this->logger = $logger; + $this->commands = $commands; } //-------------------------------------------------------------------- + abstract public function run(array $params); + + //-------------------------------------------------------------------- + /** - * Checks to see if this command has the listed task. + * Can be used by a command to run other commands. * - * @param string $alias - * - * @return bool + * @param string $command + * @param array $params */ - public function hasTask(string $alias): bool + protected function call(string $command, array $params=[]) { - return array_key_exists(mb_strtolower($alias), $this->tasks); + // The CommandRunner will grab the first element + // for the command name. + array_unshift($params, $command); + + return $this->commands->index($params); } //-------------------------------------------------------------------- - /** - * Used by the commandRunner to actually execute the command by - * it's alias. $params are passed to the command in the order - * presented on the command line. - * - * @param string $alias - * @param array $params - */ - public function runTask(string $alias, array $params) + public function __get(string $key) { - if (! $this->hasTask($alias)) + if (isset($this->$key)) { - throw new \InvalidArgumentException('Invalid alias: '. $alias); + return $this->$key; } - - $method = $this->tasks[$alias]; - - return $this->$method($params); } //-------------------------------------------------------------------- diff --git a/system/CLI/CommandRunner.php b/system/CLI/CommandRunner.php index 2658c11e9b64..45b6c5048676 100644 --- a/system/CLI/CommandRunner.php +++ b/system/CLI/CommandRunner.php @@ -4,6 +4,15 @@ class CommandRunner extends Controller { + /** + * Stores the info about found Commands. + * + * @var array + */ + protected $commands = []; + + //-------------------------------------------------------------------- + /** * We map all un-routed CLI methods through this function * so we have the chance to look for a Command first. @@ -28,57 +37,94 @@ public function index(array $params) { $command = array_shift($params); - $class = $this->locateCommand($command); + $this->createCommandList($command); - return $class->runTask($command, $params); + return $this->runCommand($command, $params); } //-------------------------------------------------------------------- /** - * Attempts to find a class matching our snake_case to UpperCamelCase - * version of the command passed to us. + * Actually runs the command. * * @param string $command - * - * @return null */ - protected function locateCommand(string $command) + protected function runCommand(string $command, array $params) { - if (empty($command)) return; + if (! isset($this->commands[$command])) + { + CLI::error('Command \''.$command.'\' not found'); + CLI::newLine(); + return; + } + + // The file would have already been loaded during the + // createCommandList function... + $className = $this->commands[$command]['class']; + $class = new $className($this->logger, $this); - // Convert the command to UpperCamelCase - $command = str_replace(' ', '', ucwords(str_replace('_', ' ', $command))); + return $class->run($params); + } + + //-------------------------------------------------------------------- - $files = service('locator')->search("Commands/{$command}"); + /** + * Scans all Commands directories and prepares a list + * of each command with it's group and file. + * + * @return null|void + */ + protected function createCommandList() + { + $files = service('locator')->listFiles("Commands/"); // If no matching command files were found, bail if (empty($files)) { - return null; + return; } // Loop over each file checking to see if a command with that // alias exists in the class. If so, return it. Otherwise, try the next. foreach ($files as $file) { - $class = service('locator')->findQualifiedNameFromPath($file); + $className = service('locator')->findQualifiedNameFromPath($file); - if (empty($class)) + if (empty($className)) { continue; } - $class = new $class($this->logger); + $class = new $className($this->logger, $this); - if ($class->hasTask($command)) + // Store it! + if ($class->group !== null) { - return $class; + $this->commands[$class->name] = [ + 'class' => $className, + 'file' => $file, + 'group' => $class->group, + 'description' => $class->description + ]; } $class = null; unset($class); } + + asort($this->commands); + } + + //-------------------------------------------------------------------- + + /** + * Allows access to the current commands that have been found. + * + * @return array + */ + public function getCommands() + { + return $this->commands; } //-------------------------------------------------------------------- diff --git a/system/Commands/Database/CreateMigration.php b/system/Commands/Database/CreateMigration.php new file mode 100644 index 000000000000..d570f2553b73 --- /dev/null +++ b/system/Commands/Database/CreateMigration.php @@ -0,0 +1,37 @@ + 'index' - ]; + protected $group = 'CodeIgniter'; + + /** + * The Command's name + * + * @var string + */ + protected $name = 'help'; + + /** + * the Command's short description + * + * @var string + */ + protected $description = 'Displays basic usage information.'; + + //-------------------------------------------------------------------- /** * Displays the help for the ci.php cli script itself. + * + * @param array $params */ - public function index() + public function run(array $params) { CLI::write('Usage:'); - CLI::write("\tcommands [arguments]"); + CLI::write("\tcommand [arguments]"); + + $this->call('list'); + + CLI::newLine(); } } diff --git a/system/Commands/ListCommands.php b/system/Commands/ListCommands.php new file mode 100644 index 000000000000..c12ab95107eb --- /dev/null +++ b/system/Commands/ListCommands.php @@ -0,0 +1,137 @@ +commands->getCommands(); + + $this->describeCommands($commands); + + CLI::newLine(); + } + + //-------------------------------------------------------------------- + + /** + * Displays the commands on the CLI. + * + * @param array $commands + */ + protected function describeCommands(array $commands = []) + { + $names = array_keys($commands); + $descs = array_column($commands, 'description'); + $groups = array_column($commands, 'group'); + $lastGroup = ''; + + // Pad each item to the same length + $names = $this->padArray($names, 2, 2); + + for ($i = 0; $i < count($names); $i++) + { + $lastGroup = $this->describeGroup($groups[$i], $lastGroup); + + $out = CLI::color($names[$i], 'yellow'); + + if (isset($descs[$i])) + { + $out .= CLI::wrap($descs[$i], 125, strlen($names[$i])); + } + + CLI::write($out); + } + } + + //-------------------------------------------------------------------- + + /** + * Outputs the description, if necessary. + * + * @param string $new + * @param string $old + * + * @return string + */ + protected function describeGroup(string $new, string $old) + { + if ($new == $old) + { + return $old; + } + + CLI::newLine(); + CLI::write($new); + + return $new; + } + + //-------------------------------------------------------------------- + + /** + * Returns a new array where all of the string elements have + * been padding with trailing spaces to be the same length. + * + * @param array $array + * @param int $extra // How many extra spaces to add at the end + * + * @return array + */ + protected function padArray($array, $extra = 2, $indent=0) + { + $max = max(array_map('strlen', $array))+$extra+$indent; + + foreach ($array as &$item) + { + $item = str_repeat(' ', $indent).$item; + $item = str_pad($item, $max); + } + + return $array; + } + + //-------------------------------------------------------------------- + +} diff --git a/system/Commands/MigrationsCommand.php b/system/Commands/MigrationsCommand.php deleted file mode 100644 index a323273a9d8f..000000000000 --- a/system/Commands/MigrationsCommand.php +++ /dev/null @@ -1,330 +0,0 @@ -runner = Services::migrations(); - } - - //-------------------------------------------------------------------- - - /** - * Provides a list of available commands. - */ - public function index() - { - CLI::write('Migration Commands', 'white'); - CLI::write(CLI::color('latest', 'yellow'). lang('Migrations.migHelpLatest')); - CLI::write(CLI::color('current', 'yellow'). lang('Migrations.migHelpCurrent')); - CLI::write(CLI::color('version [v]', 'yellow'). lang('Migrations.migHelpVersion')); - CLI::write(CLI::color('rollback', 'yellow'). lang('Migrations.migHelpRollback')); - CLI::write(CLI::color('refresh', 'yellow'). lang('Migrations.migHelpRefresh')); - CLI::write(CLI::color('seed [name]', 'yellow'). lang('Migrations.migHelpSeed')); - CLI::write(CLI::color('create [name]', 'yellow'). lang('Migrations.migCreate')); - } - - //-------------------------------------------------------------------- - - - /** - * Ensures that all migrations have been run. - */ - public function latest() - { - CLI::write(lang('Migrations.migToLatest'), 'yellow'); - - try { - $this->runner->latest(); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Migrates the database up or down to get to the specified version. - * - * @param int $version - */ - public function version(int $version = null) - { - if (is_null($version)) - { - $version = CLI::prompt(lang('Migrations.version')); - } - - if (is_null($version)) - { - CLI::error(lang('Migrations.invalidVersion')); - exit(); - } - - CLI::write(sprintf(lang('Migrations.migToVersionPH'), $version), 'yellow'); - - try { - $this->runner->version($version); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Migrates us up or down to the version specified as $currentVersion - * in the migrations config file. - */ - public function current() - { - CLI::write(lang('Migrations.migToVersion'), 'yellow'); - - try { - $this->runner->current(); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Runs all of the migrations in reverse order, until they have - * all been un-applied. - */ - public function rollback() - { - CLI::write(lang('Migrations.migRollingBack'), 'yellow'); - - try { - $this->runner->version(0); - } - catch (\Exception $e) - { - $this->showError($e); - } - - CLI::write('Done'); - } - - //-------------------------------------------------------------------- - - /** - * Does a rollback followed by a latest to refresh the current state - * of the database. - */ - public function refresh() - { - $this->rollback(); - $this->latest(); - } - - //-------------------------------------------------------------------- - - /** - * Displays a list of all migrations and whether they've been run or not. - */ - public function status() - { - $migrations = $this->runner->findMigrations(); - $history = $this->runner->getHistory(); - - if (empty($migrations)) - { - return CLI::error(lang('Migrations.migNoneFound')); - } - - $max = 0; - - foreach ($migrations as $version => $file) - { - $file = substr($file, strpos($file, $version.'_')); - $migrations[$version] = $file; - - $max = max($max, strlen($file)); - } - - CLI::write(str_pad(lang('Migrations.filename'), $max+4).lang('Migrations.migOn'), 'yellow'); - - foreach ($migrations as $version => $file) - { - $date = ''; - foreach ($history as $row) - { - if ($row['version'] != $version) continue; - - $date = $row['time']; - } - - CLI::write(str_pad($file, $max+4). ($date ? $date : '---')); - } - } - - //-------------------------------------------------------------------- - - /** - * Runs the specified Seeder file to populate the database - * with some data. - * - * @param string $seedName - */ - public function seed(string $seedName = null) - { - $seeder = new Seeder(new \Config\Database()); - - if (empty($seedName)) - { - $seedName = CLI::prompt(lang('Migrations.migSeeder'), 'DatabaseSeeder'); - } - - if (empty($seedName)) - { - CLI::error(lang('Migrations.migMissingSeeder')); - return; - } - - try - { - $seeder->call($seedName); - } - catch (\Exception $e) - { - $this->showError($e); - } - } - - //-------------------------------------------------------------------- - - public function create(string $name = null) - { - if (empty($name)) - { - $name = CLI::prompt(lang('Migrations.migNameMigration')); - } - - if (empty($name)) - { - CLI::error(lang('Migrations.migBadCreateName')); - return; - } - - $path = APPPATH.'Database/Migrations/'.date('YmdHis_').$name.'.php'; - - $template =<<getMessage()); - CLI::write($e->getFile().' - '.$e->getLine(), 'white'); - } - - //-------------------------------------------------------------------- - -}