CakePHP 提供了一些内置命令,用于加速您的开发并自动执行例行任务。您可以使用这些相同的库为您的应用程序和插件创建命令。
让我们创建第一个命令。在本例中,我们将创建一个简单的 Hello world 命令。在应用程序的 src/Command 目录中创建 HelloCommand.php。将以下代码放入其中
<?php
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
class HelloCommand extends Command
{
public function execute(Arguments $args, ConsoleIo $io): int
{
$io->out('Hello world.');
return static::CODE_SUCCESS;
}
}
命令类必须实现一个 execute()
方法,该方法完成其大部分工作。当调用命令时,会调用此方法。让我们调用第一个命令应用程序目录,运行
bin/cake hello
您应该看到以下输出
Hello world.
我们的 execute()
方法并不十分有趣,让我们从命令行读取一些输入
<?php
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
class HelloCommand extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->addArgument('name', [
'help' => 'What is your name',
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io): int
{
$name = $args->getArgument('name');
$io->out("Hello {$name}.");
return static::CODE_SUCCESS;
}
}
保存此文件后,您应该能够运行以下命令
bin/cake hello jillian
# Outputs
Hello jillian
CakePHP 将使用约定生成命令在命令行中使用的名称。如果您想覆盖生成的名称,请在命令中实现 defaultName()
方法
public static function defaultName(): string
{
return 'oh_hi';
}
以上操作将使我们的 HelloCommand
可以通过 cake oh_hi
访问,而不是 cake hello
。
正如我们在上一个示例中看到的,我们可以使用 buildOptionParser()
钩子方法来定义参数。我们还可以定义选项。例如,我们可以向我们的 HelloCommand
添加一个 yell
选项
// ...
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser
->addArgument('name', [
'help' => 'What is your name',
])
->addOption('yell', [
'help' => 'Shout the name',
'boolean' => true,
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io): int
{
$name = $args->getArgument('name');
if ($args->getOption('yell')) {
$name = mb_strtoupper($name);
}
$io->out("Hello {$name}.");
return static::CODE_SUCCESS;
}
有关更多信息,请参阅 选项解析器 部分。
命令在执行时提供了一个 ConsoleIo
实例。此对象允许您与 stdout
、stderr
交互并创建文件。有关更多信息,请参阅 命令输入/输出 部分。
您通常需要在控制台命令中访问应用程序的业务逻辑。您可以像在控制器中使用 $this->fetchTable()
一样加载命令中的模型,因为命令使用 LocatorAwareTrait
<?php
declare(strict_types=1);
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
class UserCommand extends Command
{
// Define the default table. This allows you to use `fetchTable()` without any argument.
protected $defaultTable = 'Users';
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser
->addArgument('name', [
'help' => 'What is your name'
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io): int
{
$name = $args->getArgument('name');
$user = $this->fetchTable()->findByUsername($name)->first();
$io->out(print_r($user, true));
return static::CODE_SUCCESS;
}
}
上面的命令将按用户名获取用户并显示存储在数据库中的信息。
当您的命令遇到不可恢复的错误时,您可以使用 abort()
方法终止执行
// ...
public function execute(Arguments $args, ConsoleIo $io): int
{
$name = $args->getArgument('name');
if (strlen($name) < 5) {
// Halt execution, output to stderr, and set exit code to 1
$io->error('Name must be at least 4 characters long.');
$this->abort();
}
return static::CODE_SUCCESS;
}
您也可以在 $io
对象上使用 abort()
来发出消息和代码
public function execute(Arguments $args, ConsoleIo $io): int
{
$name = $args->getArgument('name');
if (strlen($name) < 5) {
// Halt execution, output to stderr, and set exit code to 99
$io->abort('Name must be at least 4 characters long.', 99);
}
return static::CODE_SUCCESS;
}
您可以将任何所需的退出代码传递到 abort()
中。
提示
避免使用 64 - 78 的退出代码,因为它们具有由 sysexits.h
描述的特定含义。避免使用 127 以上的退出代码,因为这些代码用于指示进程通过信号退出,例如 SIGKILL 或 SIGSEGV。
您可以在大多数 Unix 系统 (man sysexits
) 上的 sysexit 手册页或 Windows 中的 System Error Codes
帮助页中阅读有关常规退出代码的更多信息。
您可能需要从命令中调用其他命令。您可以使用 executeCommand
来做到这一点
// You can pass an array of CLI options and arguments.
$this->executeCommand(OtherCommand::class, ['--verbose', 'deploy']);
// Can pass an instance of the command if it has constructor args
$command = new OtherCommand($otherArgs);
$this->executeCommand($command, ['--verbose', 'deploy']);
注意
在循环中调用 executeCommand()
时,建议将父命令的 ConsoleIo
实例作为可选的第 3 个参数传递,以避免在某些环境中可能发生的潜在“打开文件”限制。
您可能希望通过以下方式设置命令描述
class UserCommand extends Command
{
public static function getDescription(): string
{
return 'My custom description';
}
}
这将在 Cake CLI 中显示您的描述
bin/cake
App:
- user
└─── My custom description
以及在命令的帮助部分中
cake user --help
My custom description
Usage:
cake user [-h] [-q] [-v]
为了使测试控制台应用程序更容易,CakePHP 附带了 ConsoleIntegrationTestTrait
特性,可用于测试控制台应用程序并断言其结果。
要开始测试您的控制台应用程序,请创建一个使用 Cake\TestSuite\ConsoleIntegrationTestTrait
特性的测试用例。此特性包含一个 exec()
方法,用于执行您的命令。您可以将与在 CLI 中使用的相同的字符串传递给此方法。
注意
对于 CakePHP 4.4 及更高版本,应使用 Cake\Console\TestSuite\ConsoleIntegrationTestTrait
命名空间。
让我们从一个非常简单的命令开始,该命令位于 src/Command/UpdateTableCommand.php 中
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
class UpdateTableCommand extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser->setDescription('My cool console app');
return $parser;
}
}
要为该命令编写集成测试,我们将创建一个位于 tests/TestCase/Command/UpdateTableTest.php 中的测试用例,该测试用例使用 Cake\TestSuite\ConsoleIntegrationTestTrait
特性。该命令目前没有做太多事情,但让我们测试一下命令描述是否在 stdout
中显示
namespace App\Test\TestCase\Command;
use Cake\TestSuite\ConsoleIntegrationTestTrait;
use Cake\TestSuite\TestCase;
class UpdateTableCommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;
public function testDescriptionOutput()
{
$this->exec('update_table --help');
$this->assertOutputContains('My cool console app');
}
}
我们的测试通过了!虽然这是一个非常简单的例子,但它表明为控制台应用程序创建集成测试用例可以遵循命令行约定。让我们继续通过向命令添加更多逻辑
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\I18n\DateTime;
class UpdateTableCommand extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser
->setDescription('My cool console app')
->addArgument('table', [
'help' => 'Table to update',
'required' => true
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io): int
{
$table = $args->getArgument('table');
$this->fetchTable($table)->updateQuery()
->set([
'modified' => new DateTime()
])
->execute();
return static::CODE_SUCCESS;
}
}
这是一个更完整的命令,它具有必需的选项和相关的逻辑。将您的测试用例修改为以下代码片段
namespace Cake\Test\TestCase\Command;
use Cake\Command\Command;
use Cake\I18n\DateTime;
use Cake\TestSuite\ConsoleIntegrationTestTrait;
use Cake\TestSuite\TestCase;
class UpdateTableCommandTest extends TestCase
{
use ConsoleIntegrationTestTrait;
protected $fixtures = [
// assumes you have a UsersFixture
'app.Users',
];
public function testDescriptionOutput()
{
$this->exec('update_table --help');
$this->assertOutputContains('My cool console app');
}
public function testUpdateModified()
{
$now = new DateTime('2017-01-01 00:00:00');
DateTime::setTestNow($now);
$this->loadFixtures('Users');
$this->exec('update_table Users');
$this->assertExitCode(Command::CODE_SUCCESS);
$user = $this->getTableLocator()->get('Users')->get(1);
$this->assertSame($user->modified->timestamp, $now->timestamp);
DateTime::setTestNow(null);
}
}
如您从 testUpdateModified
方法中看到的那样,我们正在测试命令是否更新了我们作为第一个参数传递的表。首先,我们断言命令以适当的退出状态代码 0
退出。然后我们检查命令是否完成了它的工作,即更新了我们提供的表并将 modified
列设置为当前时间。
请记住,exec()
将接受您在 CLI 中键入的相同字符串,因此您可以在命令字符串中包含选项和参数。
控制台通常是交互式的。使用 Cake\TestSuite\ConsoleIntegrationTestTrait
特性测试交互式命令只需要将您期望的输入作为 exec()
的第二个参数传递。它们应按您期望的顺序包含在一个数组中。
继续使用我们示例命令,让我们添加一个交互式确认。将命令类更新为以下内容
namespace App\Command;
use Cake\Command\Command;
use Cake\Console\Arguments;
use Cake\Console\ConsoleIo;
use Cake\Console\ConsoleOptionParser;
use Cake\I18n\DateTime;
class UpdateTableCommand extends Command
{
protected function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
{
$parser
->setDescription('My cool console app')
->addArgument('table', [
'help' => 'Table to update',
'required' => true
]);
return $parser;
}
public function execute(Arguments $args, ConsoleIo $io): int
{
$table = $args->getArgument('table');
if ($io->ask('Are you sure?', 'n', ['y', 'n']) !== 'y') {
$io->error('You need to be sure.');
$this->abort();
}
$this->fetchTable($table)->updateQuery()
->set([
'modified' => new DateTime()
])
->execute();
return static::CODE_SUCCESS;
}
}
现在我们有了交互式命令,我们可以添加一个测试用例来测试我们是否收到了正确的响应,以及一个测试用例来测试我们是否收到了错误的响应。删除 testUpdateModified
方法,并将以下方法添加到 tests/TestCase/Command/UpdateTableCommandTest.php 中。
public function testUpdateModifiedSure()
{
$now = new DateTime('2017-01-01 00:00:00');
DateTime::setTestNow($now);
$this->loadFixtures('Users');
$this->exec('update_table Users', ['y']);
$this->assertExitCode(Command::CODE_SUCCESS);
$user = $this->getTableLocator()->get('Users')->get(1);
$this->assertSame($user->modified->timestamp, $now->timestamp);
DateTime::setTestNow(null);
}
public function testUpdateModifiedUnsure()
{
$user = $this->getTableLocator()->get('Users')->get(1);
$original = $user->modified->timestamp;
$this->exec('my_console best_framework', ['n']);
$this->assertExitCode(Command::CODE_ERROR);
$this->assertErrorContains('You need to be sure.');
$user = $this->getTableLocator()->get('Users')->get(1);
$this->assertSame($original, $user->timestamp);
}
在第一个测试用例中,我们确认了问题,并且记录被更新。在第二个测试用例中,我们没有确认,并且记录没有被更新,我们可以检查我们的错误消息是否被写入到 stderr
。
Cake\TestSuite\ConsoleIntegrationTestTrait
特性提供了一些断言方法,可以帮助我们断言控制台输出。
// assert that the command exited as success
$this->assertExitSuccess();
// assert that the command exited as an error
$this->assertExitError();
// assert that the command exited with the expected code
$this->assertExitCode($expected);
// assert that stdout contains a string
$this->assertOutputContains($expected);
// assert that stderr contains a string
$this->assertErrorContains($expected);
// assert that stdout matches a regular expression
$this->assertOutputRegExp($expected);
// assert that stderr matches a regular expression
$this->assertErrorRegExp($expected);
与控制器类似,命令也提供了生命周期事件,允许您观察框架调用您的应用程序代码。命令有
Command.beforeExecute
在命令的 execute()
方法执行之前被调用。该事件会将 ConsoleArguments
参数作为 args
传递。此事件不能被停止或替换其结果。
Command.afterExecute
在命令的 execute()
方法执行完成后被调用。该事件包含 ConsoleArguments
作为 args
,以及命令结果作为 result
。此事件不能被停止或替换其结果。