添加自定义端点
最后更新于:2021-11-29 12:56:37
Adding Custom Endpoints
GeChiUI REST API不仅仅是一组默认路由。它也是创建自定义路由和端点的工具。GeChiUI前端提供了一组默认的URL映射,但用于创建它们的工具(例如重写API以及查询类:GC_Query
、GC_User
等)也可用于创建您自己的URL映射或自定义查询。
本文详细介绍了如何使用自己的端点创建完全自定义的路由。我们将首先完成一个简短的例子,然后将其扩展到内部使用的全控制器结构。
基础
所以你想向API添加自定义端点?太棒了!让我们从一个简单的例子开始。
让我们从一个简单的函数开始,如下所示:
<?php
/**
* Grab latest post title by an author!
*
* @param array $data Options for the function.
* @return string|null Post title for the latest, * or null if none.
*/
function my_awesome_func( $data ) {
$posts = get_posts( array(
'author' => $data['id'],
) );
if ( empty( $posts ) ) {
return null;
}
return $posts[0]->post_title;
}
要通过API提供此信息,我们需要注册一条路线。这告诉API使用我们的函数响应给定的请求。我们通过一个名为register_rest_route
的函数来做到这一点,该函数应在rest_api_init
上的回调中调用,以避免在未加载API时做额外工作。
我们需要传递三件事来register_rest_route
:命名空间、我们想要的路线和选项。我们稍后会回到命名空间,但现在,让我们选择myplugin/v1
。我们将让路由与/author/{id}
匹配任何内容,其中{id}
是一个整数。
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/author/(?P<id>d+)', array(
'methods' => 'GET',
'callback' => 'my_awesome_func',
) );
} );
目前,我们只注册了路线的一个端点。“路由”一词是指URL,而“端点”是指其背后的与方法和URL对应的函数(有关更多信息,请参阅术语表)。
For example, if your site domain is example.com
and you’ve kept the API path of gc-json
, then the full URL would be http://example.com/gc-json/myplugin/v1/author/(?Pd+)
.
每个路由可以有任意数量的端点,对于每个端点,您可以定义允许的HTTP方法、用于响应请求的回调函数和用于创建自定义权限的权限回调。此外,您可以在请求中定义允许的字段,并为每个字段指定默认值、清理回调、验证回调以及是否需要该字段。
命名空间
是端点URL的第一部分。它们应该用作供应商/包装前缀,以防止自定义路线之间的冲突。命名空间允许两个插件添加具有不同功能的同名路由。
命名空间通常应遵循vendor/v1
的结构,其中vendor
通常是您的插件或主题弹头,v1
代表API的第一个版本。如果您需要打破与新端点的兼容性,那么您可以将其提升到v2
。
上述场景是,来自两个不同插件的同名路由,要求所有供应商使用唯一的命名空间。未能这样做类似于未能在主题或插件中使用供应商函数前缀、类前缀和/或类命名空间,这非常糟糕。
使用命名空间的另一个好处是客户端可以检测对自定义API的支持。API索引列出了网站上可用的命名空间:
{
"name": "GeChiUI Site",
"description": "Just another GeChiUI site",
"url": "http://example.com/",
"namespaces": [
"gc/v2",
"vendor/v1",
"myplugin/v1",
"myplugin/v2",
]
}
如果客户想检查您的API是否存在于网站上,他们可以对照此列表进行检查。(有关更多信息,请参阅探索指南。)
参数
默认情况下,路由接收从请求传入的所有参数。这些被合并到一组参数中,然后添加到Request对象中,Request对象作为第一个参数传递到您的端点:
<?php
function my_awesome_func( GC_REST_Request $request ) {
// You can access parameters via direct array access on the object:
$param = $request['some_param'];
// Or via the helper method:
$param = $request->get_param( 'some_param' );
// You can get the combined, merged set of parameters:
$parameters = $request->get_params();
// The individual sets of parameters are also available, if needed:
$parameters = $request->get_url_params();
$parameters = $request->get_query_params();
$parameters = $request->get_body_params();
$parameters = $request->get_json_params();
$parameters = $request->get_default_params();
// Uploads aren't merged in, but can be accessed separately:
$parameters = $request->get_file_params();
}
(要确切了解参数的合并方式,请检查GC_REST_Request::get_parameter_order()
基本顺序是正文、查询、URL,然后是默认顺序。)
通常,您将获得每个参数,不会被更改。但是,您可以在注册路由时注册参数,这允许您对这些参数运行清理和验证。
如果请求在正文中有Content-type: application/json
标头集和有效的JSON,get_json_params()
将作为关联数组返回解析后的JSON主体。
参数被定义为每个端点(callback
选项旁边)键args
中的映射。此映射使用密钥参数的名称,该值是该参数的选项映射。此数组可以包含default
、required
的sanitize_callback
和validate_callback
密钥。
default
:如果没有提供,则用作参数的默认值。required
:如果定义为true,并且该参数没有传递值,则将返回错误。如果设置了默认值,则没有效果,因为参数将始终有一个值。validate_callback
:用于传递将传递参数值的函数。如果值有效,该函数应返回true;如果没有,则返回false。sanitize_callback
:用于传递一个函数,该函数用于在将参数传递给主回调之前对其进行清理参数的值。
使用sanitize_callback
和validate_callback
只允许主回调处理请求,并准备使用GC_REST_Response
类返回的数据。通过使用这两个回调,您将能够安全地假设您的输入在处理时是有效和安全的。
以我们之前的例子为例,我们可以确保传入的参数始终是一个数字:
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/author/(?P<id>d+)', array(
'methods' => 'GET',
'callback' => 'my_awesome_func',
'args' => array(
'id' => array(
'validate_callback' => function($param, $request, $key) {
return is_numeric( $param );
}
),
),
) );
} );
您还可以传递函数名来validate_callback
,但直接传递某些函数,如is_numeric
,不仅会发出关于向其传递额外参数的警告,还会返回NULL
,导致使用无效数据调用回调函数。我们希望最终在GeChiUI核心中解决这个问题。
我们也可以改用'sanitize_callback' => 'absint'
之类的东西,但验证会抛出错误,让客户了解他们做错了什么。当您宁愿更改输入的数据而不是抛出错误(例如无效的HTML)时,卫生非常有用。
返回值
调用回调后,返回值将转换为JSON,并返回给客户端。这允许您基本上返回任何形式的数据。在上面的示例中,我们将返回字符串或空字符串,这些字符串或空值由API自动处理并转换为JSON。
与任何其他GeChiUI函数一样,您还可以返回GC_Error
实例。此错误信息将与500个内部服务错误状态代码一起传递给客户端。您可以通过将GC_Error
实例数据中的status
选项设置为代码来进一步自定义错误,例如错误输入数据的400
。
以我们之前的例子为例,我们现在可以返回错误实例:
<?php
/**
* Grab latest post title by an author!
*
* @param array $data Options for the function.
* @return string|null Post title for the latest,
* or null if none.
*/
function my_awesome_func( $data ) {
$posts = get_posts( array(
'author' => $data['id'],
) );
if ( empty( $posts ) ) {
return new GC_Error( 'no_author', 'Invalid author', array( 'status' => 404 ) );
}
return $posts[0]->post_title;
}
当作者没有任何属于他们的文章时,这将向客户返回404未找到错误:
HTTP/1.1 404 Not Found
[{
"code": "no_author",
"message": "Invalid author",
"data": { "status": 404 }
}]
为了获得更高级的使用,您可以返回GC_REST_Response
对象。此对象“包装”正常主体数据,但允许您返回自定义状态代码或自定义标头。您还可以添加回复的链接。使用这个的最快方法是通过构造函数:
<?php
$data = array( 'some', 'response', 'data' );
// Create the response object
$response = new GC_REST_Response( $data );
// Add a custom status code
$response->set_status( 201 );
// Add a custom header
$response->header( 'Location', 'http://example.com/' );
包装现有回调时,您应始终在返回值上使用rest_ensure_response()
。这将获取从端点返回的原始数据,并自动将其转换为GC_REST_Response
。(请注意,GC_Error
不会转换为GC_REST_Response
,以允许正确处理错误。)
Importantly, a REST API route’s callback should always return data; it shouldn’t attempt to send the response body itself. This ensures that the additional processing that the REST API server does, like handling linking/embedding, sending headers, etc… takes place. In other words, don’t call die( gc_json_encode( $data ) );
or gc_send_json( $data )
. As of GeChiUI 5.5, a _doing_it_wrong
notice is issued if the gc_send_json()
family of functions is used during a REST API request.
使用REST API时,从回调返回GC_REST_Response或GC_Error对象。
回调权限
您还必须注册端点的权限回调。这是一个函数,在调用真实回调之前,检查用户是否可以执行操作(读取、更新等)。这允许API告诉客户端他们可以在给定的URL上执行什么操作,而无需先尝试请求。
此回调可以注册为 permission_callback
,也可以在callback
选项旁边的端点选项中注册。此回调应返回布尔值或GC_Error
实例。如果此函数返回true,则将处理响应。如果返回false,将返回默认错误消息,请求将不再继续处理。如果它返回GC_Error
,该错误将返回给客户端。
权限回调在远程身份验证后运行,远程身份验证设置了当前用户。这意味着您可以使用current_user_can
检查已验证的用户是否具有适当的操作能力,或根据当前用户ID进行任何其他检查。在可能的情况下,您应该始终使用current_user_can
;与其检查用户是否登录(身份验证),不如检查他们是否可以执行操作(授权)。
Once you register a permission_callback
, you will need to authenticate your requests (for example by including a nonce parameter) or you will receive a rest_forbidden
error. See Authentication for more details.
继续我们之前的例子,我们可以做到只有编辑或以上的编辑才能查看此作者数据。我们在这里可以检查许多不同的功能,但最好的是 edit_others_posts
,这确实是编辑器的核心。要做到这一点,我们只需要在这里回调:
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/author/(?P<id>d+)', array(
'methods' => 'GET',
'callback' => 'my_awesome_func',
'args' => array(
'id' => array(
'validate_callback' => 'is_numeric'
),
),
'permission_callback' => function () {
return current_user_can( 'edit_others_posts' );
}
) );
} );
请注意,权限回调也会接收Request对象作为第一个参数,因此如果您需要,您可以根据请求参数进行检查。
从GeChiUI 5.5起,如果没有提供 permission_callback
,REST API将发出_doing_it_wrong
通知。
myplugin/v1/author的REST API路由定义缺少所需的 permission_callback 参数。对于打算公开的REST API路由,请使用__return_true作为权限回调。
如果您的REST API端点是公开的,您可以使用__return_true
作为权限回调。
<?php
add_action( 'rest_api_init', function () {
register_rest_route( 'myplugin/v1', '/author/(?P<id>d+)', array(
'methods' => 'GET',
'callback' => 'my_awesome_func',
'permission_callback' => '__return_true',
) );
} );
发现
如果您想为自定义端点启用资源发现,您可以使用rest_queried_resource_route
过滤器进行操作。例如,考虑一个包含自定义资源ID的自定义查询var my-route
。每当使用my-route
查询变量时,以下代码片段都会添加发现链接。
function my_plugin_rest_queried_resource_route( $route ) {
$id = get_query_var( 'my-route' );
if ( ! $route && $id ) {
$route = '/my-ns/v1/items/' . $id;
}
return $route;
}
add_filter( 'rest_queried_resource_route', 'my_plugin_rest_queried_resource_route' );
注意:如果您的端点正在描述自定义文章类型或自定义分类,您最想改用rest_route_for_post
或rest_route_for_term
过滤器。
控制器结构
控制器结构是使用API处理复杂端点的最佳做法。
建议您在阅读本节之前阅读“扩展内部课程”。这样做将使您熟悉默认路由使用的结构,这是最佳做法。虽然您用于处理请求的类不需要扩展GC_REST_Controller
类或扩展它的类,但这样做允许您继承在这些类中完成的工作。此外,您可以放心,您正在遵循基于您使用的控制器方法的最佳做法。
就其核心而言,控制器只不过是一套与REST约定匹配的常用方法,以及一些方便的助手。控制器在register_routes
方法中注册路由,使用get_items
、get_item
create_item
、update_item
和delete_item
响应请求,并具有类似名称的权限检查方法。遵循此结构将确保您不会错过端点中的任何步骤或功能。
要使用控制器,您首先需要子类基本控制器。这为您提供了一套基本的方法,可以让您将自己的行为添加到其中。
一旦我们对控制器进行子类化,我们需要实例化该类才能使其工作。这应该在钩住到rest_api_init
的回调中完成,这确保我们仅在需要时实例化类。正常的控制器结构是在此回调中调用$controller->register_routes()
,然后类可以注册其端点。
示例
以下是“启动器”自定义路线:
<?php
class Slug_Custom_Route extends GC_REST_Controller {
/**
* Register the routes for the objects of the controller.
*/
public function register_routes() {
$version = '1';
$namespace = 'vendor/v' . $version;
$base = 'route';
register_rest_route( $namespace, '/' . $base, array(
array(
'methods' => GC_REST_Server::READABLE,
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
'args' => array(
),
),
array(
'methods' => GC_REST_Server::CREATABLE,
'callback' => array( $this, 'create_item' ),
'permission_callback' => array( $this, 'create_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( true ),
),
) );
register_rest_route( $namespace, '/' . $base . '/(?P<id>[d]+)', array(
array(
'methods' => GC_REST_Server::READABLE,
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
'args' => array(
'context' => array(
'default' => 'view',
),
),
),
array(
'methods' => GC_REST_Server::EDITABLE,
'callback' => array( $this, 'update_item' ),
'permission_callback' => array( $this, 'update_item_permissions_check' ),
'args' => $this->get_endpoint_args_for_item_schema( false ),
),
array(
'methods' => GC_REST_Server::DELETABLE,
'callback' => array( $this, 'delete_item' ),
'permission_callback' => array( $this, 'delete_item_permissions_check' ),
'args' => array(
'force' => array(
'default' => false,
),
),
),
) );
register_rest_route( $namespace, '/' . $base . '/schema', array(
'methods' => GC_REST_Server::READABLE,
'callback' => array( $this, 'get_public_item_schema' ),
) );
}
/**
* Get a collection of items
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|GC_REST_Response
*/
public function get_items( $request ) {
$items = array(); //do a query, call another class, etc
$data = array();
foreach( $items as $item ) {
$itemdata = $this->prepare_item_for_response( $item, $request );
$data[] = $this->prepare_response_for_collection( $itemdata );
}
return new GC_REST_Response( $data, 200 );
}
/**
* Get one item from the collection
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|GC_REST_Response
*/
public function get_item( $request ) {
//get parameters from request
$params = $request->get_params();
$item = array();//do a query, call another class, etc
$data = $this->prepare_item_for_response( $item, $request );
//return a response or error based on some conditional
if ( 1 == 1 ) {
return new GC_REST_Response( $data, 200 );
} else {
return new GC_Error( 'code', __( 'message', 'text-domain' ) );
}
}
/**
* Create one item from the collection
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|GC_REST_Response
*/
public function create_item( $request ) {
$item = $this->prepare_item_for_database( $request );
if ( function_exists( 'slug_some_function_to_create_item' ) ) {
$data = slug_some_function_to_create_item( $item );
if ( is_array( $data ) ) {
return new GC_REST_Response( $data, 200 );
}
}
return new GC_Error( 'cant-create', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
}
/**
* Update one item from the collection
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|GC_REST_Response
*/
public function update_item( $request ) {
$item = $this->prepare_item_for_database( $request );
if ( function_exists( 'slug_some_function_to_update_item' ) ) {
$data = slug_some_function_to_update_item( $item );
if ( is_array( $data ) ) {
return new GC_REST_Response( $data, 200 );
}
}
return new GC_Error( 'cant-update', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
}
/**
* Delete one item from the collection
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|GC_REST_Response
*/
public function delete_item( $request ) {
$item = $this->prepare_item_for_database( $request );
if ( function_exists( 'slug_some_function_to_delete_item' ) ) {
$deleted = slug_some_function_to_delete_item( $item );
if ( $deleted ) {
return new GC_REST_Response( true, 200 );
}
}
return new GC_Error( 'cant-delete', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
}
/**
* Check if a given request has access to get items
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|bool
*/
public function get_items_permissions_check( $request ) {
//return true; <--use to make readable by all
return current_user_can( 'edit_something' );
}
/**
* Check if a given request has access to get a specific item
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|bool
*/
public function get_item_permissions_check( $request ) {
return $this->get_items_permissions_check( $request );
}
/**
* Check if a given request has access to create items
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|bool
*/
public function create_item_permissions_check( $request ) {
return current_user_can( 'edit_something' );
}
/**
* Check if a given request has access to update a specific item
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|bool
*/
public function update_item_permissions_check( $request ) {
return $this->create_item_permissions_check( $request );
}
/**
* Check if a given request has access to delete a specific item
*
* @param GC_REST_Request $request Full data about the request.
* @return GC_Error|bool
*/
public function delete_item_permissions_check( $request ) {
return $this->create_item_permissions_check( $request );
}
/**
* Prepare the item for create or update operation
*
* @param GC_REST_Request $request Request object
* @return GC_Error|object $prepared_item
*/
protected function prepare_item_for_database( $request ) {
return array();
}
/**
* Prepare the item for the REST response
*
* @param mixed $item GeChiUI representation of the item.
* @param GC_REST_Request $request Request object.
* @return mixed
*/
public function prepare_item_for_response( $item, $request ) {
return array();
}
/**
* Get the query params for collections
*
* @return array
*/
public function get_collection_params() {
return array(
'page' => array(
'description' => '集合的分页页码',
'type' => 'integer',
'default' => 1,
'sanitize_callback' => 'absint',
),
'per_page' => array(
'description' => '返回结果集的最大条数。',
'type' => 'integer',
'default' => 10,
'sanitize_callback' => 'absint',
),
'search' => array(
'description' => '字符串匹配的结果集。',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
),
);
}
}