命令对象

class Cake\Console\Command

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 实例。此对象允许您与 stdoutstderr 交互并创建文件。有关更多信息,请参阅 命令输入/输出 部分。

在命令中使用模型

您通常需要在控制台命令中访问应用程序的业务逻辑。您可以像在控制器中使用 $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。此事件不能被停止或替换其结果。