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

各位听众,晚上好!今天咱们来聊聊 WordPress REST API 的骨架——WP_REST_Controller 类。这玩意儿就像盖楼的地基,你想在 WordPress 里搭建自己的 REST API 大厦,就得先搞清楚这地基怎么打。

一、 为什么需要 WP_REST_Controller

想象一下,如果没有一个统一的标准,每个人都按照自己的方式来创建 API 端点,那场面简直混乱不堪。你可能需要花大量时间去理解每个插件或主题的 API 使用方式,调试起来更是噩梦。

WP_REST_Controller 的出现就是为了解决这个问题。它提供了一个标准的框架,帮你规范化地创建 REST API 端点。就像一个模版,让你按照固定的格式去填充内容,从而保证 API 的一致性、可维护性和可扩展性。

二、 WP_REST_Controller 的核心概念

WP_REST_Controller 本身是一个抽象类,你不能直接实例化它。你需要创建一个新的类,继承它,并实现其中的一些方法。

简单来说,WP_REST_Controller 帮你完成了以下几件事:

  • 注册路由: 告诉你应该把 API 端点注册到哪里,用什么 URL 访问。
  • 请求方法处理: 帮你区分 GETPOSTPUTDELETE 等不同的请求方法,并分配给相应的处理函数。
  • 权限验证: 帮你检查用户是否有权限访问该 API 端点。
  • 数据格式化: 帮你将数据转换为标准的 JSON 格式,方便客户端解析。

三、 动手实践:创建一个自定义的 REST API 端点

咱们现在就来一步步创建一个简单的 REST API 端点,这个端点可以获取和修改自定义的文章类型 "book" 的信息。

1. 定义自定义文章类型 (如果还没做)

首先,确保你已经定义了一个名为 "book" 的自定义文章类型。如果没有,可以在你的主题的 functions.php 文件或插件中添加以下代码:

add_action( 'init', 'create_book_post_type' );
function create_book_post_type() {
  register_post_type( 'book',
    array(
      'labels' => array(
        'name' => __( 'Books' ),
        'singular_name' => __( 'Book' )
      ),
      'public' => true,
      'has_archive' => true,
      'supports' => array( 'title', 'editor', 'custom-fields' ), // 支持的字段
      'show_in_rest' => true,  // 关键:让它在 REST API 中可用
    )
  );
}

show_in_rest => true 是关键,它会告诉 WordPress 将这个文章类型暴露在 REST API 中。不过,我们这里要创建的是自定义的端点,所以这个其实不是必须的。

2. 创建控制器类

创建一个新的 PHP 文件,例如 class-wp-rest-book-controller.php,并添加以下代码:

<?php

/**
 * REST controller for Books.
 */
class WP_REST_Book_Controller extends WP_REST_Controller {

  /**
   * The base of this route.
   *
   * @var string
   */
  protected $base = 'books';  // API 端点的基本路径,例如 /wp-json/myplugin/v1/books

