剖析 WordPress `WP_CLI` 类的源码:它如何处理命令行参数和子命令。

各位观众老爷,各位技术宅,晚上好!(或者早上好,取决于你什么时候看)。我是你们的老朋友,今天咱们来聊聊WordPress的命令行神器:WP_CLI

别害怕,命令行听起来好像很Geek,其实用起来爽歪歪。特别是对于WordPress这种内容管理系统来说,WP_CLI能让你摆脱鼠标,用键盘征服世界!(或者至少征服你的WordPress站点)。

今天,咱们不光要用它,还要扒光它的衣服,看看它的源码,特别是它怎么处理命令行参数和子命令的。准备好了吗?发车啦!

一、WP_CLI: WordPress 的命令行瑞士军刀

首先,简单介绍一下WP_CLI。它是一个用PHP编写的WordPress命令行接口。你可以用它来更新插件,发布文章,管理用户,甚至做数据库迁移等等。总之,你能用WordPress后台做的事情,大部分都能用WP_CLI更快更方便地完成。

举个例子,假设你想更新所有插件,只需要一行命令:

wp plugin update --all

是不是比在后台一个个点更新按钮快多了?

二、WP_CLI 的核心:WP_CLI

WP_CLI的所有魔法都藏在 WP_CLI 类里(有点废话,但还是要强调一下)。这个类负责解析命令行参数,加载命令,并执行相应的操作。咱们先来看看它的基本结构。

打开 wp-cli/wp-cli.php (或者在你安装WP_CLI的地方找到这个文件),你会看到类似这样的代码:

<?php

class WP_CLI {

    /**
     * Holds registered commands.
     *
     * @var array
     */
    private static $commands = array();

    /**
     * Holds command aliases.
     *
     * @var array
     */
    private static $aliases = array();

    /**
     * Holds command doc aliases.
     *
     * @var array
     */
    private static $command_doc_aliases = array();

    /**
     * @var WP_CLIDispatcherCommandNamespace[]
     */
    private static $namespaces = array();

    /**
     * @var array
     */
    private static $hooks = array();

    // ... 其他属性和方法 ...

    /**
     * Registers a command.
     *
     * @param string $name       Command name.
     * @param mixed  $callable   Command callable.
     * @param array  $args       Command arguments.
     * @return void
     */
    public static function add_command( $name, $callable, $args = array() ) {
        // ... 注册命令的逻辑 ...
    }

    /**
     * Runs the command.
     *
     * @param array $args Command line arguments.
     */
    public static function run( $args ) {
        // ... 执行命令的逻辑 ...
    }

    // ... 其他方法 ...
}

这里我们重点关注 add_command()run() 这两个方法。

  • add_command():这个方法用于注册命令,告诉 WP_CLI 有哪些命令可用。
  • run():这个方法是 WP_CLI 的大脑,它接收命令行参数,找到对应的命令,然后执行它。

三、命令行参数的处理:WP_CLI::run() 方法

WP_CLI::run() 方法是整个流程的起点。它接收一个 $args 数组,这个数组包含了从命令行传递过来的所有参数。

public static function run( $args ) {
    // 1. 初始化
    self::init();

    // 2. 获取要执行的命令
    $command = array_shift( $args );

    // 3. 查找命令
    $r = self::find_command_to_run( $command, $args );

    if ( WP_CLIUtilsget_flag_value( $r, 'before_invoke' ) ) {
        list( $callable, $args, $assoc_args ) = self::invoke_pre_command( $r['callable'], $args, $r['assoc_args'] );
    } else {
        $callable = $r['callable'];
        $assoc_args = $r['assoc_args'];
    }

    if ( is_array( $callable ) && is_string( $callable[0] ) ) {
        // ... 处理静态方法调用 ...
    } else {
        // ... 处理普通函数调用 ...
    }

    // 4. 执行命令
    $return = call_user_func_array( $callable, $args );

    // ... 处理命令执行后的逻辑 ...
}

让我们分解一下:

  1. 初始化 (self::init()): 做一些准备工作,比如加载配置文件,设置错误处理等等。

  2. 获取要执行的命令 (array_shift( $args )): array_shift() 函数从 $args 数组中移除第一个元素,并返回它的值。这个值就是用户在命令行中输入的第一个参数,也就是要执行的命令。例如,如果用户输入 wp plugin update --all,那么 $command 的值就是 plugin

  3. 查找命令 (self::find_command_to_run()): 这个方法负责根据 $command 找到对应的 PHP 函数或类方法。它会遍历 WP_CLI::$commands 数组,查找匹配的命令。如果找到,就返回一个包含命令信息(包括 callable 和关联数组参数)的数组。

  4. 执行命令 (call_user_func_array()): 这是最关键的一步。call_user_func_array() 函数允许你动态地调用一个 PHP 函数或类方法,并将一个数组作为参数传递给它。 $callable 就是要调用的函数或类方法,$args 是传递给它的参数数组。

四、find_command_to_run() 方法: 寻找你的真爱 (命令)

find_command_to_run() 方法的职责是根据用户输入的命令名称,在已注册的命令中找到对应的 callable。它考虑了命令的命名空间和别名,使得命令的调用更加灵活。

private static function find_command_to_run( $command, $args ) {
    $assoc_args = array();

    // 1. 处理命令的命名空间
    $command_parts = explode( ' ', $command );
    $namespace = array_shift( $command_parts );
    $subcommand = implode( ' ', $command_parts );

    // 2. 查找命名空间
    if ( isset( self::$namespaces[ $namespace ] ) ) {
        $namespace_obj = self::$namespaces[ $namespace ];

        // 查找子命令
        $r = self::find_command_to_run( $subcommand, $args );
        if ( $r ) {
            return $r;
        }
    }

    // 3. 查找命令
    if ( isset( self::$commands[ $command ] ) ) {
        $callable = self::$commands[ $command ];

        // 解析参数
        list( $args, $assoc_args, $r ) = self::parse_args( $args );
        return array(
            'callable'   => $callable,
            'args'       => $args,
            'assoc_args' => $assoc_args,
        );
    }

    // 4. 处理别名
    if ( isset( self::$aliases[ $command ] ) ) {
        $alias = self::$aliases[ $command ];
        array_unshift( $args, $alias );
        return self::find_command_to_run( implode( ' ', $args ), array() );
    }

    // 5. 命令未找到
    WP_CLI::error( sprintf( "'%s' is not a registered wp-cli command. See 'wp help'.", $command ) );
}

代码逻辑如下:

  1. 处理命名空间: WP_CLI 支持命令的命名空间,例如 plugin install 中的 plugin 就是一个命名空间。代码首先尝试将命令分割成命名空间和子命令。

  2. 查找命名空间: 如果找到了命名空间,就递归地调用 find_command_to_run() 方法,查找子命令。

  3. 查找命令: 如果在 WP_CLI::$commands 数组中找到了与命令名称匹配的 callable,就调用 parse_args() 方法解析参数,并返回一个包含 callable 和参数的数组。

  4. 处理别名: WP_CLI 还支持命令别名,例如你可以将 plugin update --all 命令定义为 update-all-plugins。如果找到了命令别名,就将别名替换成原始命令,并递归地调用 find_command_to_run() 方法。

  5. 命令未找到: 如果以上步骤都没有找到匹配的命令,就输出一个错误信息。

五、parse_args() 方法: 解析命令行参数

parse_args() 方法是负责解析命令行参数的关键。它将命令行参数分解成位置参数 (positional arguments) 和关联数组参数 (associative arguments)。

private static function parse_args( $args ) {
    $positional = array();
    $assoc      = array();
    $r          = array();

    foreach ( $args as $arg ) {
        if ( preg_match( '/^--([w-]+)$/', $arg, $matches ) ) {
            $assoc[ $matches[1] ] = true;
        } elseif ( preg_match( '/^--([w-]+)=(.*)$/', $arg, $matches ) ) {
            $assoc[ $matches[1] ] = $matches[2];
        } else {
            $positional[] = $arg;
        }
    }

    return array( $positional, $assoc, $r );
}

代码逻辑如下:

  1. 遍历参数: 遍历 $args 数组中的每个参数。

  2. 识别关联数组参数: 如果参数以 -- 开头,则认为是关联数组参数。例如,--all--path=/path/to/wordpress

  3. 识别位置参数: 如果参数不是关联数组参数,则认为是位置参数。例如,pluginupdate

  4. 返回结果: 返回一个包含位置参数数组、关联数组参数数组和一个空数组的数组。

六、注册命令:add_command() 方法

add_command() 方法用于注册新的命令。它将命令名称和对应的 PHP 函数或类方法添加到 WP_CLI::$commands 数组中。

public static function add_command( $name, $callable, $args = array() ) {
    if ( isset( self::$commands[ $name ] ) ) {
        WP_CLI::error( sprintf( "Command '%s' is already defined.", $name ) );
    }

    if ( ! is_callable( $callable ) ) {
        WP_CLI::error( sprintf( "Invalid callable for command '%s'.", $name ) );
    }

    self::$commands[ $name ] = $callable;

    if ( isset( $args['before_invoke'] ) ) {
        self::$command_doc_aliases[ $name ] = $args['before_invoke'];
    }
}

代码逻辑很简单:

  1. 检查命令是否已存在: 如果命令已经存在,则输出一个错误信息。

  2. 检查 callable 是否有效: 如果 callable 不是一个有效的 PHP 函数或类方法,则输出一个错误信息。

  3. 注册命令: 将命令名称和 callable 添加到 WP_CLI::$commands 数组中。

七、一个完整的例子:wp plugin update --all

让我们用一个完整的例子来总结一下 WP_CLI 是如何处理命令行参数和子命令的。

假设用户在命令行中输入 wp plugin update --all

  1. WP_CLI::run() 方法被调用,并将 array('plugin', 'update', '--all') 作为参数传递给它。

  2. array_shift() 函数从 $args 数组中移除第一个元素 plugin,并将其赋值给 $command 变量。

  3. WP_CLI::find_command_to_run() 方法被调用,并将 $command 的值 plugin$args 的值 array('update', '--all') 作为参数传递给它。

  4. WP_CLI::find_command_to_run() 方法首先检查 plugin 是否是一个命名空间。假设 plugin 是一个命名空间,那么它会递归地调用 WP_CLI::find_command_to_run() 方法,并将 $subcommand 的值 update$args 的值 array('--all') 作为参数传递给它。

  5. WP_CLI::find_command_to_run() 方法在 WP_CLI::$commands 数组中查找名为 plugin update 的命令。假设找到了这个命令,并且对应的 callable 是 Plugin_Command::update() 方法。

  6. WP_CLI::parse_args() 方法被调用,并将 $args 的值 array('--all') 作为参数传递给它。

  7. WP_CLI::parse_args() 方法将 --all 解析为一个关联数组参数,并将 $assoc['all'] 的值设置为 true

  8. WP_CLI::find_command_to_run() 方法返回一个包含 Plugin_Command::update() 方法、位置参数数组 (空数组) 和关联数组参数数组 (array('all' => true)) 的数组。

  9. WP_CLI::run() 方法调用 call_user_func_array(array('Plugin_Command', 'update'), array(array('all' => true))) 来执行 Plugin_Command::update() 方法,并将关联数组参数作为参数传递给它。

  10. Plugin_Command::update() 方法接收到关联数组参数,并根据 --all 参数的值更新所有插件。

八、总结

WP_CLI 的核心逻辑就是:

  1. 接收命令行参数。
  2. 根据命令名称找到对应的 callable。
  3. 解析命令行参数,将它们分解成位置参数和关联数组参数。
  4. 调用 callable,并将参数传递给它。

希望这次的源码剖析能帮助你更好地理解 WP_CLI 的工作原理。 掌握了这些知识,你不仅可以使用 WP_CLI 更加得心应手,还可以自己扩展 WP_CLI,添加自定义命令,让你的WordPress开发更加高效。

补充:常见命令参数类型及WP_CLI处理方式

参数类型 示例 WP_CLI 处理方式
位置参数 wp post create "Hello World" parse_args() 方法会将 "Hello World" 识别为位置参数,存储在 $positional 数组中。在执行命令时,这些参数会按照顺序传递给 callable。
关联数组参数 (flag) wp plugin update --all parse_args() 方法会将 --all 识别为关联数组参数,存储在 $assoc 数组中,值为 true。 在执行命令时,callable 可以通过检查 $assoc['all'] 的值来判断是否需要更新所有插件。
关联数组参数 (value) wp option update siteurl example.com parse_args() 方法会将 siteurlexample.com 识别为位置参数。 update 命令的具体实现会进一步解析这些位置参数,判断 siteurl 是要更新的选项名称,example.com 是新的选项值。
关联数组参数 (key-value) wp config set WP_DEBUG true parse_args() 方法无法直接处理。config set 命令的具体实现会自行解析这些参数,通常会将 WP_DEBUG 识别为 key, true 识别为 value。 这种类型的参数处理通常在命令的具体实现中完成,而不是在 WP_CLI::parse_args() 中。
混合参数 wp post list --category=news --posts_per_page=5 parse_args() 会将 --category=news--posts_per_page=5 识别为关联数组参数,存储在 $assoc 数组中。 list 命令的具体实现会利用这些参数来过滤和分页文章列表。

九、 挑战

  1. 阅读 WP_CLI 的源码,找到 WP_CLI::add_alias() 方法,并理解它是如何实现命令别名的。
  2. 编写一个自定义的 WP_CLI 命令,例如创建一个名为 wp hello 的命令,当用户执行这个命令时,输出 "Hello, World!"。
  3. 尝试使用 WP_CLI 创建一个插件,这个插件包含一个自定义的 WP_CLI 命令。

好啦,今天的分享就到这里。希望大家都能成为 WP_CLI 的高手,用命令行玩转 WordPress! 如果有任何问题,欢迎在评论区留言。 我们下次再见!

发表回复

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