Command Objects

  • class Cake\Console\Command

CakePHP comes with a number of built-in commands for speeding up yourdevelopment, and automating routine tasks. You can use these same libraries tocreate commands for your application and plugins.

Creating a Command

Let’s create our first Command. For this example, we’ll create asimple Hello world command. In your application’s src/Command directory createHelloCommand.php. Put the following code inside it:

  1. <?php
  2. namespace App\Command;
  3.  
  4. use Cake\Console\Arguments;
  5. use Cake\Console\Command;
  6. use Cake\Console\ConsoleIo;
  7.  
  8. class HelloCommand extends Command
  9. {
  10. public function execute(Arguments $args, ConsoleIo $io)
  11. {
  12. $io->out('Hello world.');
  13. }
  14. }

Command classes must implement an execute() method that does the bulk oftheir work. This method is called when a command is invoked. Lets call our firstcommand application directory, run:

  1. bin/cake hello

You should see the following output:

  1. Hello world.

Our execute() method isn’t very interesting let’s read some input from thecommand line:

  1. <?php
  2. namespace App\Command;
  3.  
  4. use Cake\Console\Arguments;
  5. use Cake\Console\Command;
  6. use Cake\Console\ConsoleIo;
  7. use Cake\Console\ConsoleOptionParser;
  8.  
  9. class HelloCommand extends Command
  10. {
  11. protected function buildOptionParser(ConsoleOptionParser $parser)
  12. {
  13. $parser->addArgument('name', [
  14. 'help' => 'What is your name'
  15. ]);
  16. return $parser;
  17. }
  18.  
  19. public function execute(Arguments $args, ConsoleIo $io)
  20. {
  21. $name = $args->getArgument('name');
  22. $io->out("Hello {$name}.");
  23. }
  24. }

After saving this file, you should be able to run the following command:

  1. bin/cake hello jillian
  2.  
  3. # Outputs
  4. Hello jillian

Changing the Default Command Name

CakePHP will use conventions to generate the name your commands use on thecommand line. If you want to overwrite the generated name implement thedefaultName() method in your command:

  1. public static function defaultName(): string
  2. {
  3. return 'oh_hi';
  4. }

The above would make our HelloCommand accessible by cake oh_hi insteadof cake hello.

Defining Arguments and Options

As we saw in the last example, we can use the buildOptionParser() hookmethod to define arguments. We can also define options. For example, we couldadd a yell option to our HelloCommand:

  1. // ...
  2. protected function buildOptionParser(ConsoleOptionParser $parser)
  3. {
  4. $parser
  5. ->addArgument('name', [
  6. 'help' => 'What is your name'
  7. ])
  8. ->addOption('yell', [
  9. 'help' => 'Shout the name',
  10. 'boolean' => true
  11. ]);
  12.  
  13. return $parser;
  14. }
  15.  
  16. public function execute(Arguments $args, ConsoleIo $io)
  17. {
  18. $name = $args->getArgument('name');
  19. if ($args->getOption('yell')) {
  20. $name = mb_strtoupper($name);
  21. }
  22. $io->out("Hello {$name}.");
  23. }

See the Option Parsers section for more information.

Creating Output

Commands are provided a ConsoleIo instance when executed. This object allowsyou to interact with stdout, stderr and create files. See theCommand Input/Output section for more information.

Using Models in Commands

You’ll often need access to your application’s business logic in consolecommands. You can load models in commands, just as you would in a controllerusing loadModel(). The loaded models are set as properties attached to yourcommands:

  1. <?php
  2. namespace App\Command;
  3.  
  4. use Cake\Console\Arguments;
  5. use Cake\Console\Command;
  6. use Cake\Console\ConsoleIo;
  7. use Cake\Console\ConsoleOptionParser;
  8.  
  9. class UserCommand extends Command
  10. {
  11. // Base Command will load the Users model with this property defined.
  12. public $modelClass = 'Users';
  13.  
  14. protected function buildOptionParser(ConsoleOptionParser $parser)
  15. {
  16. $parser
  17. ->addArgument('name', [
  18. 'help' => 'What is your name'
  19. ]);
  20.  
  21. return $parser;
  22. }
  23.  
  24. public function execute(Arguments $args, ConsoleIo $io)
  25. {
  26. $name = $args->getArgument('name');
  27. $user = $this->Users->findByUsername($name)->first();
  28.  
  29. $io->out(print_r($user, true));
  30. }
  31. }

The above command, will fetch a user by username and display the informationstored in the database.

Exit Codes and Stopping Execution

When your commands hit an unrecoverable error you can use the abort() methodto terminate execution:

  1. // ...
  2. public function execute(Arguments $args, ConsoleIo $io)
  3. {
  4. $name = $args->getArgument('name');
  5. if (strlen($name) < 5) {
  6. // Halt execution, output to stderr, and set exit code to 1
  7. $io->error('Name must be at least 4 characters long.');
  8. $this->abort();
  9. }
  10. }

