WordPress源码深度解析之:`WordPress`的`REST API`:`WP_REST_Controller`基类的设计思想。

各位观众老爷们,大家好!

今天咱们聊聊WordPress REST API的基石:WP_REST_Controller。 这玩意儿听起来玄乎,其实就是个用来偷懒的工具。 程序员嘛,天生就喜欢偷懒,能复用的绝不重写,能抽象的绝不手撸。 WP_REST_Controller 就是为了让咱们在构建REST API的时候,少写重复代码,更优雅地处理各种请求。

咱们先来缕缕思路,搞清楚WP_REST_Controller 要解决什么问题。

一、REST API 的常见套路

构建 REST API,无非就是这几个步骤:

  1. 注册路由 (Routes): 告诉 WordPress,当收到特定 URL 的请求时,应该调用哪个函数来处理。
  2. 验证权限 (Permissions): 判断用户是否有权访问这个 API。 比如,不是管理员就不能删除文章,不是作者就不能编辑别人的文章。
  3. 处理请求 (Requests): 接收请求参数,进行数据处理,比如查询数据库、更新数据等。
  4. 序列化响应 (Responses): 将处理结果转换成 JSON 格式,返回给客户端。

每个 API 接口都要重复这些步骤,想想就头疼。 WP_REST_Controller 就是为了把这些通用逻辑抽出来,让咱们专注于业务逻辑。

二、WP_REST_Controller 登场:架构设计概览

WP_REST_Controller 是一个抽象类,咱们不能直接实例化它。 必须创建一个继承自它的子类,并重写一些方法,才能实现具体的 API 功能。

它的核心思想是:把 API 的通用逻辑封装在基类中,把特定的业务逻辑留给子类来实现。 这样,咱们只需要关注 API 的具体功能,而不用操心那些重复的、繁琐的步骤。

WP_REST_Controller 类本身提供了一些常用的方法和属性,主要包括:

  • $namespace (string): API 的命名空间,用于区分不同的 API 集合。 比如 wp/v2 是 WordPress 核心的 API 命名空间。
  • $rest_base (string): API 的基本路由,用于构建 API 的 URL。 比如 posts 是文章的 API 基本路由。
  • register_routes(): 注册 API 路由的核心方法,需要在子类中重写。
  • get_item(): 获取单个数据项的方法,需要在子类中重写。
  • get_items(): 获取数据项列表的方法,需要在子类中重写。
  • create_item(): 创建数据项的方法,需要在子类中重写。
  • update_item(): 更新数据项的方法,需要在子类中重写。
  • delete_item(): 删除数据项的方法,需要在子类中重写。
  • get_item_schema(): 获取数据项的 schema,用于验证请求参数和生成 API 文档。

这些方法都是抽象方法,也就是说,咱们必须在子类中实现它们,才能让 API 正常工作。

三、实战演练:创建一个简单的 API

光说不练假把式,咱们来创建一个简单的 API,演示一下 WP_REST_Controller 的用法。

假设我们要创建一个 API,用于获取自定义文章类型 book 的列表。

1. 定义文章类型:

首先,注册一个自定义文章类型 book

add_action( 'init', 'register_book_post_type' );

function register_book_post_type() {
  $args = array(
    'public' => true,
    'label'  => 'Books',
    'supports' => array( 'title', 'editor', 'custom-fields' ),
    'show_in_rest' => true, // 确保在 REST API 中显示
  );
  register_post_type( 'book', $args );
}

show_in_rest 设置为 true ,表示这个文章类型应该出现在 REST API 中。 WordPress 会自动创建一个基本的 API 接口,用于获取 book 类型的文章。但是,如果我们想自定义 API 的行为,就需要用到 WP_REST_Controller

2. 创建控制器类:

创建一个名为 Book_REST_Controller 的类,继承自 WP_REST_Controller

