Creating reusable shell tasks

Just as we have components to share functionality amongst controllers, we also have behaviors for models, and helpers for views. What about shells? CakePHP offers the concept of tasks, which are classes that also extend from the Shell class, but can be reused from other shells.

In this recipe, we will learn how to build a task that handles argument and parameter processing for our shell, can auto-generate help messages, and check the definition of mandatory arguments and optional parameters. We will implement this task in the most generic fashion, so we can use it for any future shells we may decide to build.

Getting ready

To go through this recipe we need a shell that accepts parameters and has different commands available. Follow the entire recipe Parsing command line parameters.

How to do it...

  1. Edit your app/vendors/shells/user.php file and add the following right below the declaration of the uses property:
    public $tasks = array('Help'),
    public static $commands = array(
    'add',
    'import' => array(
    'help' => 'Import user records from a CSV file',
    'args' => array(
    'path' => array(
    'help' => 'Path to CSV file',
    'mandatory' => true
    )
    ),
    'params' => array(
    'limit' => array(
    'type' => 'int',
    'help' => 'import up to N records'
    ),
    'size' => array(
    'value' => 10,
    'type' => 'int',
    'help' => 'size of generated password'
    ),
    'verbose' => array(
    'value' => false,
    'type' => 'bool',
    'help' => 'Verbose output'
    )
    )
    )
    );
    
  2. While still editing the shell, remove the help() method, and remove the following lines from the beginning of the import() method:
    $this->_checkArgs(1);
    $defaults = array(
    'limit' => null,
    'size' => 10,
    'verbose' => false
    );
    $options = array_merge(
    $defaults,
    array_intersect_key($this->params, $defaults)
    );
    $path = $this->args[0];
    
  3. Add the following lines at the beginning of the import() method:
    $options = $this->Help->parameters;
    extract($this->Help->arguments);
    
  4. Create a file named help.php and place it in your app/vendors/shells/tasks, with the following contents:
    <?php
    class HelpTask extends Shell {
    public $parameters = array();
    public $arguments = array();
    protected $commands = array();
    public function initialize() {
    $shellClass = Inflector::camelize($this->shell).'Shell';
    $vars = get_class_vars($shellClass);
    if (!empty($vars['commands'])) {
    foreach($vars['commands'] as $command => $settings) {
    if (is_numeric($command)) {
    $command = $settings;
    $settings = array();
    }
    if (!empty($settings['args'])) {
    $args = array();
    foreach($settings['args'] as $argName => $arg) {
    if (is_numeric($argName)) {
    $argName = $arg;
    $arg = array();
    }
    $args[$argName] = array_merge(array(
    'help' => null,
    'mandatory' => false
    ), $arg);
    }
    $settings['args'] = $args;
    }
    if (!empty($settings['params'])) {
    $params = array();
    foreach($settings['params'] as $paramName => $param) {
    if (is_numeric($paramName)) {
    $paramName = $param;
    $param = array();
    }
    $params[$paramName] = array_merge(array(
    'help' => null,
    'type' => 'string'
    ), $param);
    }
    }
    $this->commands[$command] = array_merge(array(
    'help' => null,
    'args' => array(),
    'params' => array()
    ), $settings);
    }
    }
    if (empty($this->command) && !in_array('main', get_class_methods($shellClass))) {
    $this->_welcome();
    $this->_help();
    } elseif (!empty($this->command) && array_key_exists($this->command, $this->commands)) {
    $command = $this->commands[$this->command];
    $number = count(array_filter(Set::extract(array_values($command['args']), '/mandatory')));
    if ($number > 0 && (count($this->args) - 1) < $number) {
    $this->err('WRONG number of parameters'),
    $this->out();
    $this->_help($this->command);
    } elseif ($number > 0) {
    $i = 0;
    foreach($command['args'] as $argName => $arg) {
    if ($number >= $i && isset($this->args[$i+1])) {
    $this->arguments[$argName] = $this->args[$i+1];
    }
    $i++;
    }
    }
    $values = array_intersect_key($this->params, $command['params']);
    foreach($command['params'] as $settingName => $setting) {
    if (!array_key_exists($settingName, $values)) {
    $this->parameters[$settingName] = array_key_exists('value', $setting) ?
    $setting['value'] :
    null;
    } elseif ($setting['type'] == 'int' && !is_numeric($values[$settingName])) {
    $this->err('ERROR: wrong value for '.$settingName);
    $this->out();
    $this->_help($this->command);
    } else {
    if ($setting['type'] == 'bool') {
    $values[$settingName] = !empty($values[$settingName]);
    }
    $this->parameters[$settingName] = $values[$settingName];
    }
    }
    }
    }
    }
    
  5. Add the following methods to the created HelpTask class:
    public function execute() {
    $this->_help(!empty($this->args) ? $this->args[0] : null);
    }
    protected function _help($command = null) {
    $usage = 'cake '.$this->shell;
    if (empty($this->commands)) {
    $this->out($usage);
    return;
    }
    $lines = array();
    $usages = array();
    if (empty($command) || !array_key_exists($command, $this->commands)) {
    foreach(array_keys($this->commands) as $currentCommand) {
    $usages[] = $this->_usageCommand($currentCommand);
    if (!empty($lines)) {
    $lines[] = null;
    }
    $lines = array_merge($lines, $this->_helpCommand($currentCommand));
    }
    } else {
    $usages = (array) $this->_usageCommand($command);
    $lines = $this->_helpCommand($command);
    }
    if (!empty($usages)) {
    $usage .= ' ';
    if (empty($command)) {
    $usage .= '<';
    }
    $usage .= implode(' | ', $usages);
    if (empty($command)) {
    $usage .= '>';
    }
    }
    $this->out($usage);
    if (!empty($lines)) {
    $this->out();
    foreach($lines as $line) {
    $this->out($line);
    }
    }
    $this->_stop();
    }
    
  6. While still editing the HelpTask class, add the following helper methods to the class:
    protected function _usageCommand($command) {
    $usage = $command;
    if (!empty($this->commands[$command]['args'])) {
    foreach($this->commands[$command]['args'] as $argName => $arg) {
    $usage .= ' ' . ($arg['mandatory'] ? '<' : '['),
    $usage .= $argName;
    $usage .= ($arg['mandatory'] ? '>' : ']'),
    }
    }
    if (!empty($this->commands[$command]['params'])) {
    $usages = array();
    foreach(array_keys($this->commands[$command]['params']) as $setting) {
    $usages[] = $this->_helpSetting($command, $setting);
    }
    $usage .= ' ['.implode(' | ', $usages).']';
    }
    return $usage;
    }
    protected function _helpCommand($command) {
    if (
    empty($this->commands[$command]['args']) &&
    empty($this->commands[$command]['params'])
    ) {
    return array();
    }
    $lines = array('Options for '.$command.':'),
    foreach($this->commands[$command]['args'] as $argName => $arg) {
    $lines[] = "	".$argName . (!empty($arg['help']) ? "		".$arg['help'] : ''),
    }
    foreach(array_keys($this->commands[$command]['params']) as $setting) {
    $lines[] = "	".$this->_helpSetting($command, $setting, true);
    }
    return $lines;
    }
    protected function _helpSetting($command, $settingName, $useHelp = false) {
    $types = array('int' => 'N', 'string' => 'S', 'bool' => null);
    $setting = $this->commands[$command]['params'][$settingName];
    $type = array_key_exists($setting['type'], $types) ? $types[$setting['type']] : null;
    $help = '-'.$settingName . (!empty($type) ? ' '.$type : ''),
    if ($useHelp && !empty($setting['help'])) {
    $help .= "		".$setting['help'];
    if (array_key_exists('value', $setting) && !is_null($setting['value'])) {
    $help .= '. DEFAULTS TO: ';
    if (empty($type)) {
    $help .= $setting['value'] ? 'Enabled' : 'Disabled';
    } else {
    $help .= $setting['value'];
    }
    }
    }
    return $help;
    }
    

