探究 WordPress `WP_CLI` 类中的 `run_command()` 方法源码:如何在代码中调用其他 WP-CLI 命令。

各位好,欢迎来到今天的“WP-CLI 命令套娃大法”讲座!今天我们来扒一扒 WP_CLI 类中的 run_command() 方法,看看它是如何让一个 WP-CLI 命令“呼唤”另一个命令的,就像俄罗斯套娃一样,一层又一层。准备好了吗?我们开始!

一、打个招呼,先来点背景知识

在开始之前,我们先简单了解一下 WP_CLI 类和 run_command() 方法。WP_CLI 类是 WP-CLI 的核心类,负责处理命令的注册、解析和执行。而 run_command() 方法则是执行具体命令的关键入口。简单来说,当我们敲下 wp plugin install akismet 命令时,最终就是通过 run_command() 方法来完成 Akismet 插件的安装。

二、run_command() 方法的“庐山真面目”

为了更深入地了解,我们先来看看 run_command() 方法的源码(简化版,去掉了部分错误处理和钩子):

<?php

class WP_CLI {

    public static function run_command( $name, $args = array(), $assoc_args = array() ) {
        // 1. 查找命令对应的 callable
        $callable = self::find_command_callable( $name );

        if ( ! $callable ) {
            WP_CLI::error( sprintf( "'%s' is not a registered wp command.", $name ) );
            return;
        }

        // 2. 解析参数
        list( $r_args, $r_assoc_args, $r_callable ) = self::parse_args( $callable, $args, $assoc_args );

        // 3. 调用命令
        $return = call_user_func_array( $r_callable, array_merge( $r_args, array( $r_assoc_args ) ) );

        return $return;
    }

    private static function find_command_callable( $name ) {
        global $wp_cli_commands;

        if ( isset( $wp_cli_commands[ $name ] ) ) {
            return $wp_cli_commands[ $name ];
        }

        return null;
    }

    private static function parse_args( $callable, $args, $assoc_args ) {
        // 这里省略参数解析的逻辑,比较复杂,但不是我们今天关注的重点
        // 简单来说,就是把用户输入的参数和命令定义中的参数进行匹配和验证
        return array($args, $assoc_args, $callable);
    }
}

从上面的代码可以看出,run_command() 主要做了三件事:

  1. 找到命令对应的“执行者” (callable): 通过 find_command_callable() 方法,根据命令名称查找对应的 PHP 函数或方法。
  2. 解析参数: 使用 parse_args() 方法,将用户输入的参数(包括位置参数和关联参数)与命令定义中声明的参数进行匹配和验证。
  3. 执行命令: 使用 call_user_func_array() 函数,调用找到的 PHP 函数或方法,并将解析后的参数传递给它。

三、命令套娃:关键在于 WP_CLI::run_command()

现在,让我们回到主题:如何在代码中调用其他 WP-CLI 命令?答案很简单:再次调用 WP_CLI::run_command() 方法!

想象一下,你正在编写一个自定义 WP-CLI 命令,它的功能是:

  1. 备份数据库。
  2. 备份主题目录。
  3. 将备份文件打包成一个压缩包。

你可以将这三个步骤分别定义为三个独立的 WP-CLI 命令,然后在你的自定义命令中依次调用它们。

下面是一个示例代码:

<?php

class My_Backup_Command {

    /**
     * 备份网站数据库和主题。
     *
     * ## EXAMPLES
     *
     *     wp my-backup backup-all
     *
     * @when before_wp_load
     */
    public function backup_all( $args, $assoc_args ) {
        WP_CLI::line( '开始备份...' );

        // 1. 备份数据库
        WP_CLI::line( '备份数据库...' );
        $db_backup_result = WP_CLI::run_command( 'db export', array( 'my-backup.sql' ) );
        if ( $db_backup_result !== 0 ) {
            WP_CLI::error( '数据库备份失败!' );
            return;
        }

        // 2. 备份主题目录
        WP_CLI::line( '备份主题目录...' );
        $theme_backup_result = WP_CLI::run_command( 'theme list', array(), array( 'format' => 'json' ) );
        if ( $theme_backup_result === null ) {
            WP_CLI::error( '主题列表获取失败!' );
            return;
        }

        $themes = json_decode( $theme_backup_result, true );
        foreach ( $themes as $theme ) {
            WP_CLI::line( '备份主题:' . $theme['name'] );
            $theme_dir = WP_CONTENT_DIR . '/themes/' . $theme['name'];
            $zip_file = 'theme-' . $theme['name'] . '.zip';
            $zip_command = sprintf( 'zip -r %s %s', $zip_file, $theme_dir );
            exec( $zip_command, $output, $return_var );  // 这里为了简化,直接用 exec,实际应用中应该使用更安全的方案
            if ($return_var !== 0) {
                WP_CLI::error( '主题备份失败:' . $theme['name'] );
                return;
            }
        }

        // 3. 将备份文件打包成一个压缩包 (这里只是示例,实际应该使用更健壮的方法)
        WP_CLI::line( '打包备份文件...' );
        $archive_name = 'backup-' . date( 'YmdHis' ) . '.zip';
        $zip_command = sprintf( 'zip -r %s my-backup.sql theme-*.zip', $archive_name );
        exec( $zip_command, $output, $return_var );
        if ($return_var !== 0) {
            WP_CLI::error( '打包备份文件失败!' );
            return;
        }

        WP_CLI::success( '备份完成!备份文件:' . $archive_name );
    }
}

