解析 WordPress `WP_REST_Controller` 类的源码:如何通过继承它来构建标准的 REST API 端点。

各位观众,各位朋友,大家好! 今天咱们聊点有意思的,就是如何像搭积木一样,用 WordPress 的 WP_REST_Controller 类来构建你自己的 REST API 端点。 放心,这玩意儿没想象中那么难,只要掌握了套路,你也能轻松驾驭它。

开场白:REST API 界的葵花宝典

话说江湖上,武功门派林立,想在 WordPress 这片地盘上混,不会点 REST API 的功夫,那可不行。 WordPress 早就为你准备好了秘籍,就是 WP_REST_Controller 类。 掌握了它,你就能在 WordPress 的 REST API 世界里横着走,想怎么玩就怎么玩。

WP_REST_Controller:你的专属 API 建造师

WP_REST_Controller 是个什么东西? 简单来说,它就是一个基类,你继承它,就能快速搭建出符合 WordPress REST API 规范的端点。 它帮你处理了很多底层细节,比如路由注册、权限验证、参数校验等等,让你专注于业务逻辑。

第一步:创建你的 API 类

首先,你要创建一个类,继承 WP_REST_Controller。 就像盖房子一样,先打好地基。

<?php
/**
 * 我的自定义 API 控制器
 */
class My_Custom_API_Controller extends WP_REST_Controller {

    /**
     * 命名空间和版本
     *
     * @var string
     */
    protected $namespace = 'my-plugin/v1';

    /**
     * 路由基地址
     *
     * @var string
     */
    protected $rest_base = 'my-data';

    /**
     * 构造函数
     */
    public function __construct() {
        // 可选:在这里设置一些初始值,比如权限回调函数
    }

    /**
     * 注册路由
     */
    public function register_routes() {
        // 稍后填充
    }
}
  • $namespace: 你的 API 的命名空间,就像你的地盘一样,要独一无二。 通常是 插件名/版本号 的形式。
  • $rest_base: API 的基地址,比如 my-data,那么你的 API 地址就是 /wp-json/my-plugin/v1/my-data
  • __construct(): 构造函数,可以在这里做一些初始化工作,比如设置权限回调函数。
  • register_routes(): 注册路由的地方,这是最关键的,告诉 WordPress 你的 API 有哪些端点,以及它们对应的处理函数。

第二步:注册你的路由

接下来,你要在 register_routes() 方法里注册你的 API 端点。 这就像给你的房子安装门牌号一样,让别人知道怎么找到你。

/**
 * 注册路由
 */
public function register_routes() {

    register_rest_route(
        $this->namespace,
        '/' . $this->rest_base,
        array(
            array(
                'methods'  => WP_REST_Server::READABLE, // GET
                'callback' => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ), //权限验证
                'args'     => array(), // 参数校验
            ),
            array(
                'methods'  => WP_REST_Server::CREATABLE, // POST
                'callback' => array( $this, 'create_item' ),
                'permission_callback' => array( $this, 'create_item_permissions_check' ),
                'args'     => array(
                    'title' => array(
                        'required' => true,
                        'type'     => 'string',
                        'description' => 'The title of the item.',
                    ),
                    'content' => array(
                        'required' => false,
                        'type'     => 'string',
                        'description' => 'The content of the item.',
                    ),
                ),
            ),
        )
    );

    register_rest_route(
        $this->namespace,
        '/' . $this->rest_base . '/(?P<id>[d]+)',
        array(
            array(
                'methods'  => WP_REST_Server::READABLE, // GET
                'callback' => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
                'args'     => array(
                    'id' => array(
                        'description' => 'The ID of the item.',
                        'type'        => 'integer',
                        'required'    => true,
                    ),
                ),
            ),
            array(
                'methods'  => WP_REST_Server::EDITABLE, // PUT/PATCH
                'callback' => array( $this, 'update_item' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'     => array(
                    'id' => array(
                        'description' => 'The ID of the item.',
                        'type'        => 'integer',
                        'required'    => true,
                    ),
                    'title' => array(
                        'required' => false,
                        'type'     => 'string',
                        'description' => 'The title of the item.',
                    ),
                    'content' => array(
                        'required' => false,
                        'type'     => 'string',
                        'description' => 'The content of the item.',
                    ),
                ),
            ),
            array(
                'methods'  => WP_REST_Server::DELETABLE, // DELETE
                'callback' => array( $this, 'delete_item' ),
                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                'args'     => array(
                    'id' => array(
                        'description' => 'The ID of the item.',
                        'type'        => 'integer',
                        'required'    => true,
                    ),
                    'force' => array(
                        'default'     => false,
                        'type'        => 'boolean',
                        'description' => 'Whether to bypass trash and force deletion.',
                    ),
                ),
            ),
        )
    );
}

register_rest_route() 函数接收三个参数:

  • $namespace: 命名空间。
  • $route: 路由规则,可以是字符串或正则表达式。 (?P<id>[d]+) 表示一个名为 id 的参数,它必须是数字。
  • $args: 一个数组,定义了该路由支持的 HTTP 方法、回调函数、权限验证函数和参数校验规则。

