基本的なこと

APIにカスタムエンドポイントを追加したいですか?素晴らしい!シンプルな例から始めましょう。

シンプルな関数は次のようになります:

  1. <?php
  2. /**
  3. * Grab latest post title by an author!
  4. *
  5. * @param array $data Options for the function.
  6. * @return string|null Post title for the latest,* or null if none.
  7. */
  8. function my_awesome_func( $data ) {
  9. $posts = get_posts( array(
  10. 'author' => $data['id'],
  11. ) );
  12. if ( empty( $posts ) ) {
  13. return null;
  14. }
  15. return $posts[0]->post_title;
  16. }

これをAPI経由で利用可能にするためには、ルートを登録する必要があります。これにより、APIは特定のリクエストに対して私たちの関数で応答することができます。これは、register_rest_routeという関数を通じて行い、APIが読み込まれていないときに余分な作業を避けるためにrest_api_initのコールバックで呼び出す必要があります。

  1. ``````bash
  2. <?php
  3. add_action( 'rest_api_init', function () {
  4. register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
  5. 'methods' => 'GET',
  6. 'callback' => 'my_awesome_func',
  7. ) );
  8. } );
  9. `

現在、私たちはそのルートのために1つのエンドポイントのみを登録しています。「ルート」という用語はURLを指し、「エンドポイント」はそれに対応するメソッドおよび URLの背後にある関数を指します(詳細については用語集を参照してください)。

たとえば、あなたのサイトのドメインがexample.comで、APIパスがwp-jsonである場合、完全なURLはhttp://example.com/wp-json/myplugin/v1/author/(?P\d+)になります。

美しいパーマリンクがないサイトでは、ルートはrest_routeパラメータとしてURLに追加されます。上記の例では、完全なURLはhttp://example.com/?rest_route=/myplugin/v1/author/(?P\d+)になります。

各ルートには任意の数のエンドポイントを持つことができ、各エンドポイントについて許可されるHTTPメソッド、リクエストに応答するためのコールバック関数、およびカスタム権限を作成するための権限コールバックを定義できます。さらに、リクエスト内で許可されるフィールドを定義し、各フィールドにデフォルト値、サニタイズコールバック、バリデーションコールバック、およびフィールドが必須かどうかを指定できます。

名前空間

名前空間はエンドポイントのURLの最初の部分です。カスタムルート間の衝突を防ぐために、ベンダー/パッケージのプレフィックスとして使用する必要があります。名前空間を使用すると、2つのプラグインが異なる機能を持つ同じ名前のルートを追加できます。

一般的に、名前空間はvendor/v1のパターンに従うべきで、vendorは通常、あなたのプラグインまたはテーマのスラッグであり、v1はAPIの最初のバージョンを表します。新しいエンドポイントで互換性を破る必要がある場合は、これをv2に上げることができます。

上記のシナリオでは、異なる2つのプラグインから同じ名前の2つのルートが必要であり、すべてのベンダーはユニークな名前空間を使用する必要があります。これを怠ることは、テーマやプラグインでベンダー関数のプレフィックス、クラスプレフィックス、および/またはクラス名前空間を使用しないことに類似しており、非常に悪いです。

名前空間を使用する追加の利点は、クライアントがあなたのカスタムAPIのサポートを検出できることです。APIインデックスは、サイト上の利用可能な名前空間をリストします:

  1. {
  2. "name": "WordPress Site",
  3. "description": "Just another WordPress site",
  4. "url": "http://example.com/",
  5. "namespaces": [
  6. "wp/v2",
  7. "vendor/v1",
  8. "myplugin/v1",
  9. "myplugin/v2",
  10. ]
  11. }

クライアントがあなたのAPIがサイトに存在するかどうかを確認したい場合、彼らはこのリストに対して確認できます。(詳細については発見ガイドを参照してください。)

引数

デフォルトでは、ルートはリクエストから渡されたすべての引数を受け取ります。これらは単一のパラメータセットにマージされ、その後リクエストオブジェクトに追加され、これはエンドポイントへの最初のパラメータとして渡されます:

  1. <?php
  2. function my_awesome_func( WP_REST_Request $request ) {
  3. // You can access parameters via direct array access on the object:
  4. $param = $request['some_param'];
  5. // Or via the helper method:
  6. $param = $request->get_param( 'some_param' );
  7. // You can get the combined, merged set of parameters:
  8. $parameters = $request->get_params();
  9. // The individual sets of parameters are also available, if needed:
  10. $parameters = $request->get_url_params();
  11. $parameters = $request->get_query_params();
  12. $parameters = $request->get_body_params();
  13. $parameters = $request->get_json_params();
  14. $parameters = $request->get_default_params();
  15. // Uploads aren't merged in, but can be accessed separately:
  16. $parameters = $request->get_file_params();
  17. }

(パラメータがどのようにマージされるかを正確に知りたい場合は、WP_REST_Request::get_parameter_order()のソースを確認してください。基本的な順序は、ボディ、クエリ、URL、次にデフォルトです。)

通常、すべてのパラメータは変更されずに取り込まれます。ただし、ルートを登録する際に引数を登録することができ、これによりサニタイズとバリデーションを実行できます。

リクエストにContent-type: application/jsonヘッダーが設定され、ボディに有効なJSONが含まれている場合、get_json_params()は解析されたJSONボディを連想配列として返します。

引数は、各エンドポイントのargsキーでマップとして定義されます(callbackオプションの隣)。このマップは、引数の名前をキーとして使用し、その値はその引数のオプションのマップです。この配列には、defaultrequiredsanitize_callback、およびvalidate_callbackのキーを含めることができます。

  • default:引数に値が提供されていない場合のデフォルト値として使用されます。
  • required:trueとして定義されている場合、その引数に値が渡されないとエラーが返されます。デフォルト値が設定されている場合は影響がありません。引数には常に値があります。
  • validate_callback:引数の値を受け取る関数を渡すために使用されます。その関数は、値が有効な場合はtrueを、そうでない場合はfalseを返す必要があります。
  • sanitize_callback:メインコールバックに渡す前に引数の値をサニタイズするために使用される関数を渡すために使用されます。
  1. 前の例を考えると、渡されたパラメータが常に数値であることを保証できます:
  2. ``````bash
  3. <?php
  4. add_action( 'rest_api_init', function () {
  5. register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
  6. 'methods' => 'GET',
  7. 'callback' => 'my_awesome_func',
  8. 'args' => array(
  9. 'id' => array(
  10. 'validate_callback' => function($param, $request, $key) {
  11. return is_numeric( $param );
  12. }
  13. ),
  14. ),
  15. ) );
  16. } );
  17. `
  1. `````'sanitize_callback' => 'absint'`````のようなものを代わりに使用することもできますが、バリデーションはエラーをスローし、クライアントが何を間違っているのかを理解できるようにします。サニタイズは、エラーをスローするのではなく、入力されるデータを変更したい場合に便利です(無効なHTMLなど)。
  2. <a name="return-value"></a>
  3. ### 戻り値
  4. コールバックが呼び出された後、戻り値はJSONに変換され、クライアントに返されます。これにより、基本的に任意の形式のデータを返すことができます。上記の例では、文字列またはnullを返しており、これらはAPIによって自動的に処理され、JSONに変換されます。
  5. 他のWordPress関数と同様に、`````WP_Error`````インスタンスを返すこともできます。このエラー情報は、500 Internal Service Errorステータスコードとともにクライアントに渡されます。`````status`````オプションを`````WP_Error`````インスタンスデータに設定することで、エラーをさらにカスタマイズできます。たとえば、無効な入力データの場合は`````400`````を使用します。
  6. 以前の例を考えると、エラーインスタンスを返すことができます:
  7. ``````bash
  8. <?php
  9. /**
  10. * Grab latest post title by an author!
  11. *
  12. * @param array $data Options for the function.
  13. * @return string|null Post title for the latest,
  14. * or null if none.
  15. */
  16. function my_awesome_func( $data ) {
  17. $posts = get_posts( array(
  18. 'author' => $data['id'],
  19. ) );
  20. if ( empty( $posts ) ) {
  21. return new WP_Error( 'no_author', 'Invalid author', array( 'status' => 404 ) );
  22. }
  23. return $posts[0]->post_title;
  24. }
  25. `

著者に属する投稿がない場合、これはクライアントに404 Not Foundエラーを返します:

  1. HTTP/1.1 404 Not Found
  2. [{
  3. "code": "no_author",
  4. "message": "Invalid author",
  5. "data": { "status": 404 }
  6. }]

より高度な使用法では、WP_REST_Responseオブジェクトを返すことができます。このオブジェクトは通常のボディデータを「ラップ」しますが、カスタムステータスコードやカスタムヘッダーを返すことを可能にします。また、レスポンスにリンクを追加することもできます。これを使用する最も簡単な方法は、コンストラクタを介して行うことです:

  1. <?php
  2. $data = array( 'some', 'response', 'data' );
  3. // Create the response object
  4. $response = new WP_REST_Response( $data );
  5. // Add a custom status code
  6. $response->set_status( 201 );
  7. // Add a custom header
  8. $response->header( 'Location', 'http://example.com/' );

既存のコールバックをラップする場合は、常に戻り値にrest_ensure_response()を使用する必要があります。これにより、エンドポイントから返された生データが自動的にWP_REST_Responseに変換されます。(WP_Errorは適切なエラーハンドリングを可能にするためにWP_REST_Responseに変換されません。)

重要なことに、REST APIルートのコールバックは常にデータを返す必要があります。レスポンスボディ自体を送信しようとすべきではありません。これにより、REST APIサーバーが行う追加の処理、リンク/埋め込みの処理、ヘッダーの送信などが行われます。言い換えれば、die( wp_json_encode( $data ) );wp_send_json( $data )を呼び出さないでください。WordPress 5.5以降、REST APIリクエスト中にwp_send_json()ファミリーの関数が使用されると、_doing_it_wrong通知が発行されます。

REST APIを使用する際は、コールバックからWP_REST_ResponseまたはWP_Errorオブジェクトを返してください。

権限コールバック

エンドポイントのために権限コールバックも登録する必要があります。これは、実際のコールバックが呼び出される前に、ユーザーがアクション(読み取り、更新など)を実行できるかどうかを確認する関数です。これにより、APIはクライアントに特定のURLで実行できるアクションを伝えることができ、最初にリクエストを試みる必要がありません。

このコールバックは、permission_callbackとして登録でき、再びcallbackオプションの隣のエンドポイントオプションにあります。このコールバックは、ブール値またはWP_Errorインスタンスを返す必要があります。この関数がtrueを返すと、レスポンスが処理されます。falseを返すと、デフォルトのエラーメッセージが返され、リクエストは処理を続行しません。WP_Errorを返すと、そのエラーがクライアントに返されます。

権限コールバックは、現在のユーザーを設定するリモート認証の後に実行されます。これにより、current_user_canを使用して、認証されたユーザーがアクションに対して適切な権限を持っているか、または現在のユーザーIDに基づいて他のチェックを行うことができます。可能な限り、current_user_canを常に使用するべきです。ユーザーがログインしているかどうか(認証)を確認するのではなく、アクションを実行できるかどうか(認可)を確認してください。

  1. 前の例を続けると、編集者以上のユーザーのみがこの著者データを表示できるようにすることができます。ここでいくつかの異なる権限を確認できますが、最も良いのは`````edit_others_posts`````で、これは実際に編集者の核心です。これを行うには、ここでコールバックが必要です:
  2. ``````bash
  3. <?php
  4. add_action( 'rest_api_init', function () {
  5. register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
  6. 'methods' => 'GET',
  7. 'callback' => 'my_awesome_func',
  8. 'args' => array(
  9. 'id' => array(
  10. 'validate_callback' => 'is_numeric'
  11. ),
  12. ),
  13. 'permission_callback' => function () {
  14. return current_user_can( 'edit_others_posts' );
  15. }
  16. ) );
  17. } );
  18. `