  /**
   * Registers the routes for the objects of the controller.
   *
   * @since 4.7.0
   *
   * @see register_rest_route()
   */
  public function register_routes() {
    $namespace = 'myplugin/v1'; // 命名空间

    register_rest_route( $namespace, '/' . $this->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'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), // 参数验证
      ),
    ) );

    register_rest_route( $namespace, '/' . $this->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(
            'validate_callback' => function( $param, $request, $key ) {
              return is_numeric( $param );  // ID 必须是数字
            }
          ),
        ),
      ),
      array(
        'methods'             => WP_REST_Server::EDITABLE, // PUT
        '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, // DELETE
        'callback'            => array( $this, 'delete_item' ), // 删除书籍
        'permission_callback' => array( $this, 'delete_item_permissions_check' ), // 权限验证
        'args'                => array(
          'force' => array(
            'default'     => false,
            'type'        => 'boolean',
            'description' => __( 'Whether to bypass Trash and force deletion.' ),
          ),
        ),
      ),
    ) );
  }

  /**
   * Checks if a given request has access to get items.
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function get_items_permissions_check( $request ) {
    // 只有管理员才能查看所有书籍
    if ( ! current_user_can( 'manage_options' ) ) {
      return new WP_Error( 'rest_forbidden', __( 'You cannot view the resource.' ), array( 'status' => $this->authorization_status_code() ) );
    }
    return true;
  }

  /**
   * Retrieves all items.
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|WP_REST_Response
   */
  public function get_items( $request ) {
    $posts = get_posts( array(
      'post_type' => 'book',
      'posts_per_page' => -1, // 获取所有书籍
    ) );

    $data = array();

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

  /**
   * Checks if a given request has access to get a specific item.
   *
   * @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 ); // 使用相同的权限检查
  }

  /**
   * Retrieves one item from the collection.
   *
   * @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'];
    $post = get_post( $id );

    if ( empty( $post ) || $post->post_type !== 'book' ) {
      return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
    }

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

    return rest_ensure_response( $data );
  }

  /**
   * Checks if a given request has access to create items.
   *
   * @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( 'publish_posts' ) ) { // 只有能发布文章的用户才能创建书籍
      return new WP_Error( 'rest_forbidden', __( 'You cannot create the resource.' ), array( 'status' => $this->authorization_status_code() ) );
    }
    return true;
  }

  /**
   * Creates one item from the collection.
   *
   * @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'] );

    $post_id = wp_insert_post( array(
      'post_type'   => 'book',
      '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 );
  }

  /**
   * Checks if a given request has access to update a specific item.
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function update_item_permissions_check( $request ) {
    $post = get_post( (int) $request['id'] );
    if ( ! $post || $post->post_type !== 'book' ) {
      return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
    }

    if ( ! current_user_can( 'edit_post', (int) $request['id'] ) ) { // 只有能编辑该文章的用户才能更新
      return new WP_Error( 'rest_forbidden', __( 'You cannot update the resource.' ), array( 'status' => $this->authorization_status_code() ) );
    }
    return true;
  }

  /**
   * Updates one item from the collection.
   *
   * @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 = sanitize_text_field( $request['title'] );
    $content = wp_kses_post( $request['content'] );

    $updated = wp_update_post( array(
      'ID'           => $id,
      'post_title'  => $title,
      'post_content' => $content,
    ) );

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

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

    return rest_ensure_response( $data );
  }

  /**
   * Checks if a given request has access to delete a specific item.
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function delete_item_permissions_check( $request ) {
    $post = get_post( (int) $request['id'] );
    if ( ! $post || $post->post_type !== 'book' ) {
      return new WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.' ), array( 'status' => 404 ) );
    }

    if ( ! current_user_can( 'delete_post', (int) $request['id'] ) ) { // 只有能删除该文章的用户才能删除
      return new WP_Error( 'rest_forbidden', __( 'You cannot delete the resource.' ), array( 'status' => $this->authorization_status_code() ) );
    }
    return true;
  }

  /**
   * Deletes one item from the collection.
   *
   * @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;

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

    if ( ! $deleted ) {
      return new WP_Error( 'rest_cannot_delete', __( 'The post cannot be deleted.' ), array( 'status' => 500 ) );
    }

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

    return rest_ensure_response( $data );
  }

  /**
   * Prepares the item for the REST response.
   *
   * @param mixed $item WordPress representation of the item.
   * @param WP_REST_Request $request Request object.
   * @return WP_REST_Response|WP_Error Response object.
   */
  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 );

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

    $response->add_link( 'self', rest_url( sprintf( '%s/%s/%d', 'myplugin/v1', $this->base, $item->ID ) ) );

    return $response;
  }

  /**
   * Retrieves the item's schema, defining the shape of the data.
   *
   * @return array Public-facing schema for the endpoint.
   */
  public function get_item_schema() {
    if ( $this->schema ) {
      return $this->add_additional_fields_schema( $this->schema );
    }

    $schema = array(
      // This tells the spec of JSON Schema we are using.
      '$schema'              => 'http://json-schema.org/draft-04/schema#',
      // The title property marks the identity of the resource.
      'title'                => 'book',
      'type'                 => 'object',
      // In JSON Schema you can specify object properties in the "properties" attribute.
      'properties'           => array(
        'id' => array(
          'description'  => __( 'Unique identifier for the object.' ),
          'type'         => 'integer',
          'context'      => array( 'view', 'edit', 'embed' ),
          'readonly'     => true,
        ),
        'title' => array(
          'description'  => __( 'The title for the object.' ),
          'type'         => 'string',
          'context'      => array( 'view', 'edit', 'embed' ),
          'arg_options'  => array(
            'sanitize_callback' => 'sanitize_text_field',
          ),
          'required'     => true,
        ),
        'content' => array(
          'description'  => __( 'The content for the object.' ),
          'type'         => 'string',
          'context'      => array( 'view', 'edit' ),
          'arg_options'  => array(
            'sanitize_callback' => 'wp_kses_post',
          ),
        ),
      ),
    );

    $this->schema = $schema;

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

  /**
   * Retrieves the query params for the objects collection.
   *
   * @return array Collection parameters.
   */
  public function get_collection_params() {
    return array(
      'context'  => $this->get_context_param(),
      'page'     => array(
        'description'       => __( 'Current page of the collection.' ),
        'type'              => 'integer',
        'default'           => 1,
        'sanitize_callback' => 'absint',
        'validate_callback' => 'rest_validate_request_arg',
        'minimum'           => 1,
      ),
      'per_page' => array(
        'description'       => __( 'Maximum number of items to be returned in result set.' ),
        'type'              => 'integer',
        'default'           => 10,
        'sanitize_callback' => 'absint',
        'validate_callback' => 'rest_validate_request_arg',
        'maximum'           => 100,
      ),
      'search'   => array(
        'description'       => __( 'Limit results to those matching a string.' ),
        'type'              => 'string',
        'sanitize_callback' => 'sanitize_text_field',
        'validate_callback' => 'rest_validate_request_arg',
      ),
    );
  }

  /**
   * Retrieves the authorization callback's status code.
   *
   * @since 4.7.0
   *
   * @return int Status code.
   */
  protected function authorization_status_code() {

    if ( is_user_logged_in() ) {
      return 403;
    }

    return 401;
  }
}

