WP-CLI 自定义命令开发指南

WP-CLI 自定义命令开发指南

创建自己的 WP-CLI 命令可能比想象中容易。wp scaffold package(项目仓库)可以动态生成除命令本身之外的项目内容。

概览

WP-CLI 的目标是成为 WordPress 后台的完整替代方案:凡是想在后台执行的操作,都应该有相应的 WP-CLI 命令。命令是 WP-CLI 功能的原子单元,例如 wp plugin install 和 wp plugin activate。命令能够为复杂任务提供简单而精确的操作接口,因此对 WordPress 用户很有帮助。

但 WordPress 后台如同功能极其复杂的瑞士军刀,单凭一个项目不可能覆盖所有使用场景。因此 WP-CLI 一方面提供常用内置命令,另一方面提供丰富的内部 API,让第三方编写和注册自己的命令。

命令可以作为独立软件包分发,也可以随 WordPress 插件或主题提供。独立软件包可发布为 Composer 包,并通过 Packagist 发现;同样可以用 wp scaffold package 生成命令之外的配套内容。

软件包之于 WP-CLI,就像插件之于 WordPress,但两者的开发方式有所不同。尽管 WP-CLI 不断扩展其替代 /wp-admin 的能力,编写软件包时仍应先考虑如何使用 WP-CLI 内部 API,再考虑与 WordPress API 的交互。

命令类型

内置命令:

  • 通常覆盖标准 WordPress 安装提供的功能,但也有例外,例如 wp search-replace。
  • 不依赖插件、主题等其他组件。
  • 由 WP-CLI 团队维护。

第三方命令:

  • 可以在插件或主题中定义。
  • 可以用 wp scaffold package 轻松生成独立项目骨架。
  • 可以独立于插件和主题,以 Composer 包、Git 仓库、本地路径或 ZIP 文件的形式分发。

所有命令都应遵循文档规范。

命令的构成

WP-CLI 支持将可调用的类、函数或闭包注册为命令。内置命令与第三方命令都使用 WP_CLI::add_command() 注册。

命令的 synopsis(用法概要)定义它接受的位置参数和关联参数。例如 wp plugin install:

$ wp plugin install
usage: wp plugin install <plugin|zip|url>... [--version=<version>] [--force] [--activate] [--activate-network]

这里的 <plugin|zip|url>... 是位置参数;该命令可以多次接收同类位置参数,即待安装插件的 slug、ZIP 文件或 URL。[--version=<version>] 是关联参数,用于指定插件版本。定义外围的方括号表示该参数可选。

WP-CLI 还提供适用于所有命令的一组全局参数。例如加入 --debug 后,会显示所有 PHP 错误,并输出更详细的 WP-CLI 启动信息。

注册时必需的参数

WP_CLI::add_command() 要求两个参数:

  1. $name:WP-CLI 命名空间内的命令名称,例如 plugin install 或 post list。
  2. $callable:命令实现,可以是可调用的类、函数或闭包。

以下四种 wp foo 在功能上等价:

// 1. Command is a function
function foo_command( $args ) {
    WP_CLI::success( $args[0] );
}
WP_CLI::add_command( 'foo', 'foo_command' );

// 2. Command is a closure
$foo_command = function( $args ) {
    WP_CLI::success( $args[0] );
}
WP_CLI::add_command( 'foo', $foo_command );

// 3. Command is a method on a class
class Foo_Command {
    public function __invoke( $args ) {
        WP_CLI::success( $args[0] );
    }
}
WP_CLI::add_command( 'foo', 'Foo_Command' );

// 4. Command is a method on a class with constructor arguments
class Foo_Command {
    protected $bar;
    public function __construct( $bar ) {
        $this->bar = $bar;
    }
    public function __invoke( $args ) {
        WP_CLI::success( $this->bar . ':' . $args[0] );
    }
}
$instance = new Foo_Command( 'Some text' );
WP_CLI::add_command( 'foo', $instance );

类与函数、闭包有一些不同:

  • 类的公共方法会注册为子命令。例如 Foo 类的 bar() 方法会注册为 wp foo bar。
  • __invoke() 是特殊方法。若类实现了它,命令名称会绑定到该方法,类中的其他方法就不会再注册为命令。

