snicco/better-wp-cli

为已经出色的WP-CLI添加缺失的部分

v2.0.0-beta.9 2024-09-07 14:27 UTC

README

codecov Psalm Type-Coverage Psalm level PhpMetrics - Static Analysis PHP-Versions

BetterWPCLI 是一个小型、无依赖的 PHP 库,帮助您构建企业级WordPress命令行应用程序。

BetterWPCLI 不会替代或接管 wp 运行器的任何功能。

相反,它位于 WP-CLI 和您的自定义命令之间。

目录

  1. 动机
  2. 安装
  3. 使用
    1. 命令
      1. 概要
      2. 默认标志
    2. 注册命令
    3. 控制台输入
    4. 控制台输出
      1. 详细程度
    5. 样式化命令输出
      1. 标题
      2. 部分
      3. 信息
      4. 注意
      5. 成功
      6. 警告
      7. 错误
    6. 交互式输入
      1. 请求确认
      2. 请求信息
      3. 隐藏输入
      4. 验证答案
    7. 异常处理
      1. 未捕获的异常
      2. 日志记录
      3. 将错误转换为异常
    8. 测试
  4. 贡献
  5. 问题和PR
  6. 安全
  7. 致谢

动机

我们开发这个库是为了以下原因,用于 WordPress 相关的 Snicco 项目的组件

  • WP-CLI 没有原生的依赖注入支持,也不支持命令的懒加载。

  • WP-CLI 鼓励使用元语言或通过硬编码的字符串名称进行命令配置。

  • WP-CLI 对于写入 STDOUTSTDERR 的处理不一致且不可配置。

    • WP_CLI::log()WP_CLI::success()WP_CLI::line() 写入到 STDOUT
    • WP_CLI::warning()WP_CLI::error() 写入到 STDERR
    • 进度条写入到 STDOUT,使得命令管道变得不可能。
    • 输入提示写入到 STDOUT,使得命令管道变得不可能。
    • 未捕获的PHP通知(或其他错误)写入到 STDOUT,使得命令管道变得不可能。
  • WP-CLI 没有错误处理。抛出的异常直接进入全局关闭处理器(wp_die)并在终端上显示可怕的 "这个网站发生了一个关键错误。了解更多关于WordPress故障排除的信息。"。因此,它们也进入 STDOUT 而不是 STDER

  • WP-CLI 只能检测到 STDOUT 的 ANSI 支持,而不能单独为 STDOUTSTDERR 检测。

    • 如果您正在重定向 STDOUT,可能不希望 STDERR 失去所有颜色。
  • WP-CLI 命令难以测试,因为其鼓励在命令中直接使用静态的 WP_CLI 类,而不是使用一些 Input/Output 抽象。

  • WP-CLI 与静态分析器如 psalmphpstan 不兼容。

    • 您在命令类中收到两个完全未类型化的数组。
    • 您没有简单的方法来区分位置参数和重复的位置参数。

BetterWPCLI 旨在解决所有这些问题,同时为您提供许多附加功能。

BetterWPCLI 特别 设计用于在分布式代码中可用,例如公共插件。

安装

BetterWPCLI 通过 composer 进行分发。

composer require snicco/better-wp-cli

使用

命令

所有命令都扩展了 Command 类。

一个 Command 类负责处理确切的 一个 命令并定义其自己的概要。

该命令类重现了在 WP-CLI 命令菜谱 中描述的示例命令。

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Synopsis\Synopsis;
use Snicco\Component\BetterWPCLI\Synopsis\InputFlag;
use Snicco\Component\BetterWPCLI\Synopsis\InputOption;
use Snicco\Component\BetterWPCLI\Synopsis\InputArgument;

class ExampleCommand extends Command {
    
    // You can set an explicit command name. 
    // If a command name is not set explicitly, it's determined from the class name.
    protected static string $name = 'example';
    
    
    
    // The short description of the command that will be shown
    // when running "wp help example"
    protected static string $short_description = 'Prints a greeting'
    
    
    