WP_CLI::add_command( 'my-backup', 'My_Backup_Command' );

代码解释:

  • My_Backup_Command 类定义了一个名为 backup_all 的方法,它就是我们的自定义 WP-CLI 命令。
  • WP_CLI::add_command( 'my-backup', 'My_Backup_Command' ) 将这个类注册为一个 WP-CLI 命令,命令名为 my-backup
  • backup_all 方法中,我们使用 WP_CLI::run_command() 方法分别调用了 db exporttheme list 命令。
  • WP_CLI::line()WP_CLI::error()WP_CLI::success() 是 WP-CLI 提供的用于输出信息的辅助函数。
  • 注意,这里备份主题使用了 exec 函数,这只是为了演示方便,在实际生产环境中,应该使用更安全可靠的方法,例如 WP_Filesystem API。

使用方法:

  1. 将上面的代码保存为一个 PHP 文件,例如 my-backup-command.php
  2. 将该文件放到 WordPress 插件目录下,例如 wp-content/plugins/my-backup-plugin/my-backup-command.php
  3. 激活该插件。
  4. 在命令行中运行 wp my-backup backup-all 命令。

四、参数传递的技巧

在调用 WP_CLI::run_command() 方法时,我们需要传递三个参数:

  1. $name:要执行的命令名称,例如 'db export'
  2. $args:位置参数,是一个数组,例如 array( 'my-backup.sql' )
  3. $assoc_args:关联参数,是一个关联数组,例如 array( 'format' => 'json' )

参数传递的一些小技巧:

  • 位置参数按顺序传递: 位置参数的顺序必须与被调用命令的定义一致。
  • 关联参数使用键值对: 关联参数使用键值对的形式传递,键对应于被调用命令定义的参数名称。
  • 混合使用: 可以同时传递位置参数和关联参数。
  • 参数覆盖: 如果在调用 run_command() 时传递了与被调用命令默认值相同的参数,那么传递的参数会覆盖默认值。

举个栗子:

假设我们有一个名为 my-command 的命令,它的定义如下:

/**
 * 这是一个示例命令。
 *
 * ## OPTIONS
 *
 * <name>
 * : 你的名字.
 *
 * [--greeting=<greeting>]
 * : 问候语.
 * ---
 * default: Hello
 * ---
 *
 * ## EXAMPLES
 *
 *     wp my-command John --greeting="Good morning"
 *
 * @subcommand hello
 */
public function hello( $args, $assoc_args ) {
    $name = $args[0];
    $greeting = $assoc_args['greeting'];

    WP_CLI::line( $greeting . ', ' . $name . '!' );
}

现在,我们想在另一个命令中调用它:

WP_CLI::run_command( 'my-command hello', array( 'Alice' ), array( 'greeting' => 'Hi' ) );

上面的代码会输出:Hi, Alice!

五、返回值处理

WP_CLI::run_command() 方法会返回被调用命令的返回值。通常情况下,WP-CLI 命令会返回以下值:

  • 0:表示命令执行成功。
  • 非零整数:表示命令执行失败,具体的数值表示不同的错误代码。
  • 其他类型:有些命令可能会返回其他类型的值,例如字符串、数组或对象,具体取决于命令的实现。

在调用 WP_CLI::run_command() 方法后,我们可以根据返回值来判断命令是否执行成功,并进行相应的处理。

六、安全注意事项

在使用 WP_CLI::run_command() 方法调用其他命令时,需要特别注意安全性问题。

  • 参数注入: 要确保传递给 run_command() 方法的参数是经过安全处理的,以防止参数注入攻击。特别是当参数来自用户输入时,更要格外小心。可以使用 esc_sql()sanitize_text_field() 等函数对参数进行过滤和转义。
  • 避免执行未授权的命令: 要确保只能调用经过授权的命令,防止恶意用户利用你的自定义命令来执行未授权的操作。
  • 文件系统操作: 如果要执行涉及文件系统操作的命令,例如 theme installplugin activate 等,需要确保当前用户具有足够的文件系统权限。可以使用 WP_Filesystem API 来安全地执行文件系统操作。

七、WP-CLI::launch()WP_CLI::exec() 的区别

除了 WP_CLI::run_command() 之外,WP-CLI 还提供了 WP_CLI::launch()WP_CLI::exec() 两个方法来执行命令。它们与 run_command() 有什么区别呢?

方法 说明 适用场景
WP_CLI::run_command() 执行一个已注册的 WP-CLI 命令。 在你的自定义 WP-CLI 命令中,需要调用其他已注册的 WP-CLI 命令时。
WP_CLI::launch() 执行一个外部命令,例如 lsgrep 等。 需要执行系统命令时,例如创建目录、复制文件等。
WP_CLI::exec() 执行一个外部命令,并返回命令的输出结果。 实际上是 WP_CLI::launch() 加上捕获输出。 需要执行系统命令,并且需要获取命令的输出结果时。

简单总结:

  • run_command() 用于调用 WP-CLI 内部的命令。
  • launch()exec() 用于调用系统命令。

八、总结与展望

今天我们深入探讨了 WP_CLI 类中的 run_command() 方法,了解了它是如何让一个 WP-CLI 命令“呼唤”另一个命令的。 掌握了这种“命令套娃”大法,你就可以编写更加强大和灵活的自定义 WP-CLI 命令,提高你的 WordPress 开发效率。

当然,WP-CLI 的世界远不止于此,还有很多高级特性和技巧等待我们去探索。希望今天的讲座能够帮助你打开 WP-CLI 的新世界大门! 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注