概述

要注册一个新的 REST 路由,您必须指定多个回调函数来控制端点行为,例如请求如何被处理、权限检查如何应用,以及您的资源架构如何生成。虽然可以在普通的 PHP 文件中声明所有这些方法而无需任何包装命名空间或类,但以这种方式声明的所有函数都共存于同一个全局作用域中。如果您决定为您的端点逻辑使用通用的函数名(如 get_items()),而另一个插件(或您自己插件中的另一个端点)也注册了同名函数,PHP 将因函数 get_items() 被重复声明而报错致命错误。

您可以通过使用唯一前缀来命名您的回调函数以避免潜在冲突,例如 myplugin_myendpoint_:

function myplugin_myendpoint_register_routes() { /* ... */ }
function myplugin_myendpoint_get_item() { /* ... */ }
function myplugin_myendpoint_get_item_schema() { /* ... */ }
// etcetera

add_action( 'rest_api_init', 'myplugin_myendpoint_register_routes' );

您可能已经熟悉这种方法,因为它常用于主题 functions.php 文件中。然而,这些前缀过于冗长,还有几种更好的选项可以以更可维护的方式对端点逻辑进行分组和封装。

WordPress 目前要求 PHP 5.6 或更高版本。PHP 5.6 支持 命名空间,它提供了一种简单的方式来封装端点的功能。通过在端点 PHP 文件顶部声明一个 namespace,该命名空间内的所有方法都将在此命名空间内声明,不再与全局函数冲突。然后,您可以为端点回调使用更短、更易读的命名。

namespace MyPlugin\API\MyEndpoint;

function register_routes() { /* ... */ }
function get_item() { /* ... */ }
function get_item_schema() { /* ... */ }
// and so on

add_action( 'rest_api_init', __NAMESPACE__ . '\\register_routes' );

虽然这些较短的函数名称更易于使用,但它们并没有提供比声明全局函数更多的优势。因此,WordPress 核心 REST API 端点均使用 控制器类 实现。

本文档其余部分详细介绍了如何编写您自己的控制器类,并解释了这样做的好处。

控制器

控制器接收输入(在 WordPress REST API 的情况下为 WP_REST_Request 对象)并生成响应输出为 WP_REST_Response 对象。让我们来看一个示例控制器类:

class My_REST_Posts_Controller {

    // 在这里初始化我们的命名空间和资源名称。
    public function __construct() {
        $this->namespace     = '/my-namespace/v1';
        $this->resource_name = 'posts';
    }

    // 注册我们的路由。
    public function register_routes() {
        register_rest_route( $this->namespace, '/' . $this->resource_name, array(
            // 在这里我们注册用于集合的可读端点。
            array(
                'methods'   => 'GET',
                'callback'  => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
            ),
            // 注册我们的 schema 回调。
            'schema' => array( $this, 'get_item_schema' ),
        ) );
        register_rest_route( $this->namespace, '/' . $this->resource_name . '/(?P<id>[\d]+)', array(
            // 注意我们如何注册多个端点,'schema' 对应一个 OPTIONS 请求。
            array(
                'methods'   => 'GET',
                'callback'  => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
            ),
            // 注册我们的 schema 回调。
            'schema' => array( $this, 'get_item_schema' ),
        ) );
    }