    // If a long description is not set explicitly it will default to 
    // the short_description property.
    protected static string $long_description = '## EXAMPLES' . "\n\n" . 'wp example hello Newman'
    
    
    
    public function execute(Input $input, Output $output) : int {
        
        $name = $input->getArgument('name'); // (string) Always a string
        $type = $input->getOption('flag'); // (string) Always a string
        $honk = $input->getFlag('honk'); // (bool) Always a boolean
        
        // outputs a message followed by a "\n"
        $output->writeln("$type: Hello $name!");
        
        // Writes directly to the output stream without newlines
        //$output->write('You are about');
        //$output->write(' to honk');
        
        // Writes to "\n" chars
        //$output->newLine(2);
        
        if($honk) {
            $output->writeln("Honk");
        }
        
        // (This is equivalent to returning int(0))
        return Command::SUCCESS;

        // (This is equivalent to returning int(1))
        // return Command::FAILURE;

        // (This is equivalent to returning int(2))
        // return Command::INVALID
    }
    
    
    public static function synopsis() : Synopsis{
      
      return new Synopsis(
            new InputArgument(
                'name', 
                'The name of the person to great', 
                InputArgument::REQUIRED
            ),
            
            // You can combine options by using bit flags.
            
            // new InputArgument(
            //    'some-other-arg', 
            //    'This is another arg', 
            //    InputArgument::REQUIRED | InputArgument::REPEATING
            //),
            
            new InputOption(
                'type', 
                'Whether or not to greet the person with success or error.', 
                InputOption::OPTIONAL, 
                'success',
                ['success', 'error']
            ),
            new InputFlag('honk')
      );
      
    }
    
}

概要

Synopsis 值对象可以帮助您使用清晰的 PHP API 创建命令概要。

Synopsis 具有一套丰富的验证规则,这些规则在 WP-CLI 中是隐含的。这可以帮助您立即避免某些意外,例如

  • 参数/选项/标志具有重复的名称。
  • 在重复参数之后注册位置参数。
  • 设置不在允许值列表中的默认值。
  • ...

Synopsis 由零个或多个位置参数、选项或标志组成。

这些由相应的类表示

默认标志

Command 类具有几个内置标志,您可以在您的命令中使用它们。

您可以通过将其添加到父概要中自动将其添加到所有命令中。

这完全是可选的。

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Synopsis\Synopsis;
use Snicco\Component\BetterWPCLI\Synopsis\InputArgument;

class MyCommand extends Command {

    public static function synopsis() : Synopsis{
        return parent::synopsis()->with([
            new InputArgument(
                'name', 
                'The name of the person to great', 
                InputArgument::REQUIRED
            ),
        ]);
    }
}

这将添加以下命令概要

Synopsis

注册命令

通过使用 WPCLIApplication 类来注册命令。

if(!defined('WP_CLI')) {
    return;
}

use Snicco\Component\BetterWPCLI\WPCLIApplication;
use Snicco\Component\BetterWPCLI\CommandLoader\ArrayCommandLoader;

// The namespace will be prepended to all your commands automatically.
$command_namespace = 'snicco';

// The command loader is responsible for lazily loading your commands.
// The second argument is a callable that should return an instance of
// a command by its name. This should typically be a call to your dependency injection container.

// This array can come from a configuration file.
$command_classes = [
    ExampleCommand::class,
    FooCommand::class,
    BarCommand::class,
];

$container = /* Your dependency injection container or another factory class */
$factory = function (string $command_class) use ($container) {
    return $container->get($command_class);
}

$command_loader = new ArrayCommandLoader($command_classes, $factory);

$application = new WPCLIApplication($command_namespace);

$application->registerCommands();

控制台输入

控制台输入通过一个 Input 接口进行抽象。

所有命令都将接收一个 Input 实例,该实例包含所有传递的参数。

use Snicco\Component\BetterWPCLI\Input\Input
use Snicco\Component\BetterWPCLI\Output\Output
use Snicco\Component\BetterWPCLI\Synopsis\Synopsis;
use Snicco\Component\BetterWPCLI\Synopsis\InputFlag;
use Snicco\Component\BetterWPCLI\Synopsis\InputOption;
use Snicco\Component\BetterWPCLI\Synopsis\InputArgument;