$args 数组详解:

  • methods: HTTP 方法,可以是 WP_REST_Server::READABLE (GET), WP_REST_Server::CREATABLE (POST), WP_REST_Server::EDITABLE (PUT/PATCH), WP_REST_Server::DELETABLE (DELETE)。
  • callback: 处理请求的回调函数,也就是你写的业务逻辑。
  • permission_callback: 权限验证回调函数,决定谁可以访问这个端点。
  • args: 参数校验规则,定义了哪些参数是必须的,它们的类型是什么,等等。

参数校验(args)的细节:

属性 描述
required 布尔值,表示参数是否必须。 true 表示必须,false 表示可选。
type 参数类型,可以是 stringintegerbooleanarrayobjectnumber
description 参数描述,用于 API 文档。
default 参数默认值,如果用户没有传递该参数,则使用默认值。
validate_callback 一个回调函数,用于自定义参数验证。 你可以编写自己的函数来验证参数是否符合你的要求。 如果验证失败,则返回一个 WP_Error 对象。
sanitize_callback 一个回调函数,用于对参数进行清理和过滤。 你可以编写自己的函数来清理参数,比如去除空格、HTML 标签等。
enum 一个数组,列出了参数允许的值。 例如,'enum' => array( 'pending', 'publish', 'draft' ) 表示参数只能是 pendingpublishdraft 中的一个。

第三步:编写你的业务逻辑

现在,你可以编写你的业务逻辑了,也就是 callback 对应的函数。 这些函数负责处理请求,从数据库中读取数据,或者将数据写入数据库,等等。

/**
 * 获取多个数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response
 */
public function get_items( $request ) {
    // 获取参数
    $args = array(
        'posts_per_page' => $request['per_page'] ?? 10, // 使用空合并运算符设置默认值
        'paged'          => $request['page'] ?? 1,
    );

    $query = new WP_Query( $args );

    $data = array();

    foreach ( $query->posts as $post ) {
        $item = $this->prepare_item_for_response( $post, $request );
        $data[] = $this->prepare_response_for_collection( $item ); // 确保以集合形式返回
    }

    $response = rest_ensure_response( $data );

    // 添加分页信息到响应头
    $total_posts = $query->found_posts;
    $max_pages = ceil( $total_posts / $args['posts_per_page'] );

    $response->header( 'X-WP-Total', (int) $total_posts );
    $response->header( 'X-WP-TotalPages', (int) $max_pages );

    return $response;
}

/**
 * 获取单个数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response
 */
public function get_item( $request ) {
    $id = (int) $request['id'];

    // 验证 ID 是否有效
    if ( ! get_post( $id ) ) {
        return new WP_Error( 'rest_not_found', '资源不存在', array( 'status' => 404 ) );
    }

    $post = get_post( $id );
    $data = $this->prepare_item_for_response( $post, $request );

    return rest_ensure_response( $data );
}

/**
 * 创建一个数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response
 */
public function create_item( $request ) {
    $title   = sanitize_text_field( $request['title'] );
    $content = wp_kses_post( $request['content'] ); // 允许一些 HTML 标签

    $post_id = wp_insert_post( array(
        'post_title'   => $title,
        'post_content' => $content,
        'post_status'  => 'publish',
    ) );

    if ( is_wp_error( $post_id ) ) {
        return $post_id;
    }

    $post = get_post( $post_id );
    $data = $this->prepare_item_for_response( $post, $request );

    return rest_ensure_response( $data );
}

/**
 * 更新一个数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response
 */
public function update_item( $request ) {
    $id      = (int) $request['id'];
    $title   = isset( $request['title'] ) ? sanitize_text_field( $request['title'] ) : null;
    $content = isset( $request['content'] ) ? wp_kses_post( $request['content'] ) : null;

    // 验证 ID 是否有效
    if ( ! get_post( $id ) ) {
        return new WP_Error( 'rest_not_found', '资源不存在', array( 'status' => 404 ) );
    }

    $update_data = array(
        'ID' => $id,
    );

    if ( ! is_null( $title ) ) {
        $update_data['post_title'] = $title;
    }

    if ( ! is_null( $content ) ) {
        $update_data['post_content'] = $content;
    }

    $post_id = wp_update_post( $update_data );

    if ( is_wp_error( $post_id ) ) {
        return $post_id;
    }

    $post = get_post( $post_id );
    $data = $this->prepare_item_for_response( $post, $request );

    return rest_ensure_response( $data );
}

/**
 * 删除一个数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|WP_REST_Response
 */
public function delete_item( $request ) {
    $id    = (int) $request['id'];
    $force = isset( $request['force'] ) ? (bool) $request['force'] : false;

    // 验证 ID 是否有效
    if ( ! get_post( $id ) ) {
        return new WP_Error( 'rest_not_found', '资源不存在', array( 'status' => 404 ) );
    }

    $result = wp_delete_post( $id, $force );

    if ( ! $result ) {
        return new WP_Error( 'rest_cannot_delete', '无法删除资源', array( 'status' => 500 ) );
    }

    return rest_ensure_response( array( 'deleted' => true ) );
}

