概述
虽然 WordPress REST API 可以通过 PHP 在 WordPress 代码内部被调用,但 REST API 的设计目的是通过 HTTP 进行远程使用。HTTP 是互联网上数据通信的基础,任何能够发起 HTTP 请求的应用程序都可以使用 WordPress REST API,无论该应用程序是客户端 JavaScript 界面,还是运行在 Python 或 Java 中的远程服务器应用程序。
请求的数据结构由 WP_REST_Request 类方便地处理。
WP_REST_Request
此类是 WordPress 4.4 中引入的三个主要基础设施类之一。当向 API 的端点发起 HTTP 请求时,API 将自动创建一个 WP_REST_Request 类的实例,以匹配提供的数据。响应对象在 WP_REST_Server 的 serve_request() 方法中自动生成。一旦请求创建并检查了身份验证,请求就会被分发,我们的端点回调开始触发。WP_REST_Request 对象中存储的所有数据都会传递给我们的注册端点的回调函数。因此,我们的 permission_callback 和 callback 都会收到传入的请求对象。这使我们能够在回调中访问各种请求属性,从而定制我们的响应以匹配所需的输出。
请求属性
请求对象具有许多不同的属性,每个属性都可以以各种方式使用。主要属性包括请求方法、路由、头部、参数和属性。让我们逐一分析它们在请求中的作用。如果您自己创建一个请求对象,它将如下所示:
$request = new WP_REST_Request( 'GET', '/my-namespace/v1/examples' );
在上述代码示例中,我们仅指定请求对象的方法为 GET,并且我们应该匹配路由 /my-namespace/v1/examples,在完整 URL 的上下文中看起来像这样:https://ourawesomesite.com/wp-json/my-namespace/v1/examples`。用于WP_REST_Request`构造函数的方法和路由参数用于将请求映射到所需的端点。如果请求是针对未注册的端点进行的,则响应中将返回一个有用的 404 错误消息。让我们更深入地了解各个属性。
方法
请求对象的 method 属性默认与 HTTP 请求方法匹配。在大多数情况下,该方法将是 GET、POST、PUT、DELETE、OPTIONS 或 HEAD 之一。这些方法将用于匹配注册到路由的各种端点。当 API 找到方法和路由的匹配项时,它将触发该端点的回调。
以下约定是匹配 HTTP 方法的最佳实践:GET 用于只读任务,POST 用于创建,PUT 用于更新,DELETE 用于删除。请求方法作为您端点预期功能的指示器。当您向路由发出 GET 请求时,您应该期望返回只读数据。
路由
请求的路由默认将与服务器环境变量中的路径信息匹配;$_SERVER['PATH_INFO']。当您向 WordPress REST API 的路由发出 HTTP 请求时,生成的 WP_REST_Request 对象将被设置为匹配该路径,希望随后能将其匹配到有效的端点。简而言之,请求的路由就是您希望在 API 中定位请求的位置。
如果我们注册了一个书籍端点并使用 GET,它可能位于 `https://ourawesomesite.com/wp-json/my-namespace/v1/books`。如果我们在浏览器中访问该 URL,我们将看到以 JSON 表示的我们的书籍集合。WordPress 将自动为我们生成请求对象并处理所有路由以匹配端点。因此,既然我们不需要担心路由本身,了解如何在请求中传递额外数据就是更需要理解的事情。
头部
HTTP 请求头仅仅是关于我们 HTTP 请求的额外数据。请求头可以指定缓存策略、我们的请求内容是什么、请求来自哪里以及许多其他事情。请求头不一定直接与我们的端点交互,但头部的信息有助于 WordPress 知道要做什么。为了传递我们希望端点与之交互的数据,我们要使用参数。
参数
在向 WordPress REST API 发出请求时,传递的大部分附加数据将以参数的形式出现。什么是参数?在 API 的上下文中,有四种不同类型的参数。它们是路由参数、查询参数、正文参数和文件参数。让我们更深入地了解每一个。
URL 参数
URL 参数是从请求路由中的路径变量自动生成的 WP_REST_Request。这意味着什么?让我们看看这个路由,它通过 id 获取单独的书籍:/my-namespace/v1/books/(?P<id>\d+)。看起来奇怪的 (?P<id>\d+) 是一个路径变量。路径变量的名称是 'id'。
如果我们发出类似 GET https://ourawesomesite.com/wp-json/my-namespace/v1/books/5` 的请求,5将成为我们id路径变量的值。WP_REST_Request`对象将自动获取该路径变量并将其存储为 URL 参数。现在在我们的端点回调中,我们可以非常容易地与那个 URL 参数交互。让我们看一个例子。
// Register our individual books endpoint.
function prefix_register_book_route() {
register_rest_route( 'my-namespace/v1', '/books/(?P<id>\d+)', array(
// Supported methods for this endpoint. WP_REST_Server::READABLE translates to GET.
'methods' => WP_REST_Server::READABLE,
// Register the callback for the endpoint.
'callback' => 'prefix_get_book',
) );
}
add_action( 'rest_api_init', 'prefix_register_book_route' );
/**
* Our registered endpoint callback. Notice how we are passing in $request as an argument.
* By default, the WP_REST_Server will pass in the matched request object to our callback.
*
* @param WP_REST_Request $request The current matched request object.
*/
function prefix_get_book( $request ) {
// Here we are accessing the path variable 'id' from the $request.
$book = prefix_get_the_book( $request['id'] );
return rest_ensure_response( $book );
}
// A simple function that grabs a book title from our books by ID.
function prefix_get_the_book( $id ) {
$books = array(
'Design Patterns',
'Clean Code',
'Refactoring',
'Structure and Interpretation of Computer Programs',
);
$book = '';
if ( isset( $books[ $id ] ) ) {
// Grab the matching book.
$book = $books[ $id ];
} else {
// Error handling.
return new WP_Error( 'rest_not_found', esc_html__( 'The book does not exist', 'my-text-domain' ), array( 'status' => 404 ) );
}
return $book;
}
在上面的示例中,我们看到路径变量如何作为 URL 参数存储在请求对象中。然后我们可以在端点回调中访问这些参数。上面的示例是使用 URL 参数的相当常见的用例。向路由添加过多的路径变量可能会减慢路由的匹配速度,也可能使注册端点过于复杂,建议谨慎使用 URL 参数。如果我们不应该在 URL 路径中直接使用参数,那么我们需要另一种方式来向请求传递额外信息。这就是查询参数和正文参数的作用,它们通常会在您的 API 中承担大部分工作。
查询参数
查询参数存在于 URI 的查询字符串部分。URI 的查询字符串部分在 https://ourawesomesite.com/wp-json/my-namespace/v1/books?per_page=2&genre=fiction` 中是?per_page=2&genre=fiction。查询字符串由 '?' 字符开始,查询字符串内的不同值由 '&' 字符分隔。我们在查询字符串中指定了两个参数;per_page和genre。在我们的端点中,我们希望只从小说类型中获取两本书。我们可以像这样在回调中访问这些值:$request[‘per_page’]和$request[‘genre’]`(假设 $request 是我们使用的参数名称)。如果您熟悉 PHP,您可能已经在 Web 应用程序中使用过查询参数。
在 PHP 中,查询参数存储在超全局变量 $_GET 中。值得注意的是,您绝不应直接在端点中访问任何超全局变量或服务器变量。最好始终使用 WP_REST_Request 类提供的功能。向端点传递变量的另一种常见方法是使用正文参数。
正文参数
Body 参数是存储在请求体中的键值对。如果您曾经通过 <form>、cURL 或其他方法发送过 POST 请求,那么您就已经使用过 body 参数了。使用 body 参数时,您还可以以不同的内容类型传递它们。POST 请求的默认 Content-Type 头是 x-www-form-urlencoded。当使用 x-www-form-urlencoded 时,参数像查询字符串一样发送:per_page=2&genre=fiction。HTML 表单默认会将各种输入打包并发送一个匹配 x-www-form-urlencoded 模式的 POST 请求。
值得注意的是,虽然 HTTP 规范并不禁止在 GET 请求中发送 body 参数,但建议您不要在 GET 请求中使用 body 参数。Body 参数应当用于 POST、PUT 和 DELETE 请求。
文件参数
在 WP_REST_Request 对象中的文件参数是在请求使用了特殊内容类型头 multipart/form-data 时存储的。然后可以使用 $request->get_file_params() 从请求对象中访问文件数据。文件参数等同于 PHP 超全局变量: $_FILES。请记住,不要直接访问超全局变量,只使用 WP_REST_Request 对象提供的内容。
在端点回调中,我们可以使用 wp_handle_upload() 将所需的文件添加到 WordPress 的媒体上传目录中。文件参数仅用于处理文件数据,您永远不应将它们用于其他目的。
属性
WP_REST_Request 还支持请求属性。请求的属性是与匹配路由注册关联的属性。如果我们向 my-namespace/v1/books 发出 GET 请求,然后在我们的端点回调中调用 $request->get_attributes(),我们将获得 my-namespace/v1/books 端点的所有注册选项。如果我们向同一路由发出 POST 请求,并且我们的端点回调也返回 $request->get_attributes(),我们将收到注册到 POST 端点回调的不同一组端点选项。
在属性中,我们将得到一个包含支持的方法、选项、是否显示此端点在索引中、端点的注册参数列表以及我们注册的回调的响应。它可能看起来像这样:
{
"methods": {
"GET": true
},
"accept_json": false,
"accept_raw": false,
"show_in_index": true,
"args": {
"context": {
"description": "Scope under which the request is made; determines fields present in response.",
"type": "string",
"sanitize_callback": "sanitize_key",
"validate_callback": "rest_validate_request_arg",
"enum": [
"view",
"embed",
"edit"
],
"default": "view"
},
"page": {
"description": "Current page of the collection.",
"type": "integer",
"default": 1,
"sanitize_callback": "absint",
"validate_callback": "rest_validate_request_arg",
"minimum": 1
},
"per_page": {
"description": "Maximum number of items to be returned in result set.",
"type": "integer",
"default": 10,
"minimum": 1,
"maximum": 100,
"sanitize_callback": "absint",
"validate_callback": "rest_validate_request_arg"
},
"search": {
"description": "Limit results to those matching a string.",
"type": "string",
"sanitize_callback": "sanitize_text_field",
"validate_callback": "rest_validate_request_arg"
},
"after": {
"description": "Limit response to resources published after a given ISO8601 compliant date.",
"type": "string",
"format": "date-time",
"validate_callback": "rest_validate_request_arg"
},
"author": {
"description": "Limit result set to posts assigned to specific authors.",
"type": "array",
"default": [],
"sanitize_callback": "wp_parse_id_list",
"validate_callback": "rest_validate_request_arg"
},
"author_exclude": {
"description": "Ensure result set excludes posts assigned to specific authors.",
"type": "array",
"default": [],
"sanitize_callback": "wp_parse_id_list",
"validate_callback": "rest_validate_request_arg"
},
"before": {
"description": "Limit response to resources published before a given ISO8601 compliant date.",
"type": "string",
"format": "date-time",
"validate_callback": "rest_validate_request_arg"
},
"exclude": {
"description": "Ensure result set excludes specific ids.",
"type": "array",
"default": [],
"sanitize_callback": "wp_parse_id_list"
},
"include": {
"description": "Limit result set to specific ids.",
"type": "array",
"default": [],
"sanitize_callback": "wp_parse_id_list"
},
"offset": {
"description": "Offset the result set by a specific number of items.",
"type": "integer",
"sanitize_callback": "absint",
"validate_callback": "rest_validate_request_arg"
},
"order": {
"description": "Order sort attribute ascending or descending.",
"type": "string",
"default": "desc",
"enum": [
"asc",
"desc"
],
"validate_callback": "rest_validate_request_arg"
},
"orderby": {
"description": "Sort collection by object attribute.",
"type": "string",
"default": "date",
"enum": [
"date",
"relevance",
"id",
"include",
"title",
"slug"
],
"validate_callback": "rest_validate_request_arg"
},
"slug": {
"description": "Limit result set to posts with a specific slug.",
"type": "string",
"validate_callback": "rest_validate_request_arg"
},
"status": {
"default": "publish",
"description": "Limit result set to posts assigned a specific status; can be comma-delimited list of status types.",
"enum": [
"publish",
"future",
"draft",
"pending",
"private",
"trash",
"auto-draft",
"inherit",
"any"
],
"sanitize_callback": "sanitize_key",
"type": "string",
"validate_callback": [
{},
"validate_user_can_query_private_statuses"
]
},
"filter": {
"description": "Use WP Query arguments to modify the response; private query vars require appropriate authorization."
},
"categories": {
"description": "Limit result set to all items that have the specified term assigned in the categories taxonomy.",
"type": "array",
"sanitize_callback": "wp_parse_id_list",
"default": []
},
"tags": {
"description": "Limit result set to all items that have the specified term assigned in the tags taxonomy.",
"type": "array",
"sanitize_callback": "wp_parse_id_list",
"default": []
}
},
"callback": [
{},
"get_items"
],
"permission_callback": [
{},
"get_items_permissions_check"
]
}
如您所见,我们已经将所有注册到端点的信息都准备好了!请求属性通常在较低级别使用,并由 WP_REST_Server 类处理,但在端点回调内部也可以做一些很酷的事情,比如限制接受的参数以匹配注册的参数。
WP REST API 的设计目的是让您无需干预任何内部机制,因此这些与 WP_REST_Request 交互的高级方法不会经常使用。使用 WP REST API 的核心在于注册路由和端点。请求是我们用来告诉 API 我们要访问哪个端点的工具。这通常是通过 HTTP 完成的,但我们也可以在内部使用 WP_REST_Request。
内部请求
进行内部请求的关键是使用 rest_do_request()。您只需要传入一个请求对象,就会返回一个响应。由于请求从未由 WP_REST_Server 服务,因此响应数据永远不会被编码为 json,这意味着我们拥有作为 PHP 对象的响应对象。这非常棒,使我们能够做很多有趣的事情。例如,我们可以创建高效的批量端点。从性能角度来看,其中一个障碍是最大限度地减少 HTTP 请求。我们可以创建批量端点,利用 rest_do_request() 在一个 HTTP 请求中在我们的内部服务所有请求。这里有一个非常简单的只读数据批量端点示例,以便您可以看到 rest_do_request() 的实际应用。
// 注册我们的模拟批处理端点。
function prefix_register_batch_route() {
register_rest_route( 'my-namespace/v1', '/batch', array(
// 此端点支持的方法。WP_REST_Server::READABLE 对应 GET。
'methods' => WP_REST_Server::READABLE,
// 注册该端点的回调函数。
'callback' => 'prefix_do_batch_request',
// 注册批处理端点的参数。
'args' => prefix_batch_request_parameters(),
) );
}
add_action( 'rest_api_init', 'prefix_register_batch_route' );
/**
* 我们注册的端点回调函数。注意我们如何将 $request 作为参数传入。
* 默认情况下,WP_REST_Server 会将匹配到的请求对象传递给我们的回调函数。
*
* @param WP_REST_Request $request 当前匹配到的请求对象。
*/
function prefix_do_batch_request( $request ) {
// 在这里初始化一个将保存响应数据的数组。
$data = array();
$data = prefix_handle_batch_requests( $request['requests'] );
return $data;
}
/**
* 这用于构建我们发出的批处理请求的响应。
*
* @param array $requests 用于构建 WP_REST_Request 对象的数据数组。
* @return WP_REST_Response 批处理端点的响应数据集合。
*/
function prefix_handle_batch_requests( $requests ) {
$data = array();
// 遍历 requests 参数中指定的每个请求并执行端点。
foreach ( $requests as $request_params ) {
$response = prefix_handle_request( $request_params );
$key = $request_params['method'] . ' ' . $request_params['route'];
$data[ $key ] = prefix_prepare_for_collection( $response );
}
return rest_ensure_response( $data );
}
/**
* 这用于构建我们发出的批处理请求的响应。
*
* @param array $request_params 用于构建 WP_REST_Request 对象的数据。
* @return WP_REST_Response 请求的响应数据。
*/
function prefix_handle_request( $request_params ) {
$request = new WP_REST_Request( $request_params['method'], $request_params['route'] );
// 将指定的请求参数添加到请求中。
if ( isset( $request_params['params'] ) ) {
foreach ( $request_params['params'] as $param_name => $param_value ) {
$request->set_param( $param_name, $param_value );
}
}
$response = rest_do_request( $request );
return $response;
}
/**
* 准备响应以插入到响应集合中。
*
* 此代码取自 WP REST API v2 插件中的 WP_REST_Controller 类。
*
* @param WP_REST_Response $response 响应对象。
* @return array 响应数据,已准备好插入到集合数据中。
*/
function prefix_prepare_for_collection( $response ) {
if ( ! ( $response instanceof WP_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;
}
/**
* 返回我们注册参数的 JSON schema 数据。
*
* @return array $params JSON Schema 数据的 PHP 表示形式。
*/
function prefix_batch_request_parameters() {
$params = array();
$params['requests'] = array(
'description' => esc_html__( '可构建为 WP_REST_Request 实例的请求对象参数数组。', 'my-text-domain' ),
'type' => 'array',
'required' => true,
'validate_callback' => 'prefix_validate_requests',
'items' => array(
array(
'type' => 'object',
'properties' => array(
'method' => array(
'description' => esc_html__( '所需请求的 HTTP 方法。', 'my-text-domain' ),
'type' => 'string',
'required' => true,
'enum' => array(
'GET',
'POST',
'PUT',
'DELETE',
'OPTIONS',
),
),
'route' => array(
'description' => esc_html__( '所需的请求路由。', 'my-text-domain' ),
'required' => true,
'type' => 'string',
'format' => 'uri',
),
'params' => array(
'description' => esc_html__( '所需请求参数的键值对。', 'my-text-domain' ),
'type' => 'object',
),
),
),
),
);
return $params;
}
function prefix_validate_requests( $requests, $request, $param_key ) {
// 如果 requests 不是请求数组,则不处理批处理。
if ( ! is_array( $requests ) ) {
return new WP_Error( 'rest_invald_param', esc_html__( 'requests 参数必须是请求数组。', 'my-text-domain' ), array( 'status' => 400 ) );
}
foreach ( $requests as $request ) {
// 如果未设置 method 或 route,则不运行请求。
if ( ! isset( $request['method'] ) || ! isset( $request['route'] ) ) {
return new WP_Error( 'rest_invald_param', esc_html__( '您必须为每个请求指定 method 和 route。', 'my-text-domain' ), array( 'status' => 400 ) );
}
if ( isset( $request['params'] ) && ! is_array( $request['params'] ) ) {
return new WP_Error( 'rest_invald_param', esc_html__( '您必须将每个请求的 params 指定为命名键值对的数组。', 'my-text-domain' ), array( 'status' => 400 ) );
}
}
// 这是一种黑名单式的数据验证方法。
return true;
}
这是一段相当不错的代码,涵盖了多个主题,但所有内容都围绕着 prefix_handle_request() 中发生的情况展开。在这里,我们传入一个数组,其中包含告诉我们 HTTP 方法、路由以及我们希望将其转换为请求的一组参数的信息。然后我们为方法和路由构建请求对象。如果指定了任何参数,我们使用 WP_REST_Request::set_param() 方法来添加所需的参数。一旦我们的 WP_REST_Request 准备就绪,我们就使用 rest_do_request 在内部匹配该端点,并将响应返回给我们的批处理端点响应集合。使用这种批处理端点可以为您带来巨大的性能提升,因为您只需发出一个 HTTP 请求即可获取多个端点的响应。此实现不一定是最佳方案,仅作为示例;并非实现此功能的唯一方式。