Reputation: 5395
I'm building an application in CakePHP 3.8 which uses Console Commands to execute several processes.
These processes are quite resource intensive so I've written them with Commands because they would easily time-out if executed in a browser.
There are 5 different scripts that do different tasks: src/Command/Stage1Command.php
,
... src/Command/Stage5Command.php
.
The scripts are being executed in order (Stage 1 ... Stage 5) manually, i.e. src/Command/Stage1Command.php
is executed with:
$ php bin/cake.php stage1
All 5 commands accept one parameter - an ID - and then perform some work. This has been set up as follows (the code in buildOptionsParser()
exists in each command):
class Stage1Command extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser)
{
$parser->addArgument('filter_id', [
'help' => 'Filter ID must be passed as an argument',
'required' => true
]);
return $parser;
}
}
So I can execute "Stage 1" as follows, assuming 428
is the ID I want to pass.
$ php bin/cake.php stage1 428
Instead of executing these manually, I want to achieve the following:
Create a new Command which loops through a set of Filter ID's and then calls each of the 5 commands, passing the ID.
Update a table to show the outcome (success, error) of each command.
For (1) I have created src/Command/RunAllCommand.php
and then used a loop on my table of Filters to generate the IDs, and then execute the 5 commands, passing the ID. The script looks like this:
namespace App\Command;
use Cake\ORM\TableRegistry;
// ...
class RunAllCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io)
{
$FiltersTable = TableRegistry::getTableLocator()->get('Filters');
$all_filters = $FiltersTable->find()->toArray();
foreach ($all_filters as $k => $filter) {
$io->out($filter['id']);
// execute Stage1Command.php
$command = new Stage1Command(['filter_id' => $filter['id']]);
$this->executeCommand($command);
// ...
// execute Stage5Command.php
$command5 = new Stage5Command(['filter_id' => $filter['id']]);
$this->executeCommand($command5);
}
}
}
This doesn't work. It gives an error:
Filter ID must be passed as an argument
I can tell that the commands are being called because these are my own error messages from buildOptionsParser()
.
This makes no sense because the line $io->out($filter['id'])
in RunAllCommand.php
is showing that the filter IDs are being read from my database. How do you pass an argument in this way? I'm following the docs on Calling Other Commands (https://book.cakephp.org/3/en/console-and-shells/commands.html#calling-other-commands).
I don't understand how to achieve (2). In each of the Commands I've added code such as this when an error occurs which stops execution of the rest of that Command. For example if this gets executed in Stage1Command
it should abort and move to Stage2Command
:
// e.g. this code can be anywhere in execute() in any of the 5 commands where an error occurs.
$io->error('error message');
$this->abort();
If $this->abort()
gets called anywhere I need to log this into another table in my database. Do I need to add code before $this->abort()
to write this to a database, or is there some other way, e.g. try...catch
in RunAllCommand
?
Background information: The idea with this is that RunAllCommand.php
would be executed via Cron. This means that the processes carried out by each Stage would occur at regular intervals without requiring manual execution of any of the scripts - or passing IDs manually as command parameters.
Upvotes: 0
Views: 591
Reputation: 60463
The arguments sent to the "main" command are not automatically being passed to the "sub" commands that you're invoking with executeCommand()
, the reason for that being that they might very well be incompatible, the "main" command has no way of knowing which arguments should or shouldn't be passed. The last thing you want is a sub command do something that you haven't asked it to do just because of an argument that the main command makes use of.
So you need to pass the arguments that you want your sub commands to receive manually, that would be the second argument of \Cake\Console\BaseCommand::executeCommand()
, not the command constructor, it doesn't take any arguments at all (unless you've overwritten the base constructor).
$this->executeCommand($stage1, [$filter['id']]);
Note that the arguments array is not associative, the values are passed as single value entries, just like PHP would receive them in the $argv
variable, ie:
['positional argument value', '--named', 'named option value']
With regards to errors, executeCommand()
returns the exit code of the command. Calling $this->abort()
in your sub command will trigger an exception, which is being catched in executeCommand()
and has its code returned just like the normal exit code from your sub command's execute()
method.
So if you just need to log a failure, then you could simply evaluate the return code, like:
$result = $this->executeCommand($stage1, [$filter['id']]);
// assuming your sub commands do always return a code, and do not
// rely on `null` (ie no return value) being treated as success too
if ($result !== static::CODE_SUCCESS) {
$this->log('Stage 1 failed');
}
If you need additional information to be logged, then you could of course log inside of your sub commands where that information is available, or maybe store error info in the command and expose a method to read that info, or throw an exception with error details that your main command could catch and evaluate. However, throwing an exception would not be overly nice when running the commands standalone, so you'll have to figure what the best option is in your case.
Upvotes: 2