You can also use abort() on the $io object to emit a message and code:

  1. public function execute(Arguments $args, ConsoleIo $io)
  2. {
  3. $name = $args->getArgument('name');
  4. if (strlen($name) < 5) {
  5. // Halt execution, output to stderr, and set exit code to 99
  6. $io->abort('Name must be at least 4 characters long.', 99);
  7. }
  8. }

You can pass any desired exit code into abort().

Tip

Avoid exit codes 64 - 78, as they have specific meanings described bysysexits.h. Avoid exit codes above 127, as these are used to indicateprocess exit by signal, such as SIGKILL or SIGSEGV.

You can read more about conventional exit codes in the sysexit manual pageon most Unix systems (man sysexits), or the System Error Codes helppage in Windows.

Calling other Commands

You may need to call other commands from your command. You can useexecuteCommand to do that:

  1. // You can pass an array of CLI options and arguments.
  2. $this->executeCommand(OtherCommand::class, ['--verbose', 'deploy']);
  3.  
  4. // Can pass an instance of the command if it has constructor args
  5. $command = new OtherCommand($otherArgs);
  6. $this->executeCommand($command, ['--verbose', 'deploy']);

Testing Commands

To make testing console applications easier, CakePHP comes with aConsoleIntegrationTestTrait trait that can be used to test console applicationsand assert against their results.

To get started testing your console application, create a test case that uses theCake\TestSuite\ConsoleIntegrationTestTrait trait. This trait contains a methodexec() that is used to execute your command. You can pass the same stringyou would use in the CLI to this method.

Let’s start with a very simple command, located insrc/Command/UpdateTableCommand.php:

  1. namespace App\Command;
  2.  
  3. use Cake\Console\Arguments;
  4. use Cake\Console\Command;
  5. use Cake\Console\ConsoleIo;
  6. use Cake\Console\ConsoleOptionParser;
  7.  
  8. class UpdateTableCommand extends Command
  9. {
  10. protected function buildOptionParser(ConsoleOptionParser $parser)
  11. {
  12. $parser->setDescription('My cool console app');
  13.  
  14. return $parser;
  15. }
  16. }

To write an integration test for this shell, we would create a test case intests/TestCase/Command/UpdateTableTest.php that uses theCake\TestSuite\ConsoleIntegrationTestTrait trait. This shell doesn’t do much at themoment, but let’s just test that our shell’s description is displayed in stdout:

  1. namespace App\Test\TestCase\Command;
  2.  
  3. use Cake\TestSuite\ConsoleIntegrationTestTrait;
  4. use Cake\TestSuite\TestCase;
  5.  
  6. class UpdateTableCommandTest extends TestCase
  7. {
  8. use ConsoleIntegrationTestTrait;
  9.  
  10. public function setUp()
  11. {
  12. parent::setUp();
  13. $this->useCommandRunner();
  14. }
  15.  
  16. public function testDescriptionOutput()
  17. {
  18. $this->exec('update_table --help');
  19. $this->assertOutputContains('My cool console app');
  20. }
  21. }

Our test passes! While this is very trivial example, it shows that creating anintegration test case for console applications is quite easy. Let’s continue byadding more logic to our command:

  1. namespace App\Command;
  2.  
  3. use Cake\Console\Arguments;
  4. use Cake\Console\Command;
  5. use Cake\Console\ConsoleIo;
  6. use Cake\Console\ConsoleOptionParser;
  7. use Cake\I18n\FrozenTime;
  8.  
  9. class UpdateTableCommand extends Command
  10. {
  11. protected function buildOptionParser(ConsoleOptionParser $parser)
  12. {
  13. $parser
  14. ->setDescription('My cool console app')
  15. ->addArgument('table', [
  16. 'help' => 'Table to update',
  17. 'required' => true
  18. ]);
  19.  
  20. return $parser;
  21. }
  22.  
  23. public function execute(Arguments $args, ConsoleIo $io)
  24. {
  25. $table = $args->getArgument('table');
  26. $this->loadModel($table);
  27. $this->{$table}->query()
  28. ->update()
  29. ->set([
  30. 'modified' => new FrozenTime()
  31. ])
  32. ->execute();
  33. }
  34. }

This is a more complete shell that has required options and relevant logic.Modify your test case to the following snippet of code:

  1. namespace Cake\Test\TestCase\Command;
  2.  
  3. use Cake\Console\Command;
  4. use Cake\I18n\FrozenTime;
  5. use Cake\ORM\TableRegistry;
  6. use Cake\TestSuite\ConsoleIntegrationTestTrait;
  7. use Cake\TestSuite\TestCase;
  8.  
  9. class UpdateTableCommandTest extends TestCase
  10. {
  11. use ConsoleIntegrationTestTrait;
  12.  
  13. public $fixtures = [
  14. // assumes you have a UsersFixture
  15. 'app.Users'
  16. ];
  17.  
  18. public function testDescriptionOutput()
  19. {
  20. $this->exec('update_table --help');
  21. $this->assertOutputContains('My cool console app');
  22. }
  23.  
  24. public function testUpdateModified()
  25. {
  26. $now = new FrozenTime('2017-01-01 00:00:00');
  27. FrozenTime::setTestNow($now);
  28.  
  29. $this->loadFixtures('Users');
  30.  
  31. $this->exec('update_table Users');
  32. $this->assertExitCode(Command::CODE_SUCCESS);
  33.  
  34. $user = TableRegistry::getTableLocator()->get('Users')->get(1);
  35. $this->assertSame($user->modified->timestamp, $now->timestamp);
  36.  
  37. FrozenTime::setTestNow(null);
  38. }
  39. }

