探讨 WP_REST_Controller 的类继承体系与接口定义

WP_REST_Controller 类继承体系与接口定义

大家好,今天我们来深入探讨 WordPress REST API 中 WP_REST_Controller 类的继承体系以及它所定义的接口。WP_REST_Controller 是构建自定义 REST API 端点的核心类,理解它的结构和功能对于开发健壮、可维护的 API 至关重要。

1. WP_REST_Controller 的角色和职责

WP_REST_Controller 本身是一个抽象类,它的主要职责是:

  • 定义通用的 API 端点注册和处理逻辑: 它提供了一些基础方法,用于注册路由、处理请求参数、执行权限验证等。
  • 提供钩子和过滤器: 允许开发者在 API 请求的不同阶段进行自定义操作。
  • 作为自定义控制器类的基类: 鼓励开发者通过继承 WP_REST_Controller 来构建自己的 API 端点逻辑,实现代码复用和模块化。

2. 继承体系

WP_REST_Controller 并没有直接的父类,它是 WordPress REST API 框架中的一个基类。然而,我们通常会继承它来实现特定的 API 控制器。一个常见的例子是 WP_REST_Posts_Controller,它负责处理文章相关的 API 请求。

以下是一个简单的继承关系示例:

Object
  └─ WP_REST_Controller
      └─ WP_REST_Posts_Controller
          └─ Your_Custom_Posts_Controller

在这个例子中,Your_Custom_Posts_Controller 继承了 WP_REST_Posts_Controller,而 WP_REST_Posts_Controller 又继承了 WP_REST_Controller。这允许 Your_Custom_Posts_Controller 继承 WP_REST_Posts_Controller 已有的文章相关逻辑,并在此基础上进行定制。

3. 关键方法和属性

WP_REST_Controller 类定义了一些重要的属性和方法,这些属性和方法在子类中经常被使用和重写。

3.1 属性

  • $namespace: (string) API 的命名空间,例如 'my-plugin/v1'
  • $rest_base: (string) API 的路由基地址,例如 'posts'

3.2 方法

以下是一些常用的方法,以及它们的作用:

方法名 描述
register_routes() 必须实现。 用于注册 API 的路由。在这个方法中,你需要使用 register_rest_route() 函数来定义你的 API 端点。
get_item( $request ) 用于获取单个资源。例如,获取 ID 为 1 的文章。
get_items( $request ) 用于获取资源列表。例如,获取所有文章。
create_item( $request ) 用于创建新的资源。例如,创建一个新的文章。
update_item( $request ) 用于更新已有的资源。例如,更新 ID 为 1 的文章。
delete_item( $request ) 用于删除资源。例如,删除 ID 为 1 的文章。
prepare_item_for_response( $item, $request ) 用于格式化资源数据,使其符合 API 响应的格式。这个方法允许你添加额外的字段、转换数据类型等。
get_item_schema() 用于定义 API 资源的 Schema。Schema 描述了资源的结构、字段类型、验证规则等。这对于 API 的文档生成和客户端的自动验证非常有用。
get_collection_params() 用于定义 API 集合端点的参数。例如,可以定义 per_page 参数来控制每页返回的资源数量。
get_endpoint_args_for_item_schema( $method = WP_REST_Server::READABLE ) 根据 Schema 获取端点参数。这可以用于验证请求参数是否符合 Schema 定义。
check_permission( $request ) 检查当前用户是否有权限访问 API 端点。默认情况下,所有用户都可以访问,你需要根据你的需求重写这个方法。通常用于细粒度权限控制。
get_public_item_schema() 获取公开的item schema, 默认返回 get_item_schema(), 可以重写以隐藏敏感信息

4. 代码示例:自定义文章类型 API 控制器

让我们创建一个简单的自定义文章类型 API 控制器。假设我们有一个名为 book 的自定义文章类型。

<?php
/**
 * 自定义文章类型 "book" 的 REST API 控制器.
 */
class WP_REST_Books_Controller extends WP_REST_Controller {