権限コールバックもリクエストオブジェクトを最初のパラメータとして受け取るため、必要に応じてリクエスト引数に基づいてチェックを行うことができます。

WordPress 5.5以降、permission_callbackが提供されていない場合、REST APIは_doing_it_wrong通知を発行します。

myplugin/v1/authorのREST APIルート定義には、必須のpermission_callback引数が欠けています。公開を意図したREST APIルートには、__return_trueを権限コールバックとして使用してください。
公開されているREST APIエンドポイントの場合、return_trueを権限コールバックとして使用できます。

  1. <?php
  2. add_action( 'rest_api_init', function () {
  3. register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
  4. 'methods' => 'GET',
  5. 'callback' => 'my_awesome_func',
  6. 'permission_callback' => '__return_true',
  7. ) );
  8. } );

発見

カスタムエンドポイントのリソース発見を有効にしたい場合は、rest_queried_resource_routeフィルターを使用して行うことができます。たとえば、カスタムリソースのIDを含むカスタムクエリ変数my-routeを考えてみてください。次のコードスニペットは、my-routeクエリ変数が使用されるたびに発見リンクを追加します。

  1. function my_plugin_rest_queried_resource_route( $route ) {
  2. $id = get_query_var( 'my-route' );
  3. if ( ! $route && $id ) {
  4. $route = '/my-ns/v1/items/' . $id;
  5. }
  6. return $route;
  7. }
  8. add_filter( 'rest_queried_resource_route', 'my_plugin_rest_queried_resource_route' );