3. 注册路由

在你的主题的 functions.php 文件或插件中,引入并实例化你的控制器类,并注册路由:

add_action( 'rest_api_init', 'register_book_routes' );
function register_book_routes() {
  require_once 'class-wp-rest-book-controller.php';  // 引入控制器类文件

  $controller = new WP_REST_Book_Controller();
  $controller->register_routes();
}

4. 代码详解

现在,咱们来详细分析一下上面的代码:

  • class WP_REST_Book_Controller extends WP_REST_Controller 定义了一个名为 WP_REST_Book_Controller 的类,它继承了 WP_REST_Controller

  • $base = 'books'; 定义了 API 端点的基本路径。 最终API的路径会是 /wp-json/myplugin/v1/books

  • register_routes() 这个方法是核心,它负责注册 API 端点。

    • register_rest_route( $namespace, '/' . $this->base, ... ): 注册一个路由,第一个参数是命名空间,第二个参数是路径,第三个参数是一个数组,包含了不同请求方法的处理函数。
    • WP_REST_Server::READABLEWP_REST_Server::CREATABLEWP_REST_Server::EDITABLEWP_REST_Server::DELETABLE: 分别代表 GETPOSTPUTDELETE 请求方法。
    • callback: 指定处理该请求的函数。
    • permission_callback: 指定权限验证函数。
    • args: 指定请求参数的验证规则。
  • get_items_permissions_check()get_item_permissions_check()create_item_permissions_check()update_item_permissions_check()delete_item_permissions_check() 这些方法负责权限验证。 它们接受一个 $request 对象作为参数,其中包含了请求的所有信息。 如果用户有权限,则返回 true,否则返回一个 WP_Error 对象。

  • get_items()get_item()create_item()update_item()delete_item() 这些方法是实际的处理函数。

    • get_items(): 获取所有书籍。
    • get_item(): 获取单本书籍。
    • create_item(): 创建书籍。
    • update_item(): 更新书籍。
    • delete_item(): 删除书籍。
    • 这些方法都接受一个 $request 对象作为参数,并返回一个 WP_REST_Response 对象或一个 WP_Error 对象。
  • prepare_item_for_response() 这个方法负责将数据格式化为标准的 JSON 格式。 它接受一个 $item 对象和一个 $request 对象作为参数,并返回一个数组。

  • get_item_schema() 这个方法定义了 API 端点返回的数据的结构。 它返回一个数组,包含了每个字段的描述、类型和上下文。

5. 测试 API 端点

现在,你可以使用任何 REST API 客户端(例如 Postman)来测试你的 API 端点。

  • 获取所有书籍: GET /wp-json/myplugin/v1/books
  • 获取单本书籍: GET /wp-json/myplugin/v1/books/{id} (将 {id} 替换为实际的书籍 ID)
  • 创建书籍: POST /wp-json/myplugin/v1/books (需要在请求体中传递 titlecontent 参数)
  • 更新书籍: PUT /wp-json/myplugin/v1/books/{id} (将 {id} 替换为实际的书籍 ID,并在请求体中传递要更新的 titlecontent 参数)
  • 删除书籍: DELETE /wp-json/myplugin/v1/books/{id} (将 {id} 替换为实际的书籍 ID,可以传递 force=true 参数来强制删除)