// ...
public static function synopsis(): Synopsis
{
    return new Synopsis(
        new InputArgument(
            'role',
            'The role that should be assigned to the users',
        ),
        new InputArgument(
            'ids',
            'A list of user ids that should be assigned to passed role',
            InputArgument::REQUIRED | InputArgument::REPEATING
        ),
        new InputFlag(
            'notify',
            'Send the user an email about his new role'
        ),
        new InputOption(
            'some-option',
        ),
    );
}

// ...
public function execute(Input $input, Output $output): int
{
    $output->writeln([
        'Changing user roles',
        '===================',
    ]);
    
    // Arguments are retrieved by their name.
    $role = $input->getArgument('role');  // (string)
    
    // The second argument is returned if the option/argument was not passed. 
    $option = $input->getOption('some-option', 'some-default-value'); // (string)
    
    $users = $input->getRepeatingArgument('ids'); // (string[]) and array of ids.
    
    $notify = $input->getFlag('notify', false);
    
    foreach($users as $id) {
        
        // assign role here
        if($notify) {
            // send email here
        }
    }            
    
        
    return Command::SUCCESS;
}

控制台输出

控制台输出通过一个 Output 接口进行抽象。

所有命令都将接收一个 Output 实例。

建议您在命令中使用此类写入输出流。

这样,您的命令将保持可测试性,因为您可以将此 Output 接口替换为测试替身。

但是,没有任何阻止您在命令中使用 WP_CLI 类的限制。

use Snicco\Component\BetterWPCLI\Input\Input
use Snicco\Component\BetterWPCLI\Output\Output

// ...
protected function execute(Input $input, Output $output): int
{
    // outputs multiple lines to the console (adding "\n" at the end of each line)
    $output->writeln([
        'Starting the command',
        '============',
        '',
    ]);

    // outputs a message followed by a "\n"
    $output->writeln('Doing something!');

    // outputs a message without adding a "\n" at the end of the line
    $output->write('You are about to ');
    $output->write('do something here');
    
    // Outputs 3 "\n" chars.
    $output->newLine(3);    
    
    // You can also use the WP_CLI class. 
    // WP_CLI::debug('doing something');
        
    return Command::SUCCESS;
}

详细程度

BetterWPCLI 有一个概念是详细程度级别,允许用户选择命令输出的详细程度。

请参阅:在 默认标志 中添加标志到您的命令的说明。

WP-CLI 有一个类似的概念,但只允许您在 quiet(无输出)和 debug(非常详尽的输出,包括 wp-cli 内部信息)之间进行选择。

BetterWPCLI 有以下五个详细级别,可以是每个命令单独设置或通过使用 SHELL_VERBOSITY 环境值。

(命令行参数的优先级高于 SHELL_VERBOSITY--debug 以及 --quiet,它们将覆盖 BetterWPCLI 的所有唯一值。)

可以只为特定的详细级别打印特定信息。

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Verbosity;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;

class AssignUserRoles extends Command {
    
    public function execute(Input $input,Output $output) : int{
        
        $output->writeln('Always printed', Verbosity::QUIET);
        
        $output->writeln('only printed for verbosity normal and above', Verbosity::NORMAL);
        
        $output->writeln('only printed for verbosity verbose and above', Verbosity::VERBOSE);
        
        $output->writeln('only printed for verbosity very-verbose and above', Verbosity::VERY_VERBOSE);
        
        $output->writeln('only printed for verbosity debug', Verbosity::DEBUG);
        
        return Command::SUCCESS;
    }
    
    // .. synopsis defined here.
    
}

样式化命令输出

BetterWPCLI 为您提供了一个实用工具类 SniccoStyle,您可以在您的命令中实例化。

该类包含许多创建丰富控制台输出的辅助方法。该样式基于 symfony/console 包的样式。