注意:エンドポイントがカスタム投稿タイプまたはカスタムタクソノミーを説明している場合、rest_route_for_postまたはrest_route_for_termフィルターを使用することをお勧めします。

コントローラーパターン

コントローラーパターンは、APIで複雑なエンドポイントを扱うためのベストプラクティスです。

このセクションを読む前に、「内部クラスの拡張」を読むことをお勧めします。これにより、デフォルトのルートで使用されるパターンに慣れることができ、これはベストプラクティスです。リクエストを処理するために使用するクラスがWP_REST_Controllerクラスまたはそれを拡張するクラスを拡張する必要はありませんが、そうすることで、これらのクラスで行われた作業を継承できます。さらに、使用しているコントローラーメソッドに基づいてベストプラクティスに従っていることを確認できます。

コントローラの本質は、RESTの慣習に一致するように一般的に命名されたメソッドのセットと便利なヘルパーです。コントローラは、register_routesメソッドでルートを登録し、get_itemsget_itemcreate_itemupdate_itemdelete_itemでリクエストに応答し、同様に命名された権限チェックメソッドを持っています。このパターンに従うことで、エンドポイントのステップや機能を見逃すことがありません。

コントローラを使用するには、まず基本コントローラをサブクラス化する必要があります。これにより、独自の動作を追加するための基本的なメソッドセットが得られます。