    /**
     * 检查文章的权限。
     *
     * @param WP_REST_Request $request 当前请求。
     */
    public function get_items_permissions_check( $request ) {
        if ( ! current_user_can( 'read' ) ) {
            return new WP_Error( 'rest_forbidden', esc_html__( '您无法查看文章资源。', 'my-textdomain' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

    /**
     * 获取最新的五篇文章并以 REST 响应形式输出。
     *
     * @param WP_REST_Request $request 当前请求。
     */
    public function get_items( $request ) {
        $args = array(
            'post_per_page' => 5,
        );
        $posts = get_posts( $args );

        $data = array();

        if ( empty( $posts ) ) {
            return rest_ensure_response( $data );
        }

        foreach ( $posts as $post ) {
            $response = $this->prepare_item_for_response( $post, $request );
            $data[] = $this->prepare_response_for_collection( $response );
        }

        // 返回所有我们的评论响应数据。
        return rest_ensure_response( $data );
    }

    /**
     * 检查文章的权限。
     *
     * @param WP_REST_Request $request 当前请求。
     */
    public function get_item_permissions_check( $request ) {
        if ( ! current_user_can( 'read' ) ) {
            return new WP_Error( 'rest_forbidden', esc_html__( '您无法查看文章资源。', 'my-textdomain' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

    /**
     * 获取指定文章 ID 的文章数据并以 REST 响应形式输出。
     *
     * @param WP_REST_Request $request 当前请求。
     */
    public function get_item( $request ) {
        $id = (int) $request['id'];
        $post = get_post( $id );

        if ( empty( $post ) ) {
            return rest_ensure_response( array() );
        }

        $response = $this->prepare_item_for_response( $post, $request );

        // 返回所有我们的文章响应数据。
        return $response;
    }

    /**
     * 将文章数据匹配到我们要的 schema。
     *
     * @param WP_Post $post 正在准备响应的文章对象。
     */
    public function prepare_item_for_response( $post, $request ) {
        $post_data = array();

        $schema = $this->get_item_schema( $request );

        // 我们还将字段重命名为更易理解的名字。
        if ( isset( $schema['properties']['id'] ) ) {
            $post_data['id'] = (int) $post->ID;
        }

        if ( isset( $schema['properties']['content'] ) ) {
            $post_data['content'] = apply_filters( 'the_content', $post->post_content, $post );
        }

        return rest_ensure_response( $post_data );
    }

    /**
     * 准备响应以插入到响应集合中。
     *
     * 此代码复制自 WP REST API v2 插件中的 WP_REST_Controller 类。
     *
     * @param WP_REST_Response $response 响应对象。
     * @return array 响应数据,准备好插入集合数据。
     */
    public function prepare_response_for_collection( $response ) {
        if ( ! ( $response instanceof WP_REST_Response ) ) {
            return $response;
        }

        $data = (array) $response->get_data();
        $server = rest_get_server();

        if ( method_exists( $server, 'get_compact_response_links' ) ) {
            $links = call_user_func( array( $server, 'get_compact_response_links' ), $response );
        } else {
            $links = call_user_func( array( $server, 'get_response_links' ), $response );
        }

        if ( ! empty( $links ) ) {
            $data['_links'] = $links;
        }

        return $data;
    }

    /**
     * 获取文章示例 schema。
     *
     * @return array 文章的示例 schema
     */
    public function get_item_schema() {
        if ( $this->schema ) {
            // 自 WordPress 5.3 起,schema 可以缓存在 $schema 属性中。
            return $this->schema;
        }

        $this->schema = array(
            // 这告诉规范我们正在使用的 JSON Schema 版本是 draft 4。
            '$schema'              => 'http://json-schema.org/draft-04/schema#',
            // title 属性标记资源的标识。
            'title'                => 'post',
            'type'                 => 'object',
            // 在 JSON Schema 中,您可以在 properties 属性中指定对象属性。
            'properties'           => array(
                'id' => array(
                    'description'  => esc_html__( '对象的唯一标识符。', 'my-textdomain' ),
                    'type'         => 'integer',
                    'context'      => array( 'view', 'edit', 'embed' ),
                    'readonly'     => true,
                ),
                'content' => array(
                    'description'  => esc_html__( '对象的内容。', 'my-textdomain' ),
                    'type'         => 'string',
                ),
            ),
        );

        return $this->schema;
    }

    // 设置适当的 HTTP 状态码用于授权。
    public function authorization_status_code() {

        $status = 401;

        if ( is_user_logged_in() ) {
            $status = 403;
        }

        return $status;
    }
}

// 函数用于从控制器注册我们的新路由。
function prefix_register_my_rest_routes() {
    $controller = new My_REST_Posts_Controller();
    $controller->register_routes();
}

add_action( 'rest_api_init', 'prefix_register_my_rest_routes' );

类的优势

此类包含的所有组件都可以使用简单的函数编写。类的结构提供了一种方便的方式来引用相关方法,使用 $this->method_name() 语法,但与命名空间不同,类还允许我们缓存值并共享逻辑。

在 get_item_schema 方法中,请注意我们将生成的 schema 存储在类上作为 $this->schema。类属性使得缓存这类生成的值变得容易。WordPress 5.3 引入的 schema 缓存功能使某些核心 REST API 集合响应速度提高了多达 40%,因此您绝对应该在自己的控制器中遵循这种模式。

类继承与 WP_REST_Controller

我们在上面已经看到类如何解决全局函数封装问题,以及类实例如何用于缓存复杂值以加快响应处理速度。类的另一个主要优势在于类继承的方式,它让您可以在多个端点之间共享逻辑。

我们上面的示例类没有扩展任何基类,但在 WordPress 核心中,所有端点控制器都扩展了一个名为 WP_REST_Controller 的单一 abstract 控制器类。扩展此类可让您访问许多有用的方法,包括但不限于:

特定于端点的方法,如 get_item、register_routes 和 update_item_permissions_check,并未在抽象类中完全实现,必须在您自己的类中定义。

访问 WP_REST_Controller 类参考页面 以获取此控制器方法的完整列表。

需要注意的是,WP_REST_Controller 是作为 abstract 类实现的,仅包含多个类中明显需要的逻辑。继承会将您的类与所扩展的基类耦合,考虑不周的继承树会使您的端点更难维护。

例如,如果您为帖子端点编写了一个控制器类(如上例所示),并且希望同时支持自定义文章类型,则不应像这样扩展您的 My_REST_Posts_Controller:class My_CPT_REST_Controller extends My_REST_Posts_Controller。相反,您应该要么为共享逻辑创建一个完全独立的基础控制器类,要么让 My_REST_Posts_Controller 处理所有可用的文章类型。端点逻辑可能会随业务需求的变化而改变,您不希望每次更新基础帖子控制器时都要更改许多无关的控制器。

在大多数情况下,您希望创建一个基础控制器类,作为 interface 或 abstract class,以便您的每个端点控制器可以实施或扩展它,或者直接扩展 WordPress 核心 REST 类之一。

WordPress 内部 REST API 类

WordPress REST API 为其内部类遵循有意的设计模式,可分为 基础设施 类或 端点 类。

端点类封装了执行 WordPress 资源 CRUD 操作所需的功能逻辑。WordPress 暴露了许多 REST API 端点(例如 WP_REST_Posts_Controller),但如上所述,所有端点都从一个公共基础控制器类扩展而来:

  • WP_REST_Controller: 所有 WordPress 核心端点的基类。该类旨在代表一种用于操作 WordPress 资源的一致模式。当与实现 WP_REST_Controller 的端点交互时,HTTP 客户端可以期望每个端点以一致的方式运行。

基础设施类支持端点类。它们处理 WordPress REST API 的逻辑,而不执行任何数据转换。WordPress REST API 实现了三个关键的基础设施类:

  • WP_REST_Server: WordPress REST API 的主要控制器。路由在 WordPress 内注册到服务器。当调用 WP_REST_Server 来提供服务请求时,它会确定要调用哪个路由,并将路由回调传递一个 WP_REST_Request 对象。WP_REST_Server 还处理身份验证,并可以执行请求验证和权限检查。
  • WP_REST_Request: 表示请求性质的对象。该对象包括请求详情,如请求头、参数和方法,以及路由。它还可以执行请求验证和清理。
  • WP_REST_Response: 表示响应性质的对象。该类扩展了 WP_HTTP_Response,其中包含头、正文和状态码,并提供辅助方法,如用于添加链接媒体的 add_link(),以及用于获取查询导航头的 query_navigation_headers()。

大多数类型的 API 驱动应用程序不需要您扩展或直接与基础设施层交互,但如果要实现您自己的 REST API 端点,您的应用程序很可能会受益于一个或多个扩展自 WP_REST_Controller 的端点控制器类。