命令既可注册到独立的顶级命名空间(如 wp foo),也可成为现有命名空间的子命令(如 wp core foo)。后一种情况下,在命令定义中包含已有命名空间即可:

class Foo_Command {
    public function __invoke( $args ) {
        WP_CLI::success( $args[0] );
    }
}
WP_CLI::add_command( 'core foo', 'Foo_Command' );

快速执行一次性脚本

如果只是为一次性任务编写短脚本,无需正式调用 WP_CLI::add_command() 注册,可以使用 wp eval-file。例如 simple-command.php:

<?php
WP_CLI::success( "The script has run!" );

运行 wp eval-file simple-command.php 即可执行它。若脚本不需要加载 WordPress,可加入 --skip-wordpress。

可选的注册参数

命令的配置选项可通过 PHPDoc 声明,也可作为 WP_CLI::add_command() 的第三个 $args 参数传入。

使用 PHPDoc

下面是一个完整示例:

<?php
/**
 * Implements example command.
 */
class Example_Command {

	/**
	 * Prints a greeting.
	 *
	 * ## OPTIONS
	 *
	 * <name>
	 * : The name of the person to greet.
	 *
	 * [--type=<type>]
	 * : Whether or not to greet the person with success or error.
	 * ---
	 * default: success
	 * options:
	 *   - success
	 *   - error
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     wp example hello Newman
	 *
	 * @when after_wp_load
	 */
	function hello( $args, $assoc_args ) {
		list( $name ) = $args;

		// Print the message with type
		$type = $assoc_args['type'];
		WP_CLI::$type( "Hello, $name!" );
	}
}

WP_CLI::add_command( 'example', 'Example_Command' );

这段 PHPDoc 会从三个方面解释。

Shortdesc:简短描述

PHPDoc 第一行就是 shortdesc:

	/**
	 * Prints a greeting.

Longdesc:详细描述

PHPDoc 中间部分就是 longdesc:

	 * ## OPTIONS
	 *
	 * <name>
	 * : The name of the person to greet.
	 *
	 * [--type=<type>]
	 * : Whether or not to greet the person with success or error.
	 * ---
	 * default: success
	 * options:
	 *   - success
	 *   - error
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     wp example hello Newman

longdesc 中定义的选项会解释为命令的 synopsis:

  • <name> 是必需的位置参数;<name>... 表示一个或多个;[<name>] 表示可选;[<name>...] 表示多个可选位置参数。
  • [--type=<type>] 是可选关联参数,默认值为 success,可接受 success 或 error。改成 [--error] 就成为可选布尔标志。
  • [--field[=<value>]] 允许参数有值或无值。例如全局参数 --skip-plugins[=<plugins>] 可以跳过全部插件,也可以只跳过逗号分隔列表中的插件。
  • [--status=<status>...] 的结尾省略号声明可重复的关联参数。同一标志多次出现(如 –status=active –status=inactive)时,值会收集为数组,$assoc_args['status'] 为 [ 'active', 'inactive' ]。不带省略号时,多次传值只保留最后一个。布尔标志(如 [–verbose])始终保留最后一个值,无论是否带省略号,都不会收集成数组。

要接受任意数量的可选关联参数,可以使用 [--<field>=<value>]:

	 * [--<field>=<value>]
	 * : Allow unlimited number of associative parameters.

命令 synopsis 会在参数传给具体实现之前,用于校验参数。

参数别名

在 synopsis 的参数标记内,用 | 分隔并追加一个或多个别名。用户可以输入短形式或其他名称,WP-CLI 会自动将其映射到 $assoc_args 中的规范参数名:

 * [--with-dependencies|w]
 * : Include dependencies in the operation.

此定义同时接受 –with-dependencies 和 -w,两者都会填充 $assoc_args['with-dependencies']。

多个别名用额外的 | 分隔:

 * [--verbose|v|wordy]
 * : Enable verbose output.

带值的关联参数同样支持别名:

 * [--format=<format>|f]
 * : Output format.

wp example hello -f=json 等价于 wp example hello --format=json。短别名支持单连字符形式 -<alias>=value,参数名称支持双连字符形式 --<name>=value。

YAML 参数选项

参数描述之后的 --- 块允许使用 YAML 设置额外元数据。default 指定省略参数时采用的默认值:

 * [--type=<type>]
 * : Whether or not to greet the person with success or error.
 * ---
 * default: success
 * options:
 *   - success
 *   - error
 * ---

options 限制允许输入的值;不在列表中的值会触发校验错误。

sensitive 标记密码或 API 密钥等敏感参数。使用全局标志 –prompt 时,WP-CLI 会向标准输出记录即将运行的完整命令;被标记为 sensitive: true 的参数值会在该行中替换为 [REDACTED],防止敏感值出现在该日志中。

 * [--password=<password>]
 * : Database password.
 * ---
 * sensitive: true
 * ---

longdesc 也会显示在 help 输出中,例如 wp help example hello。其语法使用 Markdown Extra,处理规则如下:

  • 通常按自由格式文本处理,OPTIONS 和 EXAMPLES 只是常用且推荐的章节名,并非强制要求。
  • 章节名(## NAME)会着色显示,不缩进。
  • 其他内容缩进 2 个字符;选项描述再增加 2 个字符。
  • 为了尽量利用行宽、避免下一行仅剩一两个单词,选项描述应在冒号和空格之后按 75 字符硬换行,其他内容按 90 字符硬换行。

更多排版细节见 WP-CLI 文档规范。

Docblock 标签

最后一部分紧接在 longdesc 后:

	 * @when after_wp_load
	 */