四、 重点方法解析

咱们来重点看看几个关键方法:

1. register_routes()

这个方法是灵魂,它定义了你的 API 端点的 URL 结构和对应的处理函数。

方法 描述
GET 从服务器获取数据。例如,获取所有书籍或单本书籍。
POST 向服务器提交数据,通常用于创建新的资源。例如,创建一个新的书籍。
PUT 更新服务器上的资源。例如,更新一本书籍的标题或内容。
DELETE 从服务器删除资源。例如,删除一本书籍。
callback 指定处理该请求的函数。
permission_callback 指定权限验证函数。
args 定义请求参数的结构,包括参数的类型、描述、是否必填、验证规则等。 WordPress REST API 会自动根据这里的定义来验证请求参数,并返回错误信息。

2. 权限验证方法

例如 get_items_permissions_check(),这些方法至关重要,它们决定了谁可以访问你的 API 端点。

  public function get_items_permissions_check( $request ) {
    // 只有管理员才能查看所有书籍
    if ( ! current_user_can( 'manage_options' ) ) {
      return new WP_Error( 'rest_forbidden', __( 'You cannot view the resource.' ), array( 'status' => $this->authorization_status_code() ) );
    }
    return true;
  }

current_user_can() 是 WordPress 内置的权限检查函数,可以根据用户角色或权限来判断用户是否可以执行某个操作。

3. 数据处理方法

例如 get_items()create_item()update_item(),这些方法负责实际的数据操作。

  public function get_items( $request ) {
    $posts = get_posts( array(
      'post_type' => 'book',
      'posts_per_page' => -1, // 获取所有书籍
    ) );

    $data = array();

    foreach ( $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 );
  }
  • get_posts(): WordPress 内置的函数,用于获取文章。
  • prepare_item_for_response(): 将文章数据格式化为标准的 JSON 格式。
  • rest_ensure_response(): 确保返回的是一个 WP_REST_Response 对象。

4. prepare_item_for_response()

这个方法是数据格式化的关键。它将 WordPress 的数据结构(例如 WP_Post 对象)转换为 API 响应所需的 JSON 格式。

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

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

    $response->add_link( 'self', rest_url( sprintf( '%s/%s/%d', 'myplugin/v1', $this->base, $item->ID ) ) );

    return $response;
  }

5. get_item_schema()

这个方法定义了 API 端点返回的数据的结构。

  public function get_item_schema() {
    $schema = array(
      '$schema'              => 'http://json-schema.org/draft-04/schema#',
      'title'                => 'book',
      'type'                 => 'object',
      'properties'           => array(
        'id' => array(
          'description'  => __( 'Unique identifier for the object.' ),
          'type'         => 'integer',
          'context'      => array( 'view', 'edit', 'embed' ),
          'readonly'     => true,
        ),
        'title' => array(
          'description'  => __( 'The title for the object.' ),
          'type'         => 'string',
          'context'      => array( 'view', 'edit', 'embed' ),
          'arg_options'  => array(
            'sanitize_callback' => 'sanitize_text_field',
          ),
          'required'     => true,
        ),
        'content' => array(
          'description'  => __( 'The content for the object.' ),
          'type'         => 'string',
          'context'      => array( 'view', 'edit' ),
          'arg_options'  => array(
            'sanitize_callback' => 'wp_kses_post',
          ),
        ),
      ),
    );

    return $schema;
  }

get_item_schema的作用:

  • 文档化: 提供 API 的文档,让开发者知道 API 端点返回的数据是什么样的。
  • 数据验证: 用于验证客户端提交的数据是否符合规范。
  • 自动生成 API 文档: 一些工具可以根据 schema 自动生成 API 文档。

五、 总结

WP_REST_Controller 提供了一个强大的框架,可以帮助你规范化地创建 WordPress REST API 端点。 记住,关键在于:

  1. 继承 WP_REST_Controller
  2. 定义 $base
  3. 实现 register_routes()
  4. 实现权限验证方法
  5. 实现数据处理方法
  6. 实现 prepare_item_for_response()
  7. 实现 get_item_schema()

当然,这只是一个简单的例子。 实际应用中,你可能需要处理更复杂的数据结构、验证更复杂的参数,并实现更高级的权限控制。

希望今天的讲座能帮助你更好地理解 WP_REST_Controller,并开始构建你自己的 WordPress REST API 大厦! 祝大家编程愉快!

发表回复

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