颜色支持会根据操作系统自动检测,是否通过管道传递命令以及提供的标志(如:--no-color--no-ansi)。参见:默认标志

除非您进行了配置,否则该类将写入 STDERR

您应该使用 Output 实例将 重要 信息写入 STDOUT

重要信息是理论上可以传递到其他命令的信息。

如果您的命令没有输出此类信息,只需返回 Command::SUCCESS 并不输出任何内容。《沉默是金》。

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;

   // ... 
   protected function execute(Input $input, Output $output): int
    {
        $io = new SniccoStyle($input, $output);
        // ...
        
        // Not so important information
        //$io->title('Command title);
        
        $output->writeln('Some important command output that should be piped.'); 
        
        return Command::SUCCESS;
    }

标题

应该在命令开始时使用一次 title() 方法。

$io->title('This is the command title');

Title

部分

可以使用 section() 方法来分隔命令的多个连贯部分。

$io->section('This is a new section');

Section

信息

可以使用 info() 方法表示一个部分的完成。

$io->info('This is an info');

Info

注意

可以使用 note() 方法来额外强调消息。请勿过度使用。

$io->note('This is a note');

Note

文本

可以使用 text() 方法输出未经颜色化的普通文本。

// Passing an array is optional.
$io->text(['This is a text', 'This is another text']);

Text

成功

应该在命令结束时使用一次 success() 方法。

// Passing an array is optional.
$io->success(['This command', 'was successful']);

Success

警告

应该在命令结束时使用一次 warning() 方法。

// Passing an array is optional.
$io->warning(['This command', 'displays a warning']);

Warning

错误

如果命令失败,应该在命令结束时使用一次 error() 方法。

// Passing an array is optional.
$io->error(['This command', 'did not work']);

Error

交互式输入

SniccoStyle 类提供了一些方法来从用户那里获取更多信息。

如果命令带有 --no-interaction 标志运行,将自动使用默认答案。参见:默认标志

所有交互式问题的输出都将写入 STDERR

请求确认

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;

   // ... 
   protected function execute(Input $input, Output $output): int
    {
        $io = new SniccoStyle($input, $output);
        
        // The second argument is the default value
        if(!$io->confirm('Are you sure that you want to continue', false)) {
        
            $io->warning('Command aborted');
            
            return Command::SUCCESS;
        }
        // Proceed
        
        return Command::SUCCESS;
    }

Confirmation

请求信息

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;

   // ... 
   protected function execute(Input $input, Output $output): int
    {
        $io = new SniccoStyle($input, $output);
        
        $domain = $io->ask('Please tell use your company domain', 'snicco.io');
        
        $output->writeln('Your domain is: '. $domain);
    }

Information

隐藏输入

您也可以提问并隐藏响应。

这将通过更改终端的 stty 模式来完成。

如果 stty 不可用,除非您进行了配置,否则将回退到可见输入。

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Question\Question;

   // ... 
   protected function execute(Input $input, Output $output): int
    {
        $io = new SniccoStyle($input, $output);
        
        // This will fall back to visible input if stty is not available.
        // e.g. on Windows
        $secret = $io->askHidden('What is your secret?')
        
        $question = (new Question('What is your secret'))
                        ->withHiddenInput()
                        ->withFallbackVisibleInput(false);
        
        // This will throw an exception if hidden input can not be ensured.
        $secret = $io->askQuestion($question);
        
        //
    }

验证提供的输入

您可以验证用户提供的答案。如果验证失败,用户将再次收到相同的问题。

您还可以设置最大尝试次数。如果超过最大尝试次数,将抛出 InvalidAnswer 异常。

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Output\Output;
use Snicco\Component\BetterWPCLI\Question\Question;
use Snicco\Component\BetterWPCLI\Exception\InvalidAnswer;

   // ... 
   protected function execute(Input $input, Output $output): int
    {
        $io = new SniccoStyle($input, $output);
        
        $validator = function (string $answer) :void {
            if(strlen($answer) < 5) {
                throw new InvalidAnswer('The name must have at least 6 characters.');
            }
        };
        
        $attempts = 2; 
                
        $question = new Question('Please enter a name', 'default_name', $validator, $attempts);
        
        $answer = $io->askQuestion($question);
    }