支持以下标签。

@subcommand

有时方法名称不能直接等于子命令名称。原文以 PHP 保留字 list 为例,此时可通过 @subcommand 指定对外的子命令名称:

	/**
	 * @subcommand list
	 */
	function _list( $args, $assoc_args ) {
		...
	}

	/**
	 * @subcommand do-chores
	 */
	function do_chores( $args, $assoc_args ) {
		...
	}

@alias

给子命令添加另一种调用方式:

	/**
	 * @alias hi
	 */
	function hello( $args, $assoc_args ) {
		...
	}
$ wp example hi Joe
Success: Hello, Joe!

@when

指定 WP-CLI 何时执行命令,支持所有已注册 WP-CLI 钩子。多数命令在 WordPress 加载之后执行,默认行为为:

@when after_wp_load

要在 WordPress 加载之前运行:

@when before_wp_load

多数 WP-CLI 钩子在 WordPress 加载之前触发。如果命令从插件或主题加载,那么 WordPress 此时已经加载完成,@when 实际上会被忽略,不再产生预期的提前执行效果。

@skipglobalargcheck

当命令定义的参数名称与已有全局参数(例如 –debug、–user、–quiet)冲突时,WP-CLI 会在注册阶段发出警告。它提醒作者避免全局参数优先于命令参数而造成混淆。

若有意复用全局参数名称,例如封装另一个使用相同标志的工具,可以用 @skipglobalargcheck 关闭该警告:

	/**
	 * @skipglobalargcheck
	 * @when before_wp_load
	 */
	function my_command( $args, $assoc_args ) {
		...
	}

WP_CLI::add_command() 的第三个 $args 参数

PHPDoc 支持的配置也可以在注册时通过第三个参数传入:

$hello_command = function( $args, $assoc_args ) {
	list( $name ) = $args;
	$type = $assoc_args['type'];
	WP_CLI::$type( "Hello, $name!" );
	if ( isset( $assoc_args['honk'] ) ) {
		WP_CLI::log( 'Honk!' );
	}
};
WP_CLI::add_command( 'example hello', $hello_command, array(
	'shortdesc' => 'Prints a greeting.',
	'synopsis' => array(
		array(
			'type'        => 'positional',
			'name'        => 'name',
			'description' => 'The name of the person to greet.',
			'optional'    => false,
			'repeating'   => false,
		),
		array(
			'type'        => 'assoc',
			'name'        => 'type',
			'description' => 'Whether or not to greet the person with success or error.',
			'optional'    => true,
			'default'     => 'success',
			'options'     => array( 'success', 'error' ),
		),
		array(
			'type'     => 'flag',
			'name'     => 'honk',
			'optional' => true,
		),
	),
	'when' => 'after_wp_load',
	'longdesc' =>   '## EXAMPLES' . "\n\n" . 'wp example hello Newman',
) );