If you now run the shell without any parameters, with a command such as the following:

$ cake/console/cake user

we would get the thorough help message shown in the following screenshot:

How to do it...

We can also obtain detailed help for a specific command. Running the shell with a command such as the following:

$ cake/console/cake user help import

would show us the help message for the import command, as shown in the following screenshot:

How to do it...

Running the shell with the same parameters as the ones used in the recipe Parsing command line parameters to import CSV files should work as expected.

How it works...

When a shell includes the property tasks in its declaration, it is said to use the specified tasks. Tasks are stored in the app/vendors/shells/tasks folder, and are accessible in the shell as instances. In our case, we add a task named Help, which should be implemented in a class named HelpTask and placed in a file named help.php in the tasks folder, and we refer to it as $this->Help from within the shell.

Before proceeding, a point has to be made regarding the naming of this particular task. As we want our task to automatically generate help messages for our shell, we somehow need to catch the call to the help() command. This is only achievable if we first understand how the shell dispatching process works. Let us assume the following invocation:

$ cake/console/cake user import

The shell dispatcher, implemented in the file cake/console/cake.php, would go through the following steps:

  1. Instantiate the shell class UserShell.
  2. Call its initialize() method.
  3. Load all tasks defined in the tasks property of the shell.
  4. For each of those tasks, call their initialize() method, and load any tasks that they themselves may be using.