class Book_REST_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' ),
      ),
      'schema' => array( $this, 'get_item_schema' ),
    ) );

    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(
          'id' => array(
            'validate_callback' => function( $param, $request, $key ) {
              return is_numeric( $param );
            }
          ),
        ),
      ),
      'schema' => array( $this, 'get_item_schema' ),
    ) );
  }

  /**
   * 检查是否有权限获取数据项列表
   *
   * @param WP_REST_Request $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 请求对象
   * @return WP_REST_Response|WP_Error
   */
  public function get_items( $request ) {
    $args = array(
      'post_type' => 'book',
      'posts_per_page' => 10, // 可以通过请求参数来控制
    );

    $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 );
    }

    return new WP_REST_Response( $data, 200 );
  }

  /**
   * 检查是否有权限获取单个数据项
   *
   * @param WP_REST_Request $request 请求对象
   * @return WP_Error|bool
   */
  public function get_item_permissions_check( $request ) {
    return $this->get_items_permissions_check( $request );
  }

  /**
   * 获取单个数据项
   *
   * @param WP_REST_Request $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', '找不到该书', array( 'status' => 404 ) );
    }

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

    return new WP_REST_Response( $data, 200 );
  }

  /**
   * 准备数据项的响应
   *
   * @param WP_Post $post 文章对象
   * @param WP_REST_Request $request 请求对象
   * @return WP_REST_Response
   */
  public function prepare_item_for_response( $post, $request ) {
    $data = array(
      'id'    => $post->ID,
      'title' => $post->post_title,
      'content' => apply_filters( 'the_content', $post->post_content ),
      'author' => get_the_author_meta( 'display_name', $post->post_author ),
      'meta' => get_post_meta( $post->ID ),
    );

    $response = new WP_REST_Response( $data, 200 );
    $response->add_link( 'self', rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $post->ID ) ) );

    return $response;
  }

  /**
   * 获取数据项的 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' => 'The ID of the book.',
          'type'        => 'integer',
          'context'     => array( 'view', 'edit', 'embed' ),
          'readonly'    => true,
        ),
        'title' => array(
          'description' => 'The title of the book.',
          'type'        => 'string',
          'context'     => array( 'view', 'edit', 'embed' ),
        ),
        'content' => array(
          'description' => 'The content of the book.',
          'type'        => 'string',
          'context'     => array( 'view', 'edit' ),
        ),
        'author' => array(
          'description' => 'The author of the book.',
          'type'        => 'string',
          'context'     => array( 'view', 'edit', 'embed' ),
        ),
      ),
    );

    return $schema;
  }
}

3. 注册控制器:

在 WordPress 初始化时,注册这个控制器。

add_action( 'rest_api_init', 'register_book_rest_controller' );

function register_book_rest_controller() {
  $controller = new Book_REST_Controller();
  $controller->register_routes();
}

4. 代码解析:

  • __construct(): 构造函数,设置 API 的命名空间和基本路由。 $this->namespace 设置为 my-plugin/v1$this->rest_base 设置为 books。 这意味着我们的 API 的 URL 将会是 my-plugin/v1/books
  • register_routes(): 注册 API 路由。 register_rest_route() 函数用于注册路由。 第一个参数是命名空间,第二个参数是路由规则,第三个参数是路由的处理方式。 我们注册了两个路由:
    • my-plugin/v1/books: 用于获取 book 类型的文章列表。 methods 设置为 WP_REST_Server::READABLE,表示只允许 GET 请求。 callback 设置为 $this->get_items,表示当收到 GET 请求时,调用 get_items() 方法来处理。 permission_callback 设置为 $this->get_items_permissions_check,表示在调用 get_items() 方法之前,先调用 get_items_permissions_check() 方法来检查用户是否有权限访问。
    • my-plugin/v1/books/(?P<id>[d]+): 用于获取单个 book 类型的文章。 (?P<id>[d]+) 是一个正则表达式,用于匹配文章的 ID。 id 是一个参数,可以在 get_item() 方法中通过 $request['id'] 来获取。
  • get_items_permissions_check(): 检查是否有权限获取数据项列表。 这里为了简单起见,直接返回 true,表示允许所有用户访问。 在实际项目中,应该根据用户的角色和权限来判断是否允许访问。
  • get_items(): 获取数据项列表。 这个方法使用 WP_Query 来查询 book 类型的文章。 然后,遍历查询结果,调用 prepare_item_for_response() 方法将文章对象转换成 API 响应的格式。
  • get_item_permissions_check(): 检查是否有权限获取单个数据项。
  • get_item(): 获取单个数据项。 这个方法通过文章 ID 来获取文章对象。 如果找不到文章,或者文章类型不是 book,则返回一个错误。 否则,调用 prepare_item_for_response() 方法将文章对象转换成 API 响应的格式。
  • prepare_item_for_response(): 准备数据项的响应。 这个方法将文章对象转换成 API 响应的格式。 它创建一个包含文章 ID、标题、内容和作者信息的数组。 然后,创建一个 WP_REST_Response 对象,并将数据数组设置成响应的内容。 最后,添加一个 self 链接,指向该文章的 API 接口。
  • get_item_schema(): 获取数据项的 schema。 Schema 用于描述 API 响应的数据结构。 它可以用于验证请求参数和生成 API 文档。

5. 测试 API:

现在,就可以通过 REST API 来访问 book 类型的文章了。

  • 获取文章列表: 访问 your-website.com/wp-json/my-plugin/v1/books 可以获取 book 类型的文章列表。
  • 获取单个文章: 访问 your-website.com/wp-json/my-plugin/v1/books/123 可以获取 ID 为 123 的 book 类型的文章。

四、WP_REST_Controller 的优势

使用 WP_REST_Controller 有以下几个优势:

  • 代码复用: 将 API 的通用逻辑封装在基类中,减少重复代码。
  • 代码组织: 将 API 的代码组织成类,提高代码的可读性和可维护性。
  • 权限控制: 提供了一套标准的权限控制机制,方便进行权限验证。
  • 数据验证: 通过 schema 验证请求参数,防止非法数据。
  • API 文档: 通过 schema 生成 API 文档,方便开发者使用。

五、高级技巧:定制化与扩展

WP_REST_Controller 虽然提供了很多便利,但有时候也需要进行定制化和扩展,以满足特定的需求。

  • 自定义路由: 除了标准的 CRUD 操作之外,还可以注册自定义的路由。
  • 自定义权限验证: 可以根据用户的角色和权限,实现更复杂的权限验证逻辑。
  • 自定义数据处理: 可以对请求参数进行预处理,或者对响应数据进行后处理。
  • 自定义序列化: 可以使用不同的序列化方式,比如 XML 或 YAML。

六、WP_REST_Controller 方法详解

方法名 作用 是否需要在子类中重写
register_routes() 注册 API 路由。这是最核心的方法,定义了 API 的 URL 和处理方式。
get_item() 获取单个数据项。
get_items() 获取数据项列表。
create_item() 创建数据项。 是 (如果需要)
update_item() 更新数据项。 是 (如果需要)
delete_item() 删除数据项。 是 (如果需要)
get_item_schema() 获取数据项的 schema,用于验证请求参数和生成 API 文档。 是 (建议)
get_item_permissions_check() 检查是否有权限获取单个数据项。 否 (可以重写)
get_items_permissions_check() 检查是否有权限获取数据项列表。 否 (可以重写)
prepare_item_for_response() 准备数据项的响应。将数据项对象转换成 API 响应的格式。 否 (可以重写)
prepare_response_for_collection() 准备集合的响应。将单个数据项的响应包装成集合的格式。 否 (一般不需要)
get_endpoint_args_for_schema() 获取 endpoint 的参数,用于验证请求参数。 否 (可以重写)

七、总结

WP_REST_Controller 是构建 WordPress REST API 的利器。 掌握了它的用法,就可以更高效、更优雅地构建 API 接口。 当然,这只是个开始, REST API 的世界还有很多值得探索的地方。 希望今天的讲解能帮助大家入门,以后有机会咱们再深入研究。

今天的讲座就到这里,谢谢大家!

发表回复

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