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

各位观众,欢迎来到今天的WordPress REST API开发讲座!我是你们的老朋友,代码界的段子手,今天咱们来聊聊如何优雅地继承WP_REST_Controller,打造你自己的REST API端点,让你的WordPress项目瞬间高大上起来。

一、WP_REST_Controller:REST API的基石

首先,咱们得认识一下这位大佬:WP_REST_Controller。它就像是盖楼的地基,为你提供了一套标准的API结构,让你专注于业务逻辑,而不是重复造轮子。

WP_REST_Controller定义了一些核心方法,帮助你处理:

  • 路由注册: 将你的API端点与特定的URL关联起来。
  • 权限验证: 确保只有授权用户才能访问你的API。
  • 参数验证和清理: 保证输入数据的有效性和安全性。
  • 响应格式化: 以统一的JSON格式返回数据。

二、继承WP_REST_Controller:你的专属API蓝图

继承WP_REST_Controller,就像拿到了一张API蓝图,你只需要按照蓝图上的指示,填充自己的业务逻辑,就能快速搭建出一个功能完善的API端点。

下面,咱们用一个简单的例子来说明:创建一个管理书籍的API。

1. 创建你的控制器类

<?php
/**
 * 书籍控制器类
 */
class Book_REST_Controller extends WP_REST_Controller {

    /**
     * 构造函数
     */
    public function __construct() {
        $this->namespace = 'my-plugin/v1'; // API命名空间,避免冲突
        $this->rest_base = 'books'; // API端点基础路径,例如 /wp-json/my-plugin/v1/books
    }