コントローラをサブクラス化したら、クラスをインスタンス化して動作させる必要があります。これは、rest_api_initにフックされたコールバック内で行うべきで、必要なときにのみクラスをインスタンス化することを保証します。通常のコントローラーパターンは、このコールバック内で$controller->register_routes()を呼び出すことで、クラスがそのエンドポイントを登録できるようにします。

以下は「スターター」カスタムルートです:

  1. <?php
  2. class Slug_Custom_Route extends WP_REST_Controller {
  3. /**
  4. * Register the routes for the objects of the controller.
  5. */
  6. public function register_routes() {
  7. $version = '1';
  8. $namespace = 'vendor/v' . $version;
  9. $base = 'route';
  10. register_rest_route( $namespace, '/' . $base, array(
  11. array(
  12. 'methods' => WP_REST_Server::READABLE,
  13. 'callback' => array( $this, 'get_items' ),
  14. 'permission_callback' => array( $this, 'get_items_permissions_check' ),
  15. 'args' => array(
  16. ),
  17. ),
  18. array(
  19. 'methods' => WP_REST_Server::CREATABLE,
  20. 'callback' => array( $this, 'create_item' ),
  21. 'permission_callback' => array( $this, 'create_item_permissions_check' ),
  22. 'args' => $this->get_endpoint_args_for_item_schema( true ),
  23. ),
  24. ) );
  25. register_rest_route( $namespace, '/' . $base . '/(?P<id>[\d]+)', array(
  26. array(
  27. 'methods' => WP_REST_Server::READABLE,
  28. 'callback' => array( $this, 'get_item' ),
  29. 'permission_callback' => array( $this, 'get_item_permissions_check' ),
  30. 'args' => array(
  31. 'context' => array(
  32. 'default' => 'view',
  33. ),
  34. ),
  35. ),
  36. array(
  37. 'methods' => WP_REST_Server::EDITABLE,
  38. 'callback' => array( $this, 'update_item' ),
  39. 'permission_callback' => array( $this, 'update_item_permissions_check' ),
  40. 'args' => $this->get_endpoint_args_for_item_schema( false ),
  41. ),
  42. array(
  43. 'methods' => WP_REST_Server::DELETABLE,
  44. 'callback' => array( $this, 'delete_item' ),
  45. 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
  46. 'args' => array(
  47. 'force' => array(
  48. 'default' => false,
  49. ),
  50. ),
  51. ),
  52. ) );
  53. register_rest_route( $namespace, '/' . $base . '/schema', array(
  54. 'methods' => WP_REST_Server::READABLE,
  55. 'callback' => array( $this, 'get_public_item_schema' ),
  56. ) );
  57. }
  58. /**
  59. * Get a collection of items
  60. *
  61. * @param WP_REST_Request $request Full data about the request.
  62. * @return WP_Error|WP_REST_Response
  63. */
  64. public function get_items( $request ) {
  65. $items = array(); //do a query, call another class, etc
  66. $data = array();
  67. foreach( $items as $item ) {
  68. $itemdata = $this->prepare_item_for_response( $item, $request );
  69. $data[] = $this->prepare_response_for_collection( $itemdata );
  70. }
  71. return new WP_REST_Response( $data, 200 );
  72. }
  73. /**
  74. * Get one item from the collection
  75. *
  76. * @param WP_REST_Request $request Full data about the request.
  77. * @return WP_Error|WP_REST_Response
  78. */
  79. public function get_item( $request ) {
  80. //get parameters from request
  81. $params = $request->get_params();
  82. $item = array();//do a query, call another class, etc
  83. $data = $this->prepare_item_for_response( $item, $request );
  84. //return a response or error based on some conditional
  85. if ( 1 == 1 ) {
  86. return new WP_REST_Response( $data, 200 );
  87. } else {
  88. return new WP_Error( 'code', __( 'message', 'text-domain' ) );
  89. }
  90. }
  91. /**
  92. * Create one item from the collection
  93. *
  94. * @param WP_REST_Request $request Full data about the request.
  95. * @return WP_Error|WP_REST_Response
  96. */
  97. public function create_item( $request ) {
  98. $item = $this->prepare_item_for_database( $request );
  99. if ( function_exists( 'slug_some_function_to_create_item' ) ) {
  100. $data = slug_some_function_to_create_item( $item );
  101. if ( is_array( $data ) ) {
  102. return new WP_REST_Response( $data, 200 );
  103. }
  104. }
  105. return new WP_Error( 'cant-create', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
  106. }
  107. /**
  108. * Update one item from the collection
  109. *
  110. * @param WP_REST_Request $request Full data about the request.
  111. * @return WP_Error|WP_REST_Response
  112. */
  113. public function update_item( $request ) {
  114. $item = $this->prepare_item_for_database( $request );
  115. if ( function_exists( 'slug_some_function_to_update_item' ) ) {
  116. $data = slug_some_function_to_update_item( $item );
  117. if ( is_array( $data ) ) {
  118. return new WP_REST_Response( $data, 200 );
  119. }
  120. }
  121. return new WP_Error( 'cant-update', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
  122. }
  123. /**
  124. * Delete one item from the collection
  125. *
  126. * @param WP_REST_Request $request Full data about the request.
  127. * @return WP_Error|WP_REST_Response
  128. */
  129. public function delete_item( $request ) {
  130. $item = $this->prepare_item_for_database( $request );
  131. if ( function_exists( 'slug_some_function_to_delete_item' ) ) {
  132. $deleted = slug_some_function_to_delete_item( $item );
  133. if ( $deleted ) {
  134. return new WP_REST_Response( true, 200 );
  135. }
  136. }
  137. return new WP_Error( 'cant-delete', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
  138. }
  139. /**
  140. * Check if a given request has access to get items
  141. *
  142. * @param WP_REST_Request $request Full data about the request.
  143. * @return WP_Error|bool
  144. */
  145. public function get_items_permissions_check( $request ) {
  146. //return true; <--use to make readable by all
  147. return current_user_can( 'edit_something' );
  148. }
  149. /**
  150. * Check if a given request has access to get a specific item
  151. *
  152. * @param WP_REST_Request $request Full data about the request.
  153. * @return WP_Error|bool
  154. */
  155. public function get_item_permissions_check( $request ) {
  156. return $this->get_items_permissions_check( $request );
  157. }
  158. /**
  159. * Check if a given request has access to create items
  160. *
  161. * @param WP_REST_Request $request Full data about the request.
  162. * @return WP_Error|bool
  163. */
  164. public function create_item_permissions_check( $request ) {
  165. return current_user_can( 'edit_something' );
  166. }
  167. /**
  168. * Check if a given request has access to update a specific item
  169. *
  170. * @param WP_REST_Request $request Full data about the request.
  171. * @return WP_Error|bool
  172. */
  173. public function update_item_permissions_check( $request ) {
  174. return $this->create_item_permissions_check( $request );
  175. }
  176. /**
  177. * Check if a given request has access to delete a specific item
  178. *
  179. * @param WP_REST_Request $request Full data about the request.
  180. * @return WP_Error|bool
  181. */
  182. public function delete_item_permissions_check( $request ) {
  183. return $this->create_item_permissions_check( $request );
  184. }
  185. /**
  186. * Prepare the item for create or update operation
  187. *
  188. * @param WP_REST_Request $request Request object
  189. * @return WP_Error|object $prepared_item
  190. */
  191. protected function prepare_item_for_database( $request ) {
  192. return array();
  193. }
  194. /**
  195. * Prepare the item for the REST response
  196. *
  197. * @param mixed $item WordPress representation of the item.
  198. * @param WP_REST_Request $request Request object.
  199. * @return mixed
  200. */
  201. public function prepare_item_for_response( $item, $request ) {
  202. return array();
  203. }
  204. /**
  205. * Get the query params for collections
  206. *
  207. * @return array
  208. */
  209. public function get_collection_params() {
  210. return array(
  211. 'page' => array(
  212. 'description' => 'Current page of the collection.',
  213. 'type' => 'integer',
  214. 'default' => 1,
  215. 'sanitize_callback' => 'absint',
  216. ),
  217. 'per_page' => array(
  218. 'description' => 'Maximum number of items to be returned in result set.',
  219. 'type' => 'integer',
  220. 'default' => 10,
  221. 'sanitize_callback' => 'absint',
  222. ),
  223. 'search' => array(
  224. 'description' => 'Limit results to those matching a string.',
  225. 'type' => 'string',
  226. 'sanitize_callback' => 'sanitize_text_field',
  227. ),
  228. );
  229. }
  230. }