Working command structure in place. Just need docs and re-implement the Migration and Seed tools as commands.

This commit is contained in:
Lonnie Ezell 2016-12-16 00:00:46 -06:00
parent d0a8f7bd64
commit 49ec1083e5
No known key found for this signature in database
GPG Key ID: 88F86F2034554774
8 changed files with 356 additions and 382 deletions

View File

@ -0,0 +1,5 @@
<?php
// On the CLI, we still want errors in productions
// so just use the exception template.
include __DIR__.'/error_exception.php';

View File

@ -233,6 +233,40 @@ class FileLocator {
//--------------------------------------------------------------------
/**
* Scans the defined namespaces, returning a list of all files
* that are contained within the subpath specifed by $path.
*
* @param string $path
*
* @return array
*/
public function listFiles(string $path): array
{
if (empty($path)) return [];
$files = [];
helper('filesystem');
foreach ($this->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.

View File

@ -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;
}
//--------------------------------------------------------------------
/**
* Checks to see if this command has the listed task.
*
* @param string $alias
*
* @return bool
*/
public function hasTask(string $alias): bool
{
return array_key_exists(mb_strtolower($alias), $this->tasks);
}
abstract public function run(array $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.
* Can be used by a command to run other commands.
*
* @param string $alias
* @param string $command
* @param array $params
*/
public function runTask(string $alias, array $params)
protected function call(string $command, array $params=[])
{
if (! $this->hasTask($alias))
// The CommandRunner will grab the first element
// for the command name.
array_unshift($params, $command);
return $this->commands->index($params);
}
//--------------------------------------------------------------------
public function __get(string $key)
{
if (isset($this->$key))
{
throw new \InvalidArgumentException('Invalid alias: '. $alias);
return $this->$key;
}
$method = $this->tasks[$alias];
return $this->$method($params);
}
//--------------------------------------------------------------------

View File

@ -4,6 +4,15 @@ use CodeIgniter\Controller;
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 @@ class CommandRunner extends Controller
{
$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;
}
// Convert the command to UpperCamelCase
$command = str_replace(' ', '', ucwords(str_replace('_', ' ', $command)));
// The file would have already been loaded during the
// createCommandList function...
$className = $this->commands[$command]['class'];
$class = new $className($this->logger, $this);
$files = service('locator')->search("Commands/{$command}");
return $class->run($params);
}
//--------------------------------------------------------------------
/**
* 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;
}
//--------------------------------------------------------------------

View File

@ -0,0 +1,37 @@
<?php namespace CodeIgniter\Commands\Database;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* Creates a new migration file.
*
* @package CodeIgniter\Commands
*/
class CreateMigration extends BaseCommand
{
protected $group = 'Database';
/**
* The Command's name
*
* @var string
*/
protected $name = 'migrate:create';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Creates a new migration file.';
/**
* Displays the help for the ci.php cli script itself.
*/
public function run(array $params)
{
CLI::write('Usage:');
CLI::write("\tcommand [arguments]");
}
}

View File

@ -13,16 +13,36 @@ use CodeIgniter\CLI\CLI;
*/
class Help extends BaseCommand
{
protected $tasks = [
'help' => '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();
}
}

View File

@ -0,0 +1,137 @@
<?php namespace CodeIgniter\Commands;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
/**
* CI Help command for the ci.php script.
*
* Lists the basic usage information for the ci.php script,
* and provides a way to list help for other commands.
*
* @package CodeIgniter\Commands
*/
class ListCommands extends BaseCommand
{
protected $group = 'CodeIgniter';
/**
* The Command's name
*
* @var string
*/
protected $name = 'list';
/**
* the Command's short description
*
* @var string
*/
protected $description = 'Lists the available commands.';
/**
* The length of the longest command name.
* Used during display in columns.
*
* @var int
*/
protected $maxFirstLength = 0;
//--------------------------------------------------------------------
/**
* Displays the help for the ci.php cli script itself.
*
* @param array $params
*/
public function run(array $params)
{
$commands = $this->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;
}
//--------------------------------------------------------------------
}

View File

@ -1,330 +0,0 @@
<?php namespace CodeIgniter\Commands;
/**
* CodeIgniter
*
* An open source application development framework for PHP
*
* This content is released under the MIT License (MIT)
*
* Copyright (c) 2014 - 2016, British Columbia Institute of Technology
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*
* @package CodeIgniter
* @author CodeIgniter Dev Team
* @copyright Copyright (c) 2014 - 2016, British Columbia Institute of Technology (http://bcit.ca/)
* @license http://opensource.org/licenses/MIT MIT License
* @link http://codeigniter.com
* @since Version 3.0.0
* @filesource
*/
use CodeIgniter\CLI\CLI;
use CodeIgniter\Database\Seeder;
use CodeIgniter\Services;
use Config\Database;
/**
* Class MigrationsCommand.
*
* Migrations controller.
*/
class MigrationsCommand extends \CodeIgniter\Controller
{
/**
* Migration runner.
*
* @var \CodeIgniter\Database\MigrationRunner
*/
protected $runner;
//--------------------------------------------------------------------
/**
* Constructor.
*/
public function __construct()
{
$this->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 =<<<EOD
<?php
use CodeIgniter\Database\Migration;
class Migration_{name} extends Migration
{
public function up()
{
//
}
//--------------------------------------------------------------------
public function down()
{
//
}
}
EOD;
$template = str_replace('{name}', $name, $template);
helper('filesystem');
if (! write_file($path, $template))
{
CLI::error(lang('Migrations.migWriteError'));
return;
}
CLI::write('Done.');
}
/**
* Displays a caught exception.
*
* @param \Exception $e
*/
protected function showError(\Exception $e)
{
CLI::error($e->getMessage());
CLI::write($e->getFile().' - '.$e->getLine(), 'white');
}
//--------------------------------------------------------------------
}