    /**
     * 注册路由
     */
    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' ), // 回调函数,处理GET请求
                'permission_callback' => array( $this, 'get_items_permissions_check' ), // 权限验证回调函数
            ),
            array(
                'methods'             => WP_REST_Server::CREATABLE, // POST 方法
                'callback'            => array( $this, 'create_item' ), // 回调函数,处理POST请求
                '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, // GET 方法,根据ID获取
                'callback'            => array( $this, 'get_item' ), // 回调函数,处理GET请求
                'permission_callback' => array( $this, 'get_item_permissions_check' ), // 权限验证回调函数
                'args'                => array(
                    'context' => array(
                        'default' => 'view',
                    ),
                ),
            ),
            array(
                'methods'             => WP_REST_Server::EDITABLE, // PUT/PATCH 方法,更新
                'callback'            => array( $this, 'update_item' ), // 回调函数,处理PUT/PATCH请求
                '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' ), // 回调函数,处理DELETE请求
                'permission_callback' => array( $this, 'delete_item_permissions_check' ), // 权限验证回调函数
                'args'                => array(
                    'force' => array(
                        'default' => false,
                        'type'    => 'boolean',
                        'description' => __( '是否强制删除,跳过回收站。', 'my-plugin' ),
                    ),
                ),
            ),
        ) );
    }

    /**
     * 获取书籍列表
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_items( $request ) {
        // 查询数据库,获取书籍列表
        $books = $this->get_books();

        // 格式化数据
        $data = array();
        foreach ( $books as $book ) {
            $item = $this->prepare_item_for_response( $book, $request );
            $data[] = $this->prepare_response_for_collection( $item ); // 确保返回结果是数组
        }

        // 返回响应
        return rest_ensure_response( $data );
    }

    /**
     * 获取单个书籍
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function get_item( $request ) {
        $id = (int) $request['id'];
        $book = $this->get_book( $id );

        if ( empty( $book ) ) {
            return new WP_Error( 'rest_book_invalid_id', __( '无效的书籍ID。', 'my-plugin' ), array( 'status' => 404 ) );
        }

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

        return rest_ensure_response( $data );
    }

    /**
     * 创建书籍
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function create_item( $request ) {
        $book_data = $this->prepare_item_for_database( $request );

        // 保存到数据库
        $book_id = $this->save_book( $book_data );

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

        $book = $this->get_book( $book_id );

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

        return new WP_REST_Response( $data, 201 ); // 201 Created
    }

    /**
     * 更新书籍
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function update_item( $request ) {
        $id = (int) $request['id'];
        $book = $this->get_book( $id );

        if ( empty( $book ) ) {
            return new WP_Error( 'rest_book_invalid_id', __( '无效的书籍ID。', 'my-plugin' ), array( 'status' => 404 ) );
        }

        $book_data = $this->prepare_item_for_database( $request );
        $book_data['id'] = $id; // 确保ID存在

        // 更新数据库
        $updated = $this->update_book( $book_data );

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

        $book = $this->get_book( $id );

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

        return rest_ensure_response( $data );
    }

    /**
     * 删除书籍
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return WP_REST_Response|WP_Error
     */
    public function delete_item( $request ) {
        $id = (int) $request['id'];
        $book = $this->get_book( $id );

        if ( empty( $book ) ) {
            return new WP_Error( 'rest_book_invalid_id', __( '无效的书籍ID。', 'my-plugin' ), array( 'status' => 404 ) );
        }

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

        // 删除数据库记录
        $deleted = $this->delete_book( $id, $force );

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

        return new WP_REST_Response( array( 'deleted' => true ), 200 );
    }

    /**
     * 检查是否有读取书籍列表的权限
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return true|WP_Error
     */
    public function get_items_permissions_check( $request ) {
        // 只有登录用户才能读取
        if ( ! is_user_logged_in() ) {
            return new WP_Error( 'rest_forbidden', __( '您没有权限查看书籍列表。', 'my-plugin' ), array( 'status' => 401 ) );
        }

        return true;
    }

    /**
     * 检查是否有读取单个书籍的权限
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return true|WP_Error
     */
    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 true|WP_Error
     */
    public function create_item_permissions_check( $request ) {
        // 只有管理员才能创建
        if ( ! current_user_can( 'manage_options' ) ) {
            return new WP_Error( 'rest_forbidden', __( '您没有权限创建书籍。', 'my-plugin' ), array( 'status' => 403 ) );
        }

        return true;
    }

    /**
     * 检查是否有更新书籍的权限
     *
     * @param WP_REST_Request $request Full data about the request.
     * @return true|WP_Error
     */
    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 true|WP_Error
     */
    public function delete_item_permissions_check( $request ) {
        return $this->create_item_permissions_check( $request ); // 沿用创建的权限
    }

    /**
     * 准备用于响应的数据
     *
     * @param mixed $item WordPress representation of the item.
     * @param WP_REST_Request $request Request object.
     * @return WP_REST_Response Enriched data.
     */
    public function prepare_item_for_response( $item, $request ) {
        $data = array(
            'id'          => (int) $item->id,
            'title'       => $item->title,
            'author'      => $item->author,
            'publication_date' => $item->publication_date,
        );

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

        // 增加链接信息,例如编辑链接
        $links = array(
            'self' => array(
                'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $item->id ) ),
            ),
            'collection' => array(
                'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
            ),
        );

        $response = rest_ensure_response( $data );

        $response->add_links( $links );

        return $response;
    }

    /**
     * 获取项目模式,用于描述API的数据结构
     *
     * @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' ),
                    'arg_options'  => array(
                        'sanitize_callback' => 'sanitize_text_field',
                    ),
                    'required'     => true,
                ),
                'author' => array(
                    'description'  => __( '书籍作者', 'my-plugin' ),
                    'type'         => 'string',
                    'context'      => array( 'view', 'edit' ),
                    'arg_options'  => array(
                        'sanitize_callback' => 'sanitize_text_field',
                    ),
                ),
                'publication_date' => array(
                    'description'  => __( '出版日期', 'my-plugin' ),
                    'type'         => 'string', // 使用字符串,或者如果需要可以使用'date'类型
                    'context'      => array( 'view', 'edit' ),
                    'arg_options'  => array(
                        'validate_callback' => function( $param, $request, $key ) {
                            return strtotime( $param ); // 简单验证是否是有效日期
                        },
                    ),
                ),
            ),
        );

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

    /**
     * 准备用于数据库存储的数据
     *
     * @param WP_REST_Request $request Request object.
     * @return array|WP_Error Properly prepared data.
     */
    public function prepare_item_for_database( $request ) {
        $prepared_data = array();

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

        if ( isset( $request['author'] ) ) {
            $prepared_data['author'] = sanitize_text_field( $request['author'] );
        }

        if ( isset( $request['publication_date'] ) ) {
            $prepared_data['publication_date'] = sanitize_text_field( $request['publication_date'] );
        }

        return $prepared_data;
    }

    /**
     * 获取书籍列表(示例,需要替换为你的实际数据获取逻辑)
     *
     * @return array
     */
    private function get_books() {
        global $wpdb;
        $table_name = $wpdb->prefix . 'books'; // 你的书籍表名

        $sql = "SELECT * FROM {$table_name}";
        $results = $wpdb->get_results( $sql );

        return $results;
    }

    /**
     * 获取单个书籍(示例,需要替换为你的实际数据获取逻辑)
     *
     * @param int $id 书籍ID
     * @return object|null
     */
    private function get_book( $id ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'books'; // 你的书籍表名

        $sql = $wpdb->prepare( "SELECT * FROM {$table_name} WHERE id = %d", $id );
        $result = $wpdb->get_row( $sql );

        return $result;
    }

    /**
     * 保存书籍到数据库(示例,需要替换为你的实际数据保存逻辑)
     *
     * @param array $data 书籍数据
     * @return int|WP_Error 书籍ID或错误对象
     */
    private function save_book( $data ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'books'; // 你的书籍表名

        $result = $wpdb->insert(
            $table_name,
            $data,
            array(
                '%s', // title
                '%s', // author
                '%s', // publication_date
            )
        );

        if ( false === $result ) {
            return new WP_Error( 'db_insert_error', __( '数据库插入错误。', 'my-plugin' ), array( 'status' => 500 ) );
        }

        return $wpdb->insert_id;
    }

    /**
     * 更新书籍到数据库(示例,需要替换为你的实际数据更新逻辑)
     *
     * @param array $data 书籍数据
     * @return int|WP_Error 更新结果或错误对象
     */
     private function update_book( $data ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'books'; // 你的书籍表名
        $id = $data['id'];
        unset($data['id']);

        $result = $wpdb->update(
            $table_name,
            $data,
            array('id' => $id),
            array(
                '%s', // title
                '%s', // author
                '%s', // publication_date
            ),
            array('%d')
        );

        if ( false === $result ) {
            return new WP_Error( 'db_update_error', __( '数据库更新错误。', 'my-plugin' ), array( 'status' => 500 ) );
        }

        return true; // 返回 true 表示更新成功
    }

    /**
     * 删除书籍从数据库(示例,需要替换为你的实际数据删除逻辑)
     *
     * @param int $id 书籍ID
     * @param bool $force 是否强制删除
     * @return bool|WP_Error 删除结果或错误对象
     */
    private function delete_book( $id, $force = false ) {
        global $wpdb;
        $table_name = $wpdb->prefix . 'books'; // 你的书籍表名

        $result = $wpdb->delete(
            $table_name,
            array( 'id' => $id ),
            array( '%d' )
        );

        if ( false === $result ) {
            return new WP_Error( 'db_delete_error', __( '数据库删除错误。', 'my-plugin' ), array( 'status' => 500 ) );
        }

        return true; // 返回 true 表示删除成功
    }

}