    /**
     * 构造函数.
     */
    public function __construct() {
        $this->namespace = 'my-plugin/v1';
        $this->rest_base = 'books';
    }

    /**
     * 注册路由.
     */
    public function register_routes() {
        register_rest_route( $this->namespace, '/' . $this->rest_base, array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
                'args'                => array(),
            ),
            array(
                'methods'             => WP_REST_Server::CREATABLE,
                'callback'            => array( $this, 'create_item' ),
                'permission_callback' => array( $this, 'create_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
            ),
        ) );

        register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<id>[d]+)', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
                'args'                => array(
                    'context' => array(
                        'default' => 'view',
                    ),
                ),
            ),
            array(
                'methods'             => WP_REST_Server::EDITABLE,
                'callback'            => array( $this, 'update_item' ),
                'permission_callback' => array( $this, 'update_item_permissions_check' ),
                'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ),
            ),
            array(
                'methods'             => WP_REST_Server::DELETABLE,
                'callback'            => array( $this, 'delete_item' ),
                'permission_callback' => array( $this, 'delete_item_permissions_check' ),
                'args'                => array(
                    'force' => array(
                        'default' => false,
                        'type'    => 'boolean',
                        'description' => __( '是否强制删除,不放入回收站。', 'my-plugin' ),
                    ),
                ),
            ),
        ) );

        register_rest_route( $this->namespace, '/' . $this->rest_base . '/schema', array(
            array(
                'methods'             => WP_REST_Server::READABLE,
                'callback'            => array( $this, 'get_item_schema' ),
                'permission_callback' => '__return_true',
            ),
        ) );
    }

    /**
     * 检查获取文章列表的权限.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
     */
    public function get_items_permissions_check( $request ) {
        if ( ! current_user_can( 'read' ) ) {
            return new WP_Error( 'rest_forbidden', __( '抱歉,您没有权限查看文章列表。', 'my-plugin' ), array( 'status' => $this->authorization_status_code() ) );
        }

        return true;
    }

    /**
     * 获取文章列表.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_items( $request ) {
        $args = array(
            'post_type' => 'book',
            'posts_per_page' => 10, // 可以从 $request->get_param( 'per_page' ) 获取
        );

        $query = new WP_Query( $args );

        $data = array();

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

        return rest_ensure_response( $data );
    }

    /**
     * 检查获取单个文章的权限.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has read access for the item, WP_Error object otherwise.
     */
    public function get_item_permissions_check( $request ) {
        return $this->get_items_permissions_check( $request );  // 复用列表权限
    }

    /**
     * 获取单个文章.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_item( $request ) {
        $id = (int) $request['id'];
        $post = get_post( $id );

        if ( empty( $post ) || $post->post_type !== 'book' ) {
            return new WP_Error( 'rest_not_found', __( '找不到该文章。', 'my-plugin' ), array( 'status' => 404 ) );
        }

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

        return rest_ensure_response( $data );
    }

    /**
     * 检查创建文章的权限.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has the correct access, WP_Error object otherwise.
     */
    public function create_item_permissions_check( $request ) {
        if ( ! current_user_can( 'publish_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( '抱歉,您没有权限创建文章。', 'my-plugin' ), array( 'status' => $this->authorization_status_code() ) );
        }

        return true;
    }

    /**
     * 创建文章.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function create_item( $request ) {
        $prepared_post = $this->prepare_item_for_database( $request );

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

        $post_id = wp_insert_post( $prepared_post, true );

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

        if ( ! empty( $post_id ) ) {
            $post = get_post( $post_id );
             $this->update_additional_fields_for_object( $post, $request );
             wp_update_post( (array) $post );
        }

        $post = get_post( $post_id );
        $data = $this->prepare_item_for_response( $post, $request );
        return new WP_REST_Response( $data, 201 ); // 201 Created
    }

    /**
     * 检查更新文章的权限.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return true|WP_Error True if the request has access to update the item, WP_Error object otherwise.
     */
    public function update_item_permissions_check( $request ) {
        if ( ! current_user_can( 'edit_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( '抱歉,您没有权限编辑文章。', 'my-plugin' ), array( 'status' => $this->authorization_status_code() ) );
        }

        return true;
    }

    /**
     * 更新文章.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function update_item( $request ) {
        $id = (int) $request['id'];
        $post = get_post( $id );

        if ( empty( $post ) || $post->post_type !== 'book' ) {
            return new WP_Error( 'rest_not_found', __( '找不到该文章。', 'my-plugin' ), array( 'status' => 404 ) );
        }

        $prepared_post = $this->prepare_item_for_database( $request );

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

       $prepared_post['ID'] = $id; // 确保 ID 被设置

        $post_id = wp_update_post( $prepared_post, true );

        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 details about the request.
     * @return true|WP_Error True if the request has access to delete the item, WP_Error object otherwise.
     */
    public function delete_item_permissions_check( $request ) {
        if ( ! current_user_can( 'delete_posts' ) ) {
            return new WP_Error( 'rest_forbidden', __( '抱歉,您没有权限删除文章。', 'my-plugin' ), array( 'status' => $this->authorization_status_code() ) );
        }

        return true;
    }

    /**
     * 删除文章.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function delete_item( $request ) {
        $id = (int) $request['id'];
        $post = get_post( $id );

        if ( empty( $post ) || $post->post_type !== 'book' ) {
            return new WP_Error( 'rest_not_found', __( '找不到该文章。', 'my-plugin' ), array( 'status' => 404 ) );
        }

        $force = isset( $request['force'] ) ? (bool) $request['force'] : false;

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

        if ( ! $result ) {
            return new WP_Error( 'rest_cannot_delete', __( '无法删除该文章。', 'my-plugin' ), array( 'status' => 500 ) );
        }

        $data = array(
            'deleted'  => true,
            'previous' => $this->prepare_item_for_response( $post, $request ),
        );

        return rest_ensure_response( $data );
    }

    /**
     * 准备文章数据以进行响应.
     *
     * @param WP_Post $post 文章对象.
     * @param WP_REST_Request $request Full details about the request.
     * @return WP_REST_Response
     */
    public function prepare_item_for_response( $post, $request ) {
        $data = array(
            'id'    => $post->ID,
            'title' => get_the_title( $post ),
            'content' => apply_filters( 'the_content', $post->post_content ),
            'status' => $post->post_status,
            'author' => $post->post_author,
            'meta' => get_post_meta( $post->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 );

        // Wrap the data in a response object.
        $response = rest_ensure_response( $data );

        $response->add_links( $this->prepare_links( $post ) );

        return apply_filters( 'rest_prepare_book', $response, $post, $request );
    }

    /**
     * 准备文章数据以保存到数据库.
     *
     * @param WP_REST_Request $request Full details about the request.
     * @return array|WP_Error Array of prepared data, or WP_Error object.
     */
    public function prepare_item_for_database( $request ) {
        $prepared_post = array();

        if ( isset( $request['title'] ) ) {
            $prepared_post['post_title'] = sanitize_text_field( $request['title'] );
        }

        if ( isset( $request['content'] ) ) {
            $prepared_post['post_content'] = wp_kses_post( $request['content'] );
        }

        if ( isset( $request['status'] ) ) {
            $prepared_post['post_status'] = sanitize_text_field( $request['status'] );
        }

        $prepared_post['post_type'] = 'book';

        return $prepared_post;
    }

    /**
     * 获取文章 Schema.
     *
     * @return array
     */
    public function get_item_schema() {
        $schema = array(
            '$schema'              => 'http://json-schema.org/draft-04/schema#',
            'title'                => 'book',
            'type'                 => 'object',
            'properties'           => array(
                'id' => array(
                    'description'  => __( '唯一标识符。', 'my-plugin' ),
                    'type'         => 'integer',
                    'context'      => array( 'view', 'edit', 'embed' ),
                    'readonly'     => true,
                ),
                'title' => array(
                    'description'  => __( '文章标题。', 'my-plugin' ),
                    'type'         => 'string',
                    'context'      => array( 'view', 'edit', 'embed' ),
                    'arg_options' => array(
                        'sanitize_callback' => 'sanitize_text_field',
                    ),
                ),
                'content' => array(
                    'description'  => __( '文章内容。', 'my-plugin' ),
                    'type'         => 'string',
                    'context'      => array( 'view', 'edit' ),
                ),
                'status' => array(
                    'description'  => __( '文章状态。', 'my-plugin' ),
                    'type'         => 'string',
                    'context'      => array( 'view', 'edit' ),
                    'enum'         => array( 'publish', 'draft', 'pending', 'private' ),
                ),
            ),
        );

        return $this->add_additional_fields_schema( $schema );
    }

    /**
     * 获取文章链接.
     *
     * @param WP_Post $post 文章对象.
     * @return array
     */
    protected function prepare_links( $post ) {
        $links = array(
            'self' => array(
                'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ),
            ),
            'collection' => array(
                'href' => rest_url( $this->namespace . '/' . $this->rest_base ),
            ),
        );
        return $links;
    }

    /**
     * Determine the HTTP status for authorization.
     *
     * @since 5.5.0
     *
     * @return int
     */
    protected function authorization_status_code() {

        if ( is_user_logged_in() ) {
            return 403;
        }

        return 401;
    }
}

// 注册控制器
function register_books_routes() {
    $controller = new WP_REST_Books_Controller();
    $controller->register_routes();
}

add_action( 'rest_api_init', 'register_books_routes' );

代码解释:

  • WP_REST_Books_Controller: 继承 WP_REST_Controller,用于处理 book 自定义文章类型的 API 请求。
  • $namespace$rest_base: 定义了 API 的命名空间和路由基地址。
  • register_routes(): 注册了 API 的路由,包括获取文章列表、获取单个文章、创建文章、更新文章和删除文章。
  • get_items_permissions_check(), get_item_permissions_check(), create_item_permissions_check(), update_item_permissions_check(), delete_item_permissions_check(): 检查用户权限。
  • get_items(), get_item(), create_item(), update_item(), delete_item(): 处理 API 请求,并返回相应的数据。
  • prepare_item_for_response(): 格式化文章数据,使其符合 API 响应的格式。
  • prepare_item_for_database(): 准备文章数据以保存到数据库。
  • get_item_schema(): 定义文章的 Schema。
  • prepare_links(): 为response添加相关链接。
  • authorization_status_code(): 根据用户登录状态返回相应的HTTP状态码,用于权限验证失败时返回。
  • register_books_routes(): 实例化控制器并注册路由。
  • add_action( 'rest_api_init', 'register_books_routes' ): 在 rest_api_init 钩子中注册路由。

5. 实现接口需要注意的点

  • 权限验证: 务必实现权限验证逻辑,确保只有授权用户才能访问你的 API 端点。
  • 数据验证和清理: 对所有请求参数进行验证和清理,防止恶意数据注入。使用 WordPress 提供的 sanitize_* 函数来清理数据。
  • 错误处理: 使用 WP_Error 对象来处理错误,并返回合适的 HTTP 状态码。
  • Schema 定义: 尽可能定义清晰的 Schema,这有助于 API 的文档生成和客户端的自动验证。
  • 响应格式: 确保 API 响应的格式一致,易于解析。
  • 兼容性: 考虑到未来的兼容性,尽量使用 WordPress 提供的 API 函数,避免直接操作数据库。
  • 性能: 优化你的 API 代码,减少数据库查询和计算量,提高 API 的响应速度。可以使用缓存来提高性能。

6. 总结

WP_REST_Controller 是构建自定义 WordPress REST API 的基础。 通过继承它,并实现其关键方法,开发者可以创建功能强大,安全可靠的 API 端点。理解其继承体系和接口定义,是高效开发 REST API 的关键。

希望今天的讲解对大家有所帮助!

发表回复

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