概述

虽然 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&amp;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 请求即可获取多个端点的响应。此实现不一定是最佳方案,仅作为示例;并非实现此功能的唯一方式。