2. 注册控制器

在你的插件主文件中,或者在init钩子中,注册你的控制器:

<?php
add_action( 'rest_api_init', 'register_book_rest_controller' );
function register_book_rest_controller() {
    $controller = new Book_REST_Controller();
    $controller->register_routes();
}

3. 方法详解

  • __construct() 构造函数,设置API的命名空间和基础路径。 命名空间就像你的API的身份证,确保它在WordPress的API世界里独一无二。 基础路径定义了你的API端点的入口,例如/wp-json/my-plugin/v1/books

  • register_routes() 注册API路由。 这里定义了你的API端点,以及它们支持的HTTP方法(GET、POST、PUT、DELETE等),以及对应的回调函数。 register_rest_route()函数是关键,它将你的API端点与WordPress的REST API系统关联起来。

    • 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:定义API端点接受的参数,并进行验证和清理。
  • get_items() 获取书籍列表。 这个方法负责查询数据库,获取书籍数据,并将数据格式化为REST API可以返回的JSON格式。 记得使用rest_ensure_response()函数将数据包装成WP_REST_Response对象。

  • get_item() 获取单个书籍。 根据ID查询数据库,获取单个书籍的信息。

  • create_item() 创建书籍。 接收POST请求的数据,验证数据,然后将数据保存到数据库。 返回新创建的书籍的信息,并设置HTTP状态码为201(Created)。

  • update_item() 更新书籍。 接收PUT/PATCH请求的数据,根据ID更新数据库中的书籍信息。

  • delete_item() 删除书籍。 根据ID删除数据库中的书籍信息。 可以选择是否强制删除(跳过回收站)。

  • get_items_permissions_check()get_item_permissions_check()create_item_permissions_check()update_item_permissions_check()delete_item_permissions_check() 权限验证。 这些方法负责验证当前用户是否有权限访问API端点。 你可以根据用户的角色、权限等进行验证。 如果用户没有权限,返回WP_Error对象,并设置相应的HTTP状态码(例如401 Unauthorized、403 Forbidden)。

  • prepare_item_for_response() 格式化响应数据。 将从数据库获取的数据格式化为REST API可以返回的JSON格式。 你可以根据需要添加额外的字段,例如链接信息。

  • get_item_schema() 定义API的数据结构。 这个方法返回一个JSON Schema,描述了API端点返回的数据结构。 它可以用于生成API文档,以及客户端的数据验证。

  • prepare_item_for_database() 准备用于数据库存储的数据。 对接收到的数据进行清理和验证,确保数据安全和有效。

