概要
REST API は、多くの点で非常にシンプルです。リクエストと呼ばれる入力があります。入力は、サーバーによって解釈され、出力が作成されます。出力は、レスポンスとして知られています。ある意味では、WordPress REST API へのリクエストは、API が実行し解釈すべき、一連の指示や命令と考えることができます。デフォルトでは、WordPress REST API は、リクエスト媒体として HTTP リクエストを使用するようになっています。HTTP は、インターネット上でのデータ通信の基盤であり、WordPress REST API を非常に広範囲に及ぶ API にしています。API におけるリクエストは、URI、HTTP メソッド、ヘッダー、パラメータなど、HTTP リクエストに存在するさまざまな側面を多く利用しています。リクエストのデータ構造は、クラス WP_REST_Request
によって便利に処理されます。
WP_REST_Request
このクラスは WordPress 4.4で導入された3つの主要な基盤クラスのひとつです。API のエンドポイントに HTTP リクエストが送信されると、API は自動的にクラス WP_REST_Request
のインスタンスを生成し、提供されたデータと一致させます。レスポンス・オブジェクトは WP_REST_Server
のメソッド serve_request()
で自動生成されます。リクエストが作成され、認証がチェックされると、リクエストがディスパッチされ、エンドポイントのコールバックが開始されます。オブジェクト WP_REST_Request
に格納されたデータはすべて、登録したエンドポイントのためのコールバックに渡されます。そのため、permissions_callback
と callback
の両方がリクエスト・オブジェクトが渡された状態で呼び出されます。これにより、コールバック内でさまざまなリクエスト・プロパティにアクセスできるようになるため、希望する出力に合わせてレスポンスを調整可能になります。
リクエスト・プロパティ
リクエスト・オブジェクトにはさまざまなプロパティがあり、それぞれをさまざまな方法で使用できます。主なプロパティは、リクエスト・メソッド、ルート、ヘッダー、パラメータ、属性です。それぞれをリクエストにおける役割に分解してみましょう。リクエスト・オブジェクトを自分で作るとしたら、次のようになります:
$request = new WP_REST_Request( 'GET', '/my-namespace/v1/examples' );
上記のコード例では、リクエスト・オブジェクトのメソッドが GET
であることだけを指定していて、ルート /my-namespace/v1/examples
に一致させる必要があり、URL 全体のコンテキストでは次のようになります: https://ourawesomesite.com/wp-json/my-namepsace/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
を使ってエンドポイント books を登録したとしたら、https://ourawesomesite.com/wp-json/my-namespace/v1/books
にそれは存在するかもしれません。ブラウザでこの URL にアクセスすると、JSON で表現された books のコレクションが表示されます。WordPress は、自動的にリクエスト・オブジェクトを生成し、エンドポイントに一致するようにすべてのルーティングを処理します。したがって、ルーティングについて心配する必要はないので、リクエストに必要な追加のデータを渡す方法を理解することは、より重要なことなのです。
ヘッダー
HTTP リクエスト・ヘッダーは、HTTP リクエストに関する単なる追加データです。リクエスト・ヘッダーには、キャッシュポリシー、リクエストの内容、リクエストの送信元、その他多くのことを指定できます。リクエスト・ヘッダーは、必ずしもエンドポイントと直接やりとりするわけではありませんが、ヘッダーの情報は、WordPress が何をすべきかを知るのに役立ちます。エンドポイントとやりとりさせたいデータを渡すには、パラメータを使います。
パラメータ
WordPress REST API に対してリクエストを行う際、追加で渡されるデータのほとんどはパラメータの形を取ります。パラメータとは、何でしょうか ? API のコンテキストには、4つの異なるタイプがあります。ルート・パラメータ、クエリー・パラメータ、ボディ・パラメータ、ファイル・パラメータです。それぞれをもう少し詳しく見てみましょう。
URL パラメータ
URL パラメータは、リクエストされたルートのパス変数から WP_REST_Request
で自動的に生成されます。これは何を意味するのでしょうか ? 個々の books を id で取得する、このルートを見てみましょう: /my-namespace/v1/books/(?P\d+)
。奇妙に見える (?P\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 のクエリー文字列部分に存在します。https://ourawesomesite.com/wp-json/my-namespace/v1/books?per_page=2&genre=fiction
における URI のクエリー文字列部分は ?per_page=2&genre=fiction
です。クエリー文字列は、文字「?
」で始まり、クエリー文字列内の異なる値は、文字「&
」で区切られます。クエリー文字列には、per_page
と fiction
の2つのパラメータを指定しました。私たちのエンドポイントでは、フィクション・ジャンルから2冊の本だけを取得したいと思います。このようなコールバックでこれらの値にアクセスできます: $request['per_page']
と $request['genre']
($request が、使用する引数の名前であると仮定)。PHP に慣れている人なら、おそらく Web アプリケーションでクエリー・パラメータを使ったことがあるでしょう。
PHP では、クエリー・パラメータは、スーパーグローバル $_GET
に格納されます。注意すべき点は、エンドポイント内でスーパーグローバルやサーバー変数に、直接アクセスしてはいけないということです。クラス WP_REST_Request
が提供するものを使用するのが常にベストです。エンドポイントに変数を渡すもうひとつの一般的な方法は、ボディ・パラメータを使用することです。
ボディ・パラメータ
ボディ・パラメータは、リクエスト・ボディに格納される KVP (キーと値のペア) です。これまでに cURL やその他の方法を使用して POST
リクエストを送信したことがあれば、ボディ・パラメータを使ったことがあるはずです。ボディ・パラメータでは、異なるコンテンツタイプとして渡すこともできます。リクエスト POST
に対するデフォルトのヘッダー Content-Type
は、x-www-form-urlencoded
です。x-www-form-urlencoded
を使用する場合、パラメータはクエリー文字列のように送信されます; per_page=2&genre=fiction
。デフォルトでは、HTML フォームは、さまざまな入力を束ね、パターン x-www-form-urlencoded
に一致するリクエスト POST
を送信します。
HTTP の仕様では GET
リクエストでボディ・パラメータを送ることを禁止しているわけではありませんが、GET
リクエストでボディ・パラメータを使わないことが推奨されています。ボディ・パラメータは 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()
を使用して、内部的にすべてのリクエストを1つの HTTP リクエストで送信するバッチ・エンドポイントを作成できます。以下は、読み取り専用データ用の非常に単純なバッチ・エンドポイントで、rest_do_request()
が動作している様子を見ることができます。
// Register our mock batch endpoint.
function prefix_register_batch_route() {
register_rest_route( 'my-namespace/v1', '/batch', 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_do_batch_request',
// Register args for the batch endpoint.
'args' => prefix_batch_request_parameters(),
) );
}
add_action( 'rest_api_init', 'prefix_register_batch_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_do_batch_request( $request ) {
// Here we initialize the array that will hold our response data.
$data = array();
$data = prefix_handle_batch_requests( $request['requests'] );
return $data;
}
/**
* This handles the building of the response for the batch requests we make.
*
* @param array $requests An array of data to build WP_REST_Request objects from.
* @return WP_REST_Response A collection of response data for batch endpoints.
*/
function prefix_handle_batch_requests( $requests ) {
$data = array();
// Foreach request specified in the requests param run the endpoint.
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 );
}
/**
* This handles the building of the response for the batch requests we make.
*
* @param array $request_params Data to build a WP_REST_Request object from.
* @return WP_REST_Response Response data for the request.
*/
function prefix_handle_request( $request_params ) {
$request = new WP_REST_Request( $request_params['method'], $request_params['route'] );
// Add specified request parameters into the request.
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;
}
/**
* Prepare a response for inserting into a collection of responses.
*
* This is lifted from WP_REST_Controller class in the WP REST API v2 plugin.
*
* @param WP_REST_Response $response Response object.
* @return array Response data, ready for insertion into collection data.
*/
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;
}
/**
* Returns the JSON schema data for our registered parameters.
*
* @return array $params A PHP representation of JSON Schema data.
*/
function prefix_batch_request_parameters() {
$params = array();
$params['requests'] = array(
'description' => esc_html__( 'An array of request objects arguments that can be built into WP_REST_Request instances.', '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 Method of the desired request.', 'my-text-domain' ),
'type' => 'string',
'required' => true,
'enum' => array(
'GET',
'POST',
'PUT',
'DELETE',
'OPTIONS',
),
),
'route' => array(
'description' => esc_html__( 'Desired route for the request.', 'my-text-domain' ),
'required' => true,
'type' => 'string',
'format' => 'uri',
),
'params' => array(
'description' => esc_html__( 'Key value pairs of desired request parameters.', 'my-text-domain' ),
'type' => 'object',
),
),
),
),
);
return $params;
}
function prefix_validate_requests( $requests, $request, $param_key ) {
// If requests isn't an array of requests then we don't process the batch.
if ( ! is_array( $requests ) ) {
return new WP_Error( 'rest_invald_param', esc_html__( 'The requests parameter must be an array of requests.' ), array( 'status' => 400 ) );
}
foreach ( $requests as $request ) {
// If the method or route is not set then we do not run the requests.
if ( ! isset( $request['method'] ) || ! isset( $request['route'] ) ) {
return new WP_Error( 'rest_invald_param', esc_html__( 'You must specify the method and route for each request.' ), array( 'status' => 400 ) );
}
if ( isset( $request['params'] ) && ! is_array( $request['params'] ) ) {
return new WP_Error( 'rest_invald_param', esc_html__( 'You must specify the params for each request as an array of named key value pairs.' ), array( 'status' => 400 ) );
}
}
// This is a black listing approach to data validation.
return true;
}
これは、多くのトピックをカバーする、かなりまとまったコードの塊ですが、すべては prefix_handle_request()
で起こることが中心となっています。ここでは、HTTP メソッド、ルート、そしてリクエストとしたいパラメータのセットを配列で渡しています。次に、メソッドとルートに対してリクエスト・オブジェクトを構築します。パラメータが指定されている場合は、メソッド WP_REST_Request::set_param()
を使用して必要なパラメータを追加します。WP_REST_Request
が準備できたら、rest_do_request
を使って内部的にそのエンドポイントに一致させ、レスポンスをバッチ・エンドポイント・レスポンスコレクションに返します。このようにバッチ・エンドポイントを使えば、複数のエンドポイントに対するレスポンスを得るための HTTP リクエストは1回だけですので、パフォーマンスが大幅に向上します。この実装は必ずしもベストではなく、あくまで一例であり、これが唯一の方法という訳ではありません。