Validation

异常处理

BetterWPCLI 提供了非常坚实的异常/错误处理。

然而,这种行为是完全隔离的,并且仅适用于您的命令。核心命令或其他插件的命令不会受到任何影响。

未捕获的异常

如果您的命令抛出一个未捕获的异常,将会发生两件事

  1. 在考虑当前详细程度的情况下,异常会在STDERR中显示。
  2. 异常使用Logger接口进行记录。(这是传递给WPCLIApplication的第三个参数)

这是不同详细程度下异常的显示方式

VERBOSITY::NORMAL:

verbosity normal

VERBOSITY::VERBOSE:

verbosity verbose

VERBOSITY::VERY_VERBOSE及以上

verbosity very verbose

您可以禁用捕获异常,尽管这不被推荐。

use Snicco\Component\BetterWPCLI\WPCLIApplication;;

$command_loader = new ArrayCommandLoader($command_classes, $factory);

$application = new WPCLIApplication($command_namespace);

// This disables exception handling.
//All exceptions are now handled globally by WordPress again.
$application->catchException(false);

$application->registerCommands();

日志记录

默认情况下,使用StdErrLogger来记录异常,使用error_log

此类适用于分布式代码的用途,因为它会将异常记录到WP_DEBUG_LOG中配置的位置。如果您想使用自定义记录器,您必须在创建您的WPCLIApplication时将其作为第三个参数传递。

Logger将为您命令生命周期中的所有未捕获异常以及所有返回非零退出码的命令创建记录。

将错误转换为异常

在正常的WP-CLI命令中,错误(如通知、警告和弃用)根本不会处理。相反,它们会冒泡到全局的PHP错误处理器。

将通知和警告视为异常是一种最佳实践。

BetterWPCLI将您的命令期间的所有错误提升为ErrorException实例。

以下代码

use Snicco\Component\BetterWPCLI\Command;
use Snicco\Component\BetterWPCLI\Style\SniccoStyle;
use Snicco\Component\BetterWPCLI\Input\Input;
use Snicco\Component\BetterWPCLI\Output\Output;

   // ... 
   protected function execute(Input $input, Output $output): int
    {
        $arr = ['foo'];
        
        $foo = $arr[1];
        
        //
        
        return Command::SUCCESS;
    }

将抛出异常并以代码1退出。

notices-to-exceptions

默认情况下,所有错误(包括弃用)都提升为异常。

如果您觉得这太严格了,您可以根据需要自定义行为。

use Snicco\Component\BetterWPCLI\WPCLIApplication;;

$command_loader = new ArrayCommandLoader($command_classes, $factory);

$application = new WPCLIApplication($command_namespace);

// This is the default setting
$application->throwExceptionsAt(E_ALL);

// Throw exceptions for all errors expect deprecations.
$application->throwExceptionsAt(E_ALL - E_DEPRECATED - E_USER_DEPRECATED);

// This disables the behaviour entirely (NOT RECOMMENDED)
$application->throwExceptionsAt(0);

$application->registerCommands();

测试

此软件包附带专门的测试工具,这些工具位于一个单独的软件包snicco/better-wp-cli-testing中。

use Snicco\Component\BetterWPCLI\Testing\CommandTester;

$tester = new CommandTester(new CreateUserCommand());

$tester->run(['calvin', 'calvin@snicco.io'], ['send-email' => true]);

$tester->assertCommandIsSuccessful();

$tester->assertStatusCode(0);

$tester->seeInStdout('User created!');

$tester->dontSeeInStderr('Fail');

贡献

此存储库是Snicco项目开发存储库的只读分割。

这是您可以如何贡献的方式.

报告问题和发送拉取请求

请在此Snicco单一代码库中报告问题。

安全

如果您在BetterWPAPI中发现安全漏洞,请遵循我们的披露程序

致谢

检查symfony console symfony/console的源代码对于开发此库非常有价值。