As you can see from the testUpdateModified method, we are testing that ourcommand updates the table that we are passing as the first argument. First, weassert that the command exited with the proper status code, 0. Then we checkthat our command did its work, that is, updated the table we provided and setthe modified column to the current time.

Remember, exec() will take the same string you type into your CLI, so youcan include options and arguments in your command string.

Testing Interactive Shells

Consoles are often interactive. Testing interactive shells with theCake\TestSuite\ConsoleIntegrationTestTrait trait only requires passing theinputs you expect as the second parameter of exec(). They should beincluded as an array in the order that you expect them.

Continuing with our example command, let’s add an interactive confirmation.Update the command class to the following:

  1. namespace App\Command;
  2.  
  3. use Cake\Console\Arguments;
  4. use Cake\Console\Command;
  5. use Cake\Console\ConsoleIo;
  6. use Cake\Console\ConsoleOptionParser;
  7. use Cake\I18n\FrozenTime;
  8.  
  9. class UpdateTableCommand extends Command
  10. {
  11. protected function buildOptionParser(ConsoleOptionParser $parser)
  12. {
  13. $parser
  14. ->setDescription('My cool console app')
  15. ->addArgument('table', [
  16. 'help' => 'Table to update',
  17. 'required' => true
  18. ]);
  19.  
  20. return $parser;
  21. }
  22.  
  23. public function execute(Arguments $args, ConsoleIo $io)
  24. {
  25. $table = $args->getArgument('table');
  26. $this->loadModel($table);
  27. if ($io->ask('Are you sure?', 'n', ['y', 'n']) === 'n') {
  28. $io->error('You need to be sure.');
  29. $this->abort();
  30. }
  31. $this->{$table}->query()
  32. ->update()
  33. ->set([
  34. 'modified' => new FrozenTime()
  35. ])
  36. ->execute();
  37. }
  38. }

Now that we have an interactive subcommand, we can add a test case that teststhat we receive the proper response, and one that tests that we receive anincorrect response. Remove the testUpdateModified method and, add the following methods totests/TestCase/Command/UpdateTableCommandTest.php:

  1. public function testUpdateModifiedSure()
  2. {
  3. $now = new FrozenTime('2017-01-01 00:00:00');
  4. FrozenTime::setTestNow($now);
  5.  
  6. $this->loadFixtures('Users');
  7.  
  8. $this->exec('update_table Users', ['y']);
  9. $this->assertExitCode(Command::CODE_SUCCESS);
  10.  
  11. $user = TableRegistry::getTableLocator()->get('Users')->get(1);
  12. $this->assertSame($user->modified->timestamp, $now->timestamp);
  13.  
  14. FrozenTime::setTestNow(null);
  15. }
  16.  
  17. public function testUpdateModifiedUnsure()
  18. {
  19. $user = TableRegistry::getTableLocator()->get('Users')->get(1);
  20. $original = $user->modified->timestamp;
  21.  
  22. $this->exec('my_console best_framework', ['n']);
  23. $this->assertExitCode(Command::CODE_ERROR);
  24. $this->assertErrorContains('You need to be sure.');
  25.  
  26. $user = TableRegistry::getTableLocator()->get('Users')->get(1);
  27. $this->assertSame($original, $user->timestamp);
  28. }

In the first test case, we confirm the question, and records are updated. In thesecond test we don’t confirm and records are not updated, and we can check thatour error message was written to stderr.

Testing the CommandRunner

To test shells that are dispatched using the CommandRunner class, enable itin your test case with the following method:

  1. $this->useCommandRunner();

Assertion methods

The Cake\TestSuite\ConsoleIntegrationTestTrait trait provides a number ofassertion methods that make it easy to assert against console output:

  1. // assert that the shell exited as success
  2. $this->assertExitSuccess();
  3.  
  4. // assert that the shell exited as an error
  5. $this->assertExitError();
  6.  
  7. // assert that the shell exited with the expected code
  8. $this->assertExitCode($expected);
  9.  
  10. // assert that stdout contains a string
  11. $this->assertOutputContains($expected);
  12.  
  13. // assert that stderr contains a string
  14. $this->assertErrorContains($expected);
  15.  
  16. // assert that stdout matches a regular expression
  17. $this->assertOutputRegExp($expected);
  18.  
  19. // assert that stderr matches a regular expression
  20. $this->assertErrorRegExp($expected);