longdesc 属性会追加到根据 synopsis 生成的选项描述之后,因此适合用来补充使用示例。如果没有 synopsis,则直接使用 longdesc 作为描述。

命令内部实现

了解注册方法之后,就可以在回调内部实现所需功能。

接收参数

要处理运行时参数,应为可调用对象增加 $args 和 $assoc_args 两个参数:

function hello( $args, $assoc_args ) {
	/* Code goes here*/
}

$args 保存全部位置参数:

$ wp example hello Joe Doe
WP_CLI::line( $args[0] ); // Joe
WP_CLI::line( $args[1] ); // Doe

$assoc_args 保存以 –key=value、–flag 或 –no-flag 形式定义的参数:

$ wp example hello --name='Joe Doe' --verbose --no-option
WP_CLI::line( $assoc_args['name'] ); // Joe Doe
WP_CLI::line( $assoc_args['verbose'] ); // true
WP_CLI::line( $assoc_args['option'] ); // false

两种参数也可以混合使用:

$ wp example hello --name=Joe foo --verbose bar
WP_CLI::line( $assoc_args['name'] ); // Joe
WP_CLI::line( $assoc_args['verbose'] ); // true
WP_CLI::line( $args[0] ); // foo
WP_CLI::line( $args[1] ); // bar

有效复用 WP-CLI 内部 API