这些函数都接收一个 $request 参数,它是一个 WP_REST_Request 对象,包含了请求的所有信息,比如 URL 参数、请求体、请求头等等。

  • get_items(): 获取多个数据项的列表。
  • get_item(): 获取单个数据项。
  • create_item(): 创建一个新的数据项。
  • update_item(): 更新一个已有的数据项。
  • delete_item(): 删除一个数据项。

prepare_item_for_response()prepare_response_for_collection()

  • prepare_item_for_response(): 这个函数负责将你的数据转换为 REST API 响应的格式。 你可以自定义这个函数来控制响应的结构和内容。
  • prepare_response_for_collection(): 这个函数将单个项目准备为集合的一部分。 这对于确保响应格式的一致性至关重要,特别是对于列表端点。
/**
 * 准备数据项以进行响应
 *
 * @param mixed $item 数据项.
 * @param WP_REST_Request $request 请求对象.
 *
 * @return WP_REST_Response WordPress 数据项实例数据.
 */
public function prepare_item_for_response( $item, $request ) {
    $data = array(
        'id'      => $item->ID,
        'title'   => $item->post_title,
        'content' => $item->post_content,
        'link'    => get_permalink( $item->ID ),
    );

    $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
    $data = $this->add_additional_fields_to_object( $data, $request );
    $data = $this->filter_response_by_context( $data, $context );

    // 创建响应对象
    $response = rest_ensure_response( $data );

    // 添加链接
    $response->add_link( 'self', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $item->ID ) ) );

    return $response;
}

第四步:处理权限验证

权限验证非常重要,可以防止未经授权的访问。 你需要在 permission_callback 对应的函数里编写权限验证逻辑。

/**
 * 检查是否允许获取数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|bool
 */
public function get_items_permissions_check( $request ) {
    // 只有登录用户才能访问
    if ( ! is_user_logged_in() ) {
        return new WP_Error( 'rest_forbidden', '您没有权限查看资源', array( 'status' => 401 ) );
    }

    return true; // 允许访问
}

/**
 * 检查是否允许获取单个数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|bool
 */
public function get_item_permissions_check( $request ) {
    return $this->get_items_permissions_check( $request ); // 使用相同的权限检查
}

/**
 * 检查是否允许创建数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|bool
 */
public function create_item_permissions_check( $request ) {
    // 只有管理员才能创建
    if ( ! current_user_can( 'manage_options' ) ) {
        return new WP_Error( 'rest_forbidden', '您没有权限创建资源', array( 'status' => 403 ) );
    }

    return true; // 允许访问
}

/**
 * 检查是否允许更新数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|bool
 */
public function update_item_permissions_check( $request ) {
    return $this->create_item_permissions_check( $request ); // 使用相同的权限检查
}

/**
 * 检查是否允许删除数据项
 *
 * @param WP_REST_Request $request Full data about the request.
 * @return WP_Error|bool
 */
public function delete_item_permissions_check( $request ) {
    return $this->create_item_permissions_check( $request ); // 使用相同的权限检查
}
  • is_user_logged_in(): 检查用户是否登录。
  • current_user_can( 'manage_options' ): 检查当前用户是否具有 manage_options 权限,也就是管理员权限。

第五步:注册你的 API 类

最后,你需要注册你的 API 类,让 WordPress 知道它的存在。 你可以使用 add_action() 函数,在 rest_api_init 钩子上注册。

add_action( 'rest_api_init', function () {
    $controller = new My_Custom_API_Controller();
    $controller->register_routes();
} );

错误处理:你的 API 的急救箱

在编写 API 的时候,错误处理非常重要。 你可以使用 WP_Error 类来返回错误信息。

return new WP_Error( 'my_error_code', '错误信息', array( 'status' => 400 ) );
  • my_error_code: 错误代码,用于标识错误类型。
  • 错误信息: 错误信息,用于向用户解释错误原因。
  • status: HTTP 状态码,用于表示错误的严重程度。

总结:REST API 的乐高世界

WP_REST_Controller 类就像一个乐高积木盒,里面有很多积木块,你可以根据自己的需要,自由组合,搭建出各种各样的 REST API 端点。 只要你掌握了这些积木块的用法,就能在 WordPress 的 REST API 世界里玩得转。

一些额外的提示:

  • 使用版本控制: 在你的 API 中使用版本控制,可以让你在不破坏现有 API 的情况下,添加新的功能。
  • 编写 API 文档: 好的 API 文档可以帮助开发者更好地使用你的 API。
  • 测试你的 API: 在发布你的 API 之前,一定要进行充分的测试,确保它能够正常工作。
  • 遵循 RESTful 规范: 尽量遵循 RESTful 规范,让你的 API 更加易于理解和使用。

好了,今天的讲座就到这里。 希望大家能够通过 WP_REST_Controller 类,构建出强大而优雅的 WordPress REST API。 谢谢大家!

发表回复

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