4. 实战演练:代码拆解分析

咱们来深入分析一下几个关键方法:

  • register_routes()

    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' ),
            ),
            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():这个函数是核心,它将你的API端点注册到WordPress REST API系统中。
    • $this->namespace:API命名空间,例如my-plugin/v1
    • '/' . $this->rest_base:API端点路径,例如/books
    • methods:HTTP方法,例如WP_REST_Server::READABLE(GET)、WP_REST_Server::CREATABLE(POST)。
    • callback:处理请求的回调函数,例如array( $this, 'get_items' )
    • permission_callback:权限验证回调函数,例如array( $this, 'get_items_permissions_check' )
    • args:API参数,使用$this->get_endpoint_args_for_item_schema()从schema中获取。
  • get_items()

    public function get_items( $request ) {
        $books = $this->get_books(); // 获取书籍数据
    
        $data = array();
        foreach ( $books as $book ) {
            $item = $this->prepare_item_for_response( $book, $request ); // 格式化数据
            $data[] = $this->prepare_response_for_collection( $item ); // 确保返回结果是数组
        }
    
        return rest_ensure_response( $data ); // 返回响应
    }
    • $this->get_books():这个方法负责从数据库或其他数据源获取书籍数据。 你需要根据自己的实际情况来实现这个方法。
    • $this->prepare_item_for_response():这个方法负责将书籍数据格式化为REST API可以返回的JSON格式。
    • rest_ensure_response():这个函数将数据包装成WP_REST_Response对象,这是REST API的标准响应格式。
    • $this->prepare_response_for_collection($item): 确保返回的数据可以被正确处理为数组类型,这在返回集合数据时非常重要
  • prepare_item_for_response()

    public function prepare_item_for_response( $item, $request ) {
        $data = array(
            'id'          => (int) $item->id,
            'title'       => $item->title,
            'author'      => $item->author,
            'publication_date' => $item->publication_date,
        );
    
        $context = ! empty( $request['context'] ) ? $request['context'] : 'view';
        $data = $this->filter_response_by_context( $data, $context );
    
        // 增加链接信息
        $links = array(
            'self' => array(
                'href' => rest_url( sprintf( '%s/%s/%d', $this->namespace, $this->rest_base, $item->id ) ),
            ),
            'collection' => array(
                'href' => rest_url( sprintf( '%s/%s', $this->namespace, $this->rest_base ) ),
            ),
        );
    
        $response = rest_ensure_response( $data );
        $response->add_links( $links );
    
        return $response;
    }
    • 这个方法负责将书籍数据格式化为REST API可以返回的JSON格式。
    • 你可以根据需要添加额外的字段,例如链接信息。
    • $request['context']:这个参数可以用于控制返回的数据的详细程度。 例如,context=view可以返回简要信息,context=edit可以返回完整信息。
    • $this->filter_response_by_context():这个方法可以根据context参数过滤返回的数据。
  • get_item_schema()

    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' ),
                    'arg_options'  => array(
                        'sanitize_callback' => 'sanitize_text_field',
                    ),
                    'required'     => true,
                ),
                // ... 其他属性
            ),
        );
    
        return $this->add_additional_fields_schema( $schema );
    }
    • 这个方法定义了API的数据结构。
    • $schema['properties']:定义了API端点返回的每个字段的类型、描述、context等信息。
    • arg_options:定义了API端点接受的参数的验证和清理规则。 例如,sanitize_callback用于清理数据,validate_callback用于验证数据。

三、高级技巧:让你的API更上一层楼

  • 使用缓存: 对于经常访问的数据,可以使用WordPress的缓存API,减少数据库查询次数,提高API的性能。
  • 分页: 对于大量数据,可以使用分页功能,避免一次性返回所有数据,提高API的响应速度。
  • 版本控制: 当你的API需要进行重大变更时,可以使用版本控制,保持向后兼容性,避免影响现有客户端。
  • 自定义路由: 除了标准的CRUD操作,你还可以定义自定义路由,实现更复杂的功能。

四、总结:你的API之旅才刚刚开始

通过继承WP_REST_Controller,你可以快速构建出功能完善、安全可靠的REST API端点。 记住,代码只是工具,真正的关键在于理解REST API的设计原则,以及你的业务需求。 希望今天的讲座能帮助你开启你的API之旅,让你的WordPress项目更加强大!

记住,代码就像段子,需要不断练习,才能达到炉火纯青的地步。 祝各位编程愉快!

发表回复

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