If the given command (import in this case) is the name of one of the included tasks, call the task's startup() method, and then its execute() method.

If the given command is not a task name, then call the shell's startup() method, and execute the command's method, if it exists, or the entry method main() if the command is not implemented.

This means that if we have a task named Help included in our shell, and the user launches the shell with the following command:

$ cake/console/cake user help

Then the shell dispatcher would call the execute() method of the HelpTask class, because the command, help, is actually the name of one of the shell's tasks. Knowing this, we can remove the help() implementation of our User shell, and have the Help task handle the display of help messages.

Furthermore, our Help task needs to be generic enough to not be tied to a specific shell. Therefore, we need a way to tell it about our available commands, expected arguments, and optional parameters. This is what the commands property is there for: an array of commands, where the key is the command name and the value any of the following settings:

Setting

Purpose

help

The help message describing the purpose of the command. Defaults to no message.

args

The list of mandatory and optional arguments the command takes. Defaults to no arguments.

params

The list of optional parameters the command accepts. Defaults to no parameters.

Notice, however, that the add command is defined in a different fashion: instead of being defined in the key, it is simply the name of the command added to the commands array. This means that the command has no help message, no arguments, and no parameters.

The args command setting is an array of arguments, indexed by argument name. Each argument may define any of the following settings:

Setting

Purpose

help

The help message that describes the argument. Defaults to no message.

mandatory

If true, this argument must be present. If false, the argument may be omitted. Defaults to false.

Similarly, the params command setting is also an array, indexed by parameter name, where each parameter may define any of the following settings:

Setting

Purpose

help

The help message that describes the parameter. Defaults to no message.

type

The type of data this parameter holds. May be int, bool, or string. Any other type is interpreted as string. Defaults to string.

value

A default value to use if the parameter is not specified. Defaults to no default value.

Using the commands property in the UserShell class, we define the set of available arguments and parameters for our import command, and we then modify the import() method so that the options are obtained from the parameters property of the Help task. We also use the extract() PHP function to convert any arguments that are defined in the arguments property of the Help task to local variables. This way, the path argument will be available to the method as the variable $path.

These were all the modifications required in the UserShell class. Notice how we not only removed the help() method implementation, but also the processing of parameters, and the check for the right number of arguments from the import() method. This is all done automatically by the Help task now, based on what we define in our commands property.

This means that our Help task is indeed the Swiss Army knife of our shells, with most of its work being done in its initialize() method. This method starts by utilizing the PHP method, get_class_vars(), to obtain the commands property defined in the shell, because our task has no way of getting a hold of the instance of the UserShell class. It then proceeds to go through the list of commands, and normalizes all arguments and parameters thereby defined, assigning the resulting array to the commands property of the HelpTask class.

Once we have all our commands ready to be checked, we establish if the user has indeed selected a command to be executed through the command property, available to all classes that extend from Shell, and set to the current command. If the user has not, and if there is no main() method implemented in the shell, we use the _help() method to display the help.

If users have indeed specified a command that is within the list of available commands, we make sure that the specified arguments match the minimum number of mandatory arguments, if any, aborting the execution with a proper error message if the check fails. If the number of arguments is correct, we store the value of each given argument in the arguments property of the task.

Once the arguments are processed, we proceed to deal with the parameters. Going through the specified parameters, we check their provided value against the data type, if any, aborting the shell with a proper error message if the value given is of an incorrect type. If no value is given, the default value is used, if any. The resulting array of parameters and values is stored in the parameters property of the task.

The execute() method is the one called whenever the Help task is invoked, which is whenever the help command is used when calling the shell. Therefore, this method will simply display the help message by calling the _help() method, optionally passing to it the first argument, which would provide the user with the help message for the given command.

The _help() method builds the help message, for the entire shell or a specific command. It uses the command information stored in the commands property, and calls the _usageCommand() helper method to get the usage message for a given command, and the _helpCommand() method to get the help message for all available parameters and arguments in the command.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.138.172.130