控制器类

最后更新于:2021-11-29 13:40:01

Controller Classes

概述

要注册新的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文件中。然而,这些前缀不必要的冗长,有几个更好的选项可以以更易于维护的方式分组和封装端点的逻辑。

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


namespace MyPluginAPIMyEndpoint;

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

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

虽然这些较短的函数名称更易于使用,但它们与声明全局函数相比并不提供任何其他好处。因此,GeChiUI中的核心REST API端点都是使用控制器类实现的。

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

控制器

控制器接收输入(对于GeChiUI REST API,则为GC_REST_Request对象),并以GC_REST_Response对象生成响应输出。让我们看看一个示例控制器类:


class My_REST_Posts_Controller {

    // Here initialize our namespace and resource name.
    public function __construct() {
        $this->namespace     = '/my-namespace/v1';
        $this->resource_name = 'posts';
    }

    // Register our routes.
    public function register_routes() {
        register_rest_route( $this->namespace, '/' . $this->resource_name, array(
            // Here we register the readable endpoint for collections.
            array(
                'methods'   => 'GET',
                'callback'  => array( $this, 'get_items' ),
                'permission_callback' => array( $this, 'get_items_permissions_check' ),
            ),
            // Register our schema callback.
            'schema' => array( $this, 'get_item_schema' ),
        ) );
        register_rest_route( $this->namespace, '/' . $this->resource_name . '/(?P<id>[d]+)', array(
            // Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request.
            array(
                'methods'   => 'GET',
                'callback'  => array( $this, 'get_item' ),
                'permission_callback' => array( $this, 'get_item_permissions_check' ),
            ),
            // Register our schema callback.
            'schema' => array( $this, 'get_item_schema' ),
        ) );
    }

    /**
     * Check permissions for the posts.
     *
     * @param GC_REST_Request $request Current request.
     */
    public function get_items_permissions_check( $request ) {
        if ( ! current_user_can( 'read' ) ) {
            return new GC_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

    /**
     * Grabs the five most recent posts and outputs them as a rest response.
     *
     * @param GC_REST_Request $request Current 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 all of our comment response data.
        return rest_ensure_response( $data );
    }

    /**
     * Check permissions for the posts.
     *
     * @param GC_REST_Request $request Current request.
     */
    public function get_item_permissions_check( $request ) {
        if ( ! current_user_can( 'read' ) ) {
            return new GC_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) );
        }
        return true;
    }

    /**
     * Grabs the five most recent posts and outputs them as a rest response.
     *
     * @param GC_REST_Request $request Current 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 all of our post response data.
        return $response;
    }

    /**
     * Matches the post data to the schema we want.
     *
     * @param GC_Post $post The comment object whose response is being prepared.
     */
    public function prepare_item_for_response( $post, $request ) {
        $post_data = array();

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

        // We are also renaming the fields to more understandable names.
        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 );
    }

    /**
     * Prepare a response for inserting into a collection of responses.
     *
     * This is copied from GC_REST_Controller class in the GC REST API v2 plugin.
     *
     * @param GC_REST_Response $response Response object.
     * @return array Response data, ready for insertion into collection data.
     */
    public function prepare_response_for_collection( $response ) {
        if ( ! ( $response instanceof GC_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;
    }

    /**
     * Get our sample schema for a post.
     *
     * @return array The sample schema for a post
     */
    public function get_item_schema() {
        if ( $this->schema ) {
            // Since GeChiUI 5.3, the schema can be cached in the $schema property.
            return $this->schema;
        }

        $this->schema = array(
            // This tells the spec of JSON 结构 we are using which is draft 4.
            '$schema'              => 'http://json-schema.org/draft-04/schema#',
            // The title property marks the identity of the resource.
            'title'                => 'post',
            'type'                 => 'object',
            // In JSON 结构 you can specify object properties in the properties attribute.
            '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;
    }

    // Sets up the proper HTTP status code for authorization.
    public function authorization_status_code() {

        $status = 401;

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

        return $status;
    }
}

// Function to register our new routes from the controller.
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' );

展开完整源代码 折叠完整源代码

Benefits of Classes

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

get_item_schema方法中,请注意,我们将生成的结构存储在类上为$this->schema。类属性可以轻松缓存此类生成的值。GeChiUI 5.3中引入结构缓存将一些核心REST API集合响应的速度提高了高达40%,因此您绝对应该考虑在自己的控制器中遵循此结构。

类继承GC_REST_Controller

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

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

get_itemregister_routesupdate_item_permissions_check等特定于端点的方法不完全由抽象类实现,必须在您自己的类中定义。

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

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

例如,如果您为文章端点编写了控制器类(如上例),并希望支持自定义文章类型,则不应像这样扩展My_REST_Posts_Controllerclass My_CPT_REST_Controller extends My_REST_Posts_Controller。相反,您应该为共享逻辑创建一个完全独立的基础控制器类,或者让My_REST_Posts_Controller处理所有可用的文章类型。端点逻辑受业务要求变化的影响,您不想每次更新基站控制器时都更改一些不相关的控制器。

在大多数情况下,您需要创建一个基本控制器类,作为interfaceabstract class,您的每个端点控制器都可以实现或扩展,或者直接扩展其中一个核心GeChiUI REST类。

内部GeChiUI REST API类

GeChiUI REST API遵循其内部类的深思熟虑的设计结构,这些类可以分为基础设施端点类。

端点类封装了对GeChiUI资源执行CRUD操作所需的功能逻辑。GeChiUI公开了许多REST API端点(如GC_REST_Posts_Controller),但如上所述,所有端点都从公共基控制器类扩展:

  • GC_REST_Controller:所有GeChiUI核心端点的基类。该类旨在表示操作GeChiUI资源的一致结构。当与实现GC_REST_Controller的端点交互时,HTTP客户端可以期望每个端点以一致的方式运行。

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

  • GC_REST_Server:GeChiUI REST API的主要控制器。路由在GeChiUI中注册到服务器。当调用GC_REST_Server来服务请求时,它决定调用哪个路由,并将路由回调传递给GC_REST_Request对象。GC_REST_Server还处理身份验证,并可以执行请求验证和权限检查。
  • GC_REST_Request:表示请求性质的对象。此对象包括请求头、参数和方法等请求详细信息以及路由。它还可以执行请求验证和清理。
  • GC_REST_Response:代表响应性质的对象。此类扩展了GC_HTTP_Response,其中包括标头、正文和状态,并提供帮助方法,如用于添加链接媒体文件的add_link()和用于获取查询导航标头的query_navigation_headers()

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