Description
The current Shell/Tasks classes have a number of rough spots which are mostly due to features being incrementally added over the years. A short list of the problems I see are as follows:
- Public properties for
args
andparams
. These simple arrays have been partially wrapped with methods, but they are still a bit clunky to use. - Subcommands can be tasks or shell methods. Having both options makes it hard to give people clear directions on which approach they should use.
- The API of Shell is confusing. Feature both methods like
out()
err()
andabort()
as well as user land code. Shell has too many concerns.
Focused Commands
If shells were stripped down their goals are to provide a series of 'commands'. Each command is effectively a callable that can be invoked from the CLI. The callable could have the input/output wrappers passed in removing the need for most state in a command.
Sub-commands can be created by mounting commands in a nested fashion. For example bake test
could be a name that points to Bake\Console\Console\TestCommand
while bake controller
could be a name that resolves to Bake\Console\Console\ControllerCommand
. Each command can focus on only the task they need to accomplish.
Code Reuse in Commands
If each command is single purpose, we'll need a good way to share logic between commands. We've had good success so far with Traits being utilized to allow access to models, logging and mailers. We can leverage those traits in Commands to cover most of the need for shared logic. In addition to traits, we can improve ShellHelpers to enable more code re-use.
Shared option parser settings is another place where I can see logic needing to be shared. This can be addressed by enabling option parsers to be standalone classes like we did with validators.
Command Life-Cycle
Commands would have the following life-cycle:
CommandRunner
locates a command based on the CLI arguments.CommandRunner
constructs the command. TheCommandCollection
would be injected if necessary.- The
Command.initialize
event would be triggered. Calling the command'sinitialize()
hook if defined. - The command's OptionParser is created with
getOptionParser()
- CLI options are parsed. An error will be output if required options/arguments are missing.
- Help might be displayed if requested.
- The command's
execute()
method would be called receiving theInputArguments
andConsoleIo
objects as parameters. - The return value of the command would be converted into an exit code, and passed out to the CLI environment.
Arguments class
The Arguments
class provides an interface for interacting with the parameters and passed arguments in a command. It is passed to commands after the CLI arguments have been parsed, and would feature methods like:
// Get all parameters
$input->getParams();
// Get the parameter. No default option as defaults are defined in the option parser.
$input->getParam('debug');
// Bool if parameter is defined.
$input->hasParam('optional_thing');
// Get all arguments
$input->getArguments();
// Get the first argument by position
$input->getArgumentAt(0);
// Get arguments by name
$input->getArgument('classname');
// Bool if argument exists
$input->hasArgumentAt(0); $input->hasArgument('classname');
ConsoleIo & ConsoleOptionParser
Theses classes would be unchanged from today. I think the current API of them works pretty well.
Command Example
An example showing the various classes in concert could look like:
<?php
// Namespace change
namespace App\Console;
use Cake\Console\Command;
use Cake\Console\ConsoleIo;
use Cake\Console\Arguments;
class OrmCacheBuildCommand extends Command
{
public function execute(Arguments $input, ConsoleIo $io)
{
$schema = $this->getSchema($input->getParam('connection'));
if (!$schema) {
$io->err('No schema found.');
return false;
}
$tables = $input->getArguments();
if (empty($tables)) {
$tables = $schema->listTables();
}
foreach ($tables as $table) {
$io->verbose('Building metadata cache for ' . $table);
$schema->describe($table, ['forceRefresh' => true]);
}
$io->out('<success>Cache build complete</success>');
return true;
}
}
Command Bundles
If commands are single purpose, we'll want a way for console 'applications' to be built in a way that host applications can easily consume them. Take bake
as an example. A developer should not have to manually add all of the bake commands to their command list. Instead we'll need a simple way for them to add all the commands as a single bundle. Self contained console applications will likely come from plugins. We can extend Application
to handle plugin loading and introduce 'plugin classes' that interact with the application's bootstrapping process to add console commands.
The core console commands could be added through the same 'plugin class' system. This should remove the need to autodiscover commands, as both CakePHP and plugins will use the same system to add their commands. At a high-level, 'plugin classes' would add their console commands before Application::console()
is called so that the application can replace/remove any commands provided by plugins.
Subcommands
Sub-commands do not exist in a single purpose command class system. Instead 'subcommands' are bound to the command collection as commands with spaces in their names.
$commands->add('bake controller', ControllerCommand::class);
Shell will exist in 4.0
While we would be deprecating Shell
in 3.6, we would not be removing it in 4.0. Deprecating and removing a key base class like Shell this late in the life of 3.6 will make upgrading to 4.0 harder as applications & plugins will need to rebuild all of their shell commands. Instead, Shell
will not be removed until 5.0.0
Changelog
- Clarify when Shell would be removed.
- Separate methods for reading positional arguments by index an name.
- Fix initialize order for commands.
- Rename InputArguments to Arguments