假设需要查找多站点网络中所有未使用的主题(问题 #2523)。原文指出,在 WordPress 后台手工完成可能需要数小时甚至数天,而熟悉 WP-CLI 命令开发后,可能用 15 分钟或更短时间写出相应命令。这里的时间是原文情境估计,并非性能测试结果。

对应命令如下:

/**
 * Find unused themes on a multisite network.
 *
 * Iterates through all sites on a network to find themes which aren't enabled
 * on any site.
 */
$find_unused_themes_command = function() {
	$response = WP_CLI::launch_self( 'site list', array(), array( 'format' => 'json' ), false, true );
	$sites = json_decode( $response->stdout );
	$unused = array();
	$used = array();
	foreach( $sites as $site ) {
		WP_CLI::log( "Checking {$site->url} for unused themes..." );
		$response = WP_CLI::launch_self( 'theme list', array(), array( 'url' => $site->url, 'format' => 'json' ), false, true );
		$themes = json_decode( $response->stdout );
		foreach( $themes as $theme ) {
			if ( 'no' == $theme->enabled && 'inactive' == $theme->status && ! in_array( $theme->name, $used ) ) {
				$unused[ $theme->name ] = $theme;
			} else {
				if ( isset( $unused[ $theme->name ] ) ) {
					unset( $unused[ $theme->name ] );
				}
				$used[] = $theme->name;
			}
		}
	}
	WP_CLI\Utils\format_items( 'table', $unused, array( 'name', 'version' ) );
};
WP_CLI::add_command( 'find-unused-themes', $find_unused_themes_command, array(
	'before_invoke' => function(){
		if ( ! is_multisite() ) {
			WP_CLI::error( 'This is not a multisite installation.' );
		}
	},
) );

它用到以下内部 API:

帮助信息的呈现

help 命令会呈现 PHPDoc 或注册定义,顺序为:简短描述、用法概要、详细描述(OPTIONS、EXAMPLES 等),最后是全局参数。

编写测试

WP-CLI 使用基于 Behat 的测试框架,也建议命令作者采用它。Behat 的优势是新测试容易编写,因而更容易真正落实;测试与命令交互的方式也与用户一致。

Behat 测试放在项目的 features/ 目录中。以下示例来自 features/cli-info.feature:

Feature: Review CLI information

  Scenario: Get the path to the packages directory
    Given an empty directory

    When I run `wp cli info --format=json`
    Then STDOUT should be JSON containing:
      """
      {"wp_cli_packages_dir_path":"/tmp/wp-cli-home/.wp-cli/packages/"}
      """

    When I run `WP_CLI_PACKAGES_DIR=/tmp/packages wp cli info --format=json`
    Then STDOUT should be JSON containing:
      """
      {"wp_cli_packages_dir_path":"/tmp/packages/"}
      """

功能测试通常遵循 Given 某种背景、When 用户执行某个操作、Then 应产生某个结果(以及其他结果)的结构。可以从 wp-cli/scaffold-package-command 开始。

全局添加命令

若希望自定义命令全局可用,又不想创建完整插件或软件包,可以使用 WP-CLI 的 require 配置选项。在全局配置文件(通常为 ~/.wp-cli/config.yml)中引入 PHP 文件:

require:
  - ~/.wp-cli/commands.php

随后创建该文件并添加自定义命令:

<?php

WP_CLI::add_command( 'hello-world', function () {
    WP_CLI::success( "Hello World!" );
} );

此文件注册的命令会在每次运行 WP-CLI 时全局可用,适合在所有 WordPress 项目中使用的个人辅助命令。若只需要单次调用加载,可以在命令行传入 --require=<path>,或设置 WP_CLI_REQUIRE 环境变量。

分发

命令开发完成后,可以通过两种常见方式分享。

随插件或主题提供

可以把命令放进插件或主题,根据 WP_CLI 常量是否存在且为真,有条件地加载和注册:

if ( defined( 'WP_CLI' ) && WP_CLI ) {
	require_once dirname( __FILE__ ) . '/inc/class-plugin-cli-command.php';
}

也可用 cli_init 钩子注册命令。它在 WP-CLI Runner 启动期间触发,提供专门的注册时机,不需要检查 WP_CLI 常量:

/**
 * Register custom WP-CLI commands using the cli_init hook.
 */
function myplugin_register_cli_commands() {
	require_once dirname( __FILE__ ) . '/inc/class-plugin-cli-command.php';
	WP_CLI::add_command( 'myplugin', 'MyPlugin_CLI_Command' );
}
add_action( 'cli_init', 'myplugin_register_cli_commands' );

两种方式均可。需要根据 WP-CLI 是否存在决定加载代码时,用常量判断;需要挂接到初始化过程的具体时机时,用 cli_init。

作为独立命令分发

独立命令可以从 Git 仓库、ZIP 文件或目录安装。技术上的要求是提供有效的 composer.json,包含 autoload 声明。建议加入 "type": "wp-cli-package",明确标识软件包类型。

以下是 server 命令的完整 composer.json 示例:


{
	"name": "wp-cli/server-command",
	"description": "Start a development server for WordPress",
	"type": "wp-cli-package",
	"homepage": "https://github.com/wp-cli/server-command",
	"license": "MIT",
	"authors": [
   	    {
      	        "name": "Package Maintainer",
                "email": "packagemaintainer@homepage.com",
                "homepage": "https://www.homepage.com"
            }
        ],
	"require": {
		"php": ">=5.3.29"
	},
	"autoload": {
		"files": [ "command.php" ]
	}
}

注意 autoload 声明,它会加载 command.php。将有效的 composer.json 加入项目仓库后,用户就可以使用包管理器,从存放软件包的位置安装。

Git 仓库

向 package install 传入 Git 仓库的 HTTPS 或 SSH 地址:

# Installing the package using an HTTPS link
$ wp package install https://github.com/wp-cli/server-command.git

# Installing the package using an SSH link
$ wp package install git@github.com:wp-cli/server-command.git

ZIP 文件

向 wp package install 传入 ZIP 文件路径:

# Installing the package using a ZIP file
$ wp package install ~/Downloads/server-command-main.zip

来源:WP-CLI 贡献团队,Commands Cookbook,依据官方手册仓库对应版本翻译。全部源代码、注释和命令原样保留;示例中的历史 PHP 版本要求不代表当前项目支持承诺。本文未执行这些命令。原文涉及 sensitive 的说明针对 WP-CLI 自身打印的日志,不能视为清除 shell 已记录的历史。转载依据站点持有者已声明取得的授权。

许可证

The MIT License (MIT)

Copyright (C) 2012-2022 WP-CLI Development Group (https://github.com/wp-cli/handbook/contributors)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容