阐述 WordPress `WP_CLI::add_command()` 函数的源码:如何注册一个自定义的 WP-CLI 命令。

大家好,欢迎来到今天的WP-CLI自定义命令编程讲座。今天咱们就来扒一扒 WP_CLI::add_command() 这个看似简单,实则暗藏玄机的函数,看看它到底是如何让我们的自定义命令在WP-CLI的世界里安家落户的。

开场白:WP-CLI的世界,命令的乐园

WP-CLI,WordPress Command-Line Interface,顾名思义,就是用命令行来管理WordPress。想象一下,不用登录后台,直接敲几行命令就能更新插件、导入数据、甚至清理垃圾文件,是不是很酷?而这一切,都离不开各种各样的命令。

WordPress本身已经内置了很多实用的命令,比如wp plugin installwp user create等等。但是,总有些时候,我们需要一些定制化的功能,这时,就需要我们自己编写自定义命令了。而WP_CLI::add_command() 就是我们开启自定义命令之旅的钥匙。

正题:WP_CLI::add_command() 的源码解剖

WP_CLI::add_command() 的作用,简单来说,就是将一个PHP类或者函数注册为 WP-CLI 的一个命令。 咱们先看看它的基本用法:

WP_CLI::add_command( 'my-command', 'My_Command_Class' ); //类作为命令
WP_CLI::add_command( 'my-command', 'my_command_function' ); //函数作为命令

第一个参数 'my-command' 是命令的名称,第二个参数可以是类的名称或者函数的名称。 当我们在命令行输入 wp my-command 的时候,WP-CLI 就会执行对应的类或者函数。

那么,WP_CLI::add_command() 内部到底做了什么呢? 让我们一起深入源码一探究竟。(以下代码基于 WP-CLI 的源码,为了便于理解,进行了简化和注释)

<?php

class WP_CLI {

    protected static $commands = array();

    /**
     * Registers a command with WP-CLI.
     *
     * @param string $name Name of the command.
     * @param mixed  $callable Either a callable or a class name.
     * @param array  $args Optional. Arguments to pass to the command.
     */
    public static function add_command( $name, $callable, $args = array() ) {
        if ( ! is_string( $name ) || empty( $name ) ) {
            WP_CLI::error( 'Command name must be a non-empty string.' );
        }

        if ( ! is_callable( $callable ) && ! is_string( $callable ) ) {
            WP_CLI::error( 'Command callable must be a callable or a class name.' );
        }

        $name = self::validate_command_name( $name );

        self::$commands[ $name ] = array(
            'callable' => $callable,
            'args'     => $args,
        );
    }

    /**
     * Validates the command name.
     *
     * @param string $name Command name.
     * @return string Validated command name.
     */
    protected static function validate_command_name( $name ) {
        $name = trim( $name );

        if ( strpos( $name, ':' ) !== false ) {
            list( $parent, $child ) = explode( ':', $name, 2 );

            $parent = self::validate_command_name( $parent );
            $child  = self::validate_command_name( $child );

            $name = $parent . ':' . $child;
        }

        return $name;
    }

    /**
     * Executes a command.
     *
     * @param array $args Command arguments.
     * @param array $assoc_args Command associative arguments.
     */
    public static function run_command( $args, $assoc_args = array() ) {
        global $argv;

        // Extract command name from arguments.
        $command_name = array_shift( $args );

        if ( ! isset( self::$commands[ $command_name ] ) ) {
            WP_CLI::error( sprintf( "'%s' is not a registered WP-CLI command.", $command_name ) );
        }

        $command = self::$commands[ $command_name ];
        $callable = $command['callable'];
        $command_args = $command['args'];

        try {
            if ( is_string( $callable ) && class_exists( $callable ) ) {
                // Instantiate the class.
                $instance = new $callable( $args, $assoc_args, $command_args );

                // Check if the command has a __invoke method.
                if ( method_exists( $instance, '__invoke' ) ) {
                    $return = call_user_func( array( $instance, '__invoke' ), $args, $assoc_args );
                } else {
                    WP_CLI::error( 'Command class must have a __invoke() method.' );
                }
            } elseif ( is_callable( $callable ) ) {
                // Call the function.
                $return = call_user_func( $callable, $args, $assoc_args, $command_args );
            } else {
                WP_CLI::error( 'Command callable must be a callable or a class name.' );
            }

            // Handle return value (simplified for demonstration).
            if ( is_string( $return ) ) {
                WP_CLI::line( $return );
            } elseif ( is_array( $return ) ) {
                WP_CLI::print_table( $return ); // Assuming WP_CLI::print_table exists
            }
        } catch ( Exception $e ) {
            WP_CLI::error( $e->getMessage() );
        }
    }

    public static function print_table( $data, $headers = null ) {
        // Simplified table printing logic for demonstration.
        if ( empty( $data ) ) {
            return;
        }

        if ( $headers === null ) {
            $headers = array_keys( reset( $data ) );
        }

        // Print headers
        WP_CLI::line( implode( "t", $headers ) );

        // Print data
        foreach ( $data as $row ) {
            $values = array();
            foreach ( $headers as $header ) {
                $values[] = isset( $row[ $header ] ) ? $row[ $header ] : '';
            }
            WP_CLI::line( implode( "t", $values ) );
        }
    }

    public static function error( $message ) {
        fwrite( STDERR, WP_CLI::colorize( "%RError: {$message}%nn" ) );
        exit(1);
    }

    public static function line( $message = '' ) {
        echo WP_CLI::colorize( $message ) . "n";
    }

    public static function colorize( $string ) {
        // Simplified colorization logic for demonstration.
        $string = str_replace( '%R', "33[31m", $string ); // Red
        $string = str_replace( '%n', "33[0m", $string );  // Reset
        return $string;
    }
}

核心逻辑分析

  1. 注册命令 (add_command)

    • add_command() 首先会检查传入的命令名称 $name 和可调用对象 $callable 是否有效。 $name 必须是非空的字符串, $callable 必须是有效的可调用对象 (函数名或者类名)。
    • 然后,它会调用 validate_command_name() 函数对命令名称进行验证和规范化,主要是处理带冒号 : 的子命令。
    • 最后,它会将命令名称 $name 和对应的可调用对象 $callable 以及其他参数 $args 存储到 $commands 数组中。$commands 是一个静态的数组,用于保存所有注册的命令。

    我们可以用一个表格来总结一下 add_command() 函数的主要工作:

    步骤 描述
    1. 参数验证 检查命令名称和可调用对象是否有效。
    2. 名称规范化 使用 validate_command_name() 处理子命令。
    3. 存储命令 将命令信息存储到 $commands 数组中。
  2. 命令执行 (run_command)

    • run_command() 函数是 WP-CLI 执行命令的核心。它接收命令行参数 $args 和关联数组 $assoc_args
    • 首先,它从 $args 中提取命令名称 $command_name
    • 然后,它会在 $commands 数组中查找对应的命令信息。 如果找不到,会报错。
    • 接下来,它会判断 $callable 是类名还是函数名。
      • 如果是类名,它会实例化该类,并检查该类是否定义了 __invoke() 方法。 如果没有,会报错。 然后,它会调用 __invoke() 方法,并将 $args$assoc_args 传递给它。
      • 如果是函数名,它会直接调用该函数,并将 $args$assoc_args 传递给它。
    • 最后,它会处理命令的返回值,并将其输出到命令行。 (这里为了简化,只处理了字符串和数组两种类型)。

    我们也可以用一个表格来总结一下 run_command() 函数的主要工作:

    步骤 描述
    1. 提取命令名称 从命令行参数中提取命令名称。
    2. 查找命令 $commands 数组中查找对应的命令信息。
    3. 判断可调用对象 判断 $callable 是类名还是函数名。
    4. 执行命令 如果是类,则实例化并调用 __invoke() 方法;如果是函数,则直接调用。
    5. 处理返回值 处理命令的返回值,并将其输出到命令行。
  3. 命令名称验证 (validate_command_name)

    这个函数主要负责处理带有冒号 : 的命令名称,也就是子命令。 它会将父命令和子命令分开,并递归地调用自身进行验证,确保命令名称的格式正确。

    例如,对于命令名称 my-plugin:activate,它会将 my-pluginactivate 分开,然后分别进行验证。

实战演练:创建一个简单的自定义命令

理论说了一大堆,不如来点实际的。 咱们来创建一个简单的自定义命令,来加深对 WP_CLI::add_command() 的理解。

假设我们想要创建一个名为 wp hello 的命令,当执行这个命令时,它会在命令行输出 "Hello, WP-CLI!"。

首先,我们需要创建一个 PHP 文件,比如 hello-command.php,并将其放置在 WordPress 插件目录下,或者主题目录下,或者任何 WP-CLI 可以找到的地方(比如 ~/.wp-cli/commands 目录)。

<?php

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

class Hello_Command {
    /**
     * Says hello to WP-CLI.
     *
     * ## EXAMPLES
     *
     *     wp hello
     *
     * @when before_wp_load
     */
    public function __invoke( $args, $assoc_args ) {
        WP_CLI::line( 'Hello, WP-CLI!' );
    }
}

WP_CLI::add_command( 'hello', 'Hello_Command' );

这段代码做了以下几件事情:

  1. 检查 WP-CLI 是否存在if ( ! class_exists( 'WP_CLI' ) ) { return; } 确保只有在 WP-CLI 环境下才会加载这段代码。
  2. 定义一个类 Hello_Command:这个类就是我们自定义命令的执行主体。
  3. 定义 __invoke() 方法:这是 WP-CLI 执行命令时调用的方法。 所有的逻辑都写在这里。 $args$assoc_args 分别是命令行参数和关联数组参数。
  4. 使用 WP_CLI::line() 输出信息WP_CLI::line() 是 WP-CLI 提供的用于在命令行输出信息的函数。
  5. 使用 WP_CLI::add_command() 注册命令WP_CLI::add_command( 'hello', 'Hello_Command' );Hello_Command 类注册为 hello 命令。

现在,打开你的命令行,进入 WordPress 站点目录,然后输入 wp hello,你应该可以看到 "Hello, WP-CLI!" 输出在命令行上。

进阶:带参数的自定义命令

光说 "Hello, WP-CLI!" 显然不够酷。 咱们来让这个命令更强大一点,让它可以接受一个参数,并向指定的人打招呼。

修改 hello-command.php 文件:

<?php

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

class Hello_Command {
    /**
     * Says hello to someone.
     *
     * ## OPTIONS
     *
     * <name>
     * : The name of the person to greet.
     *
     * ## EXAMPLES
     *
     *     wp hello John
     *     wp hello --name=Jane
     *
     * @when before_wp_load
     */
    public function __invoke( $args, $assoc_args ) {
        if ( ! empty( $args ) ) {
            $name = $args[0];
        } elseif ( isset( $assoc_args['name'] ) ) {
            $name = $assoc_args['name'];
        } else {
            $name = 'WP-CLI';
        }

        WP_CLI::line( "Hello, {$name}!" );
    }
}

WP_CLI::add_command( 'hello', 'Hello_Command' );

这次,我们做了以下修改:

  1. 添加了参数说明## OPTIONS## EXAMPLES 用于生成 WP-CLI 的帮助文档。
  2. 获取参数:我们在 __invoke() 方法中,首先检查 $args 是否为空。 如果不为空,则从 $args[0] 中获取参数。 否则,检查 $assoc_args['name'] 是否存在。 如果存在,则从 $assoc_args['name'] 中获取参数。 如果都没有,则使用默认值 "WP-CLI"。
  3. 输出个性化问候语WP_CLI::line( "Hello, {$name}!" ); 根据获取到的参数,输出个性化的问候语。

现在,你可以尝试以下命令:

  • wp hello John 输出 "Hello, John!"
  • wp hello --name=Jane 输出 "Hello, Jane!"
  • wp hello 输出 "Hello, WP-CLI!"

更进一步:子命令

WP-CLI 还支持子命令,也就是命令后面再跟一个命令。 例如 wp plugin install 中的 install 就是 plugin 命令的子命令。

要创建子命令,只需要在 WP_CLI::add_command() 中使用冒号 : 分隔父命令和子命令即可。

例如,我们可以创建一个名为 wp my-plugin:activate 的命令:

<?php

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

class My_Plugin_Command {
    /**
     * Activates a plugin.
     *
     * ## OPTIONS
     *
     * <plugin>
     * : The plugin to activate.
     *
     * ## EXAMPLES
     *
     *     wp my-plugin activate my-plugin/my-plugin.php
     *
     * @when before_wp_load
     */
    public function activate( $args, $assoc_args ) {
        if ( empty( $args ) ) {
            WP_CLI::error( 'Please specify a plugin to activate.' );
        }

        $plugin = $args[0];

        // Simulate plugin activation.
        WP_CLI::line( "Activating plugin: {$plugin}..." );
        sleep(1);
        WP_CLI::success( "Plugin {$plugin} activated." );
    }
}

WP_CLI::add_command( 'my-plugin', 'My_Plugin_Command' );

if ( class_exists( 'My_Plugin_Command' ) ) {
  WP_CLI::add_command( 'my-plugin activate', array( new My_Plugin_Command, 'activate' ) );
}

注意,这里我们注册了两个命令: my-pluginmy-plugin activate

  • WP_CLI::add_command( 'my-plugin', 'My_Plugin_Command' );My_Plugin_Command 类注册为 my-plugin 命令。 执行 wp my-plugin 会实例化 My_Plugin_Command 类,并调用 __invoke() 方法 (如果存在)。
  • WP_CLI::add_command( 'my-plugin activate', array( new My_Plugin_Command, 'activate' ) );My_Plugin_Command 类的 activate 方法注册为 my-plugin activate 命令。 执行 wp my-plugin activate 会调用 My_Plugin_Command 类的 activate 方法。

现在,你可以尝试输入 wp my-plugin activate my-plugin/my-plugin.php,你应该可以看到模拟的插件激活过程。

高级技巧:使用 add_command() 的第三个参数

WP_CLI::add_command() 的第三个参数 $args 是一个可选的数组,可以用来传递一些额外的参数给命令。 这些参数可以在命令的执行过程中使用。

例如:

<?php

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

class My_Command {
    /**
     * A command that uses extra arguments.
     *
     * ## EXAMPLES
     *
     *     wp my-command
     *
     * @when before_wp_load
     */
    public function __invoke( $args, $assoc_args, $command_args ) {
        WP_CLI::line( "Extra argument: " . $command_args['message'] );
    }
}

WP_CLI::add_command( 'my-command', 'My_Command', array( 'message' => 'Hello from extra arguments!' ) );

在这个例子中,我们向 add_command() 传递了一个 $args 数组,其中包含一个 'message' 键值对。 在 __invoke() 方法中,我们可以通过 $command_args['message'] 来获取这个参数的值。

运行 wp my-command,你会看到 "Extra argument: Hello from extra arguments!"。

总结:WP_CLI::add_command() 的精髓

WP_CLI::add_command() 是 WP-CLI 自定义命令的核心函数。 它负责将 PHP 类或函数注册为 WP-CLI 的命令,并将其存储到 $commands 数组中。 当执行命令时,WP-CLI 会从 $commands 数组中查找对应的命令信息,并执行相应的类或函数。

理解 WP_CLI::add_command() 的源码,可以帮助我们更好地理解 WP-CLI 的工作原理,并编写更强大的自定义命令。

希望今天的讲座对你有所帮助。 记住,实践是检验真理的唯一标准。 多写代码,多尝试,你就能成为 WP-CLI 自定义命令的大师! 谢谢大家!

发表回复

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