概要
新しいRESTルートを登録するには、リクエストがどのように処理されるか、権限チェックがどのように適用されるか、リソースのスキーマがどのように生成されるかなど、エンドポイントの動作を制御するためのコールバック関数をいくつか指定する必要があります。これらのメソッドを通常のPHPファイルで名前空間やクラスでラップせずに宣言することは可能ですが、その方法で宣言されたすべての関数は同じグローバルスコープに共存します。エンドポイントロジックのためにget_items()
のような一般的な関数名を使用することに決め、別のプラグイン(または自分のプラグイン内の別のエンドポイント)も同じ名前の関数を登録すると、PHPは致命的なエラーで失敗します。なぜなら、関数get_items()
が二重に宣言されているからです。
この問題を回避するには、myplugin_myendpoint_
のようなユニークなプレフィックスを使用してコールバック関数に名前を付け、潜在的な競合を避けることができます:
function myplugin_myendpoint_register_routes() { /* ... */ }
function myplugin_myendpoint_get_item() { /* ... */ }
function myplugin_myendpoint_get_item_schema() { /* ... */ }
// etcetera
add_action( 'rest_api_init', 'myplugin_myendpoint_register_routes' );
このアプローチは、テーマfunctions.php
ファイル内で一般的に使用されているため、すでにご存知かもしれません。しかし、これらのプレフィックスは不必要に冗長であり、エンドポイントのロジックをより保守可能な方法でグループ化しカプセル化するためのいくつかのより良いオプションが存在します。
WordPressは現在、PHP 5.6以上を必要とします。PHP 5.6は、エンドポイントの機能をカプセル化するための簡単な方法を提供する名前空間をサポートしています。エンドポイントのPHPファイルの先頭でnamespace
を宣言することにより、その名前空間内のすべてのメソッドはその名前空間内で宣言され、グローバル関数と競合しなくなります。これにより、エンドポイントコールバックのために短く、より読みやすい名前を使用できます。
namespace MyPlugin\API\MyEndpoint;
function register_routes() { /* ... */ }
function get_item() { /* ... */ }
function get_item_schema() { /* ... */ }
// and so on
add_action( 'rest_api_init', __NAMESPACE__ . '\\register_routes' );
これらの短い関数名は扱いやすいですが、グローバル関数を宣言することに対して他の利点は提供しません。この理由から、WordPress内のコアREST APIエンドポイントはすべてコントローラークラスを使用して実装されています。
このページの残りの部分では、自分自身のコントローラークラスを書く方法と、その利点について説明します。
コントローラー
コントローラーは入力(WordPress REST APIの場合はWP_REST_Request
オブジェクト)を受け取り、WP_REST_Response
オブジェクトとして応答出力を生成します。例としてコントローラークラスを見てみましょう:
class My_REST_Posts_Controller {
// Here initialize our namespace and resource name.
public function __construct() {
$this->namespace = '/my-namespace/v1';
$this->resource_name = 'posts';
}
// Register our routes.
public function register_routes() {
register_rest_route( $this->namespace, '/' . $this->resource_name, array(
// Here we register the readable endpoint for collections.
array(
'methods' => 'GET',
'callback' => array( $this, 'get_items' ),
'permission_callback' => array( $this, 'get_items_permissions_check' ),
),
// Register our schema callback.
'schema' => array( $this, 'get_item_schema' ),
) );
register_rest_route( $this->namespace, '/' . $this->resource_name . '/(?P<id>[\d]+)', array(
// Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request.
array(
'methods' => 'GET',
'callback' => array( $this, 'get_item' ),
'permission_callback' => array( $this, 'get_item_permissions_check' ),
),
// Register our schema callback.
'schema' => array( $this, 'get_item_schema' ),
) );
}
/**
* Check permissions for the posts.
*
* @param WP_REST_Request $request Current request.
*/
public function get_items_permissions_check( $request ) {
if ( ! current_user_can( 'read' ) ) {
return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) );
}
return true;
}
/**
* Grabs the five most recent posts and outputs them as a rest response.
*
* @param WP_REST_Request $request Current request.
*/
public function get_items( $request ) {
$args = array(
'post_per_page' => 5,
);
$posts = get_posts( $args );
$data = array();
if ( empty( $posts ) ) {
return rest_ensure_response( $data );
}
foreach ( $posts as $post ) {
$response = $this->prepare_item_for_response( $post, $request );
$data[] = $this->prepare_response_for_collection( $response );
}
// Return all of our comment response data.
return rest_ensure_response( $data );
}
/**
* Check permissions for the posts.
*
* @param WP_REST_Request $request Current request.
*/
public function get_item_permissions_check( $request ) {
if ( ! current_user_can( 'read' ) ) {
return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), array( 'status' => $this->authorization_status_code() ) );
}
return true;
}
/**
* Gets post data of requested post id and outputs it as a rest response.
*
* @param WP_REST_Request $request Current request.
*/
public function get_item( $request ) {
$id = (int) $request['id'];
$post = get_post( $id );
if ( empty( $post ) ) {
return rest_ensure_response( array() );
}
$response = $this->prepare_item_for_response( $post, $request );
// Return all of our post response data.
return $response;
}
/**
* Matches the post data to the schema we want.
*
* @param WP_Post $post The comment object whose response is being prepared.
*/
public function prepare_item_for_response( $post, $request ) {
$post_data = array();
$schema = $this->get_item_schema( $request );
// We are also renaming the fields to more understandable names.
if ( isset( $schema['properties']['id'] ) ) {
$post_data['id'] = (int) $post->ID;
}
if ( isset( $schema['properties']['content'] ) ) {
$post_data['content'] = apply_filters( 'the_content', $post->post_content, $post );
}
return rest_ensure_response( $post_data );
}
/**
* Prepare a response for inserting into a collection of responses.
*
* This is copied 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.
*/
public function prepare_response_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;
}
/**
* Get our sample schema for a post.
*
* @return array The sample schema for a post
*/
public function get_item_schema() {
if ( $this->schema ) {
// Since WordPress 5.3, the schema can be cached in the $schema property.
return $this->schema;
}
$this->schema = array(
// This tells the spec of JSON Schema we are using which is draft 4.
'$schema' => 'http://json-schema.org/draft-04/schema#',
// The title property marks the identity of the resource.
'title' => 'post',
'type' => 'object',
// In JSON Schema you can specify object properties in the properties attribute.
'properties' => array(
'id' => array(
'description' => esc_html__( 'Unique identifier for the object.', 'my-textdomain' ),
'type' => 'integer',
'context' => array( 'view', 'edit', 'embed' ),
'readonly' => true,
),
'content' => array(
'description' => esc_html__( 'The content for the object.', 'my-textdomain' ),
'type' => 'string',
),
),
);
return $this->schema;
}
// Sets up the proper HTTP status code for authorization.
public function authorization_status_code() {
$status = 401;
if ( is_user_logged_in() ) {
$status = 403;
}
return $status;
}
}
// Function to register our new routes from the controller.
function prefix_register_my_rest_routes() {
$controller = new My_REST_Posts_Controller();
$controller->register_routes();
}
add_action( 'rest_api_init', 'prefix_register_my_rest_routes' );
クラスの利点
このクラスには、単純な関数を使用して書いたかもしれないすべての同じコンポーネントが含まれています。クラスの構造は、$this->method_name()
構文を使用して関連するメソッドを参照する便利な方法を提供しますが、名前空間とは異なり、クラスは値をキャッシュし、ロジックを共有することも許可します。
get_item_schema
メソッドでは、生成されたスキーマを$this->schema
としてクラスに保存することに注意してください。クラスプロパティは、この種の生成された値をキャッシュするのを容易にします。WordPress 5.3でのスキーマキャッシングの導入により、いくつかのコアREST APIコレクション応答の速度が最大40%向上したため、自分のコントローラーでもこのパターンに従うことを検討すべきです。
クラスの継承 & WP_REST_Controller
上記で、クラスがグローバル関数のカプセル化の問題をどのように解決するか、クラスインスタンスが複雑な値をキャッシュして応答処理を高速化する方法を見てきました。クラスのもう一つの大きな利点は、クラスの継承が複数のエンドポイント間でロジックを共有できる方法です。
ここでの例のクラスは、基底クラスを拡張していませんが、WordPressコア内のすべてのエンドポイントコントローラーは、abstract
コントローラークラスであるWP_REST_Controller
を拡張します。このクラスを拡張することで、次のような便利なメソッドにアクセスできますが、これに限定されません:
prepare_response_for_collection()
:コレクションに挿入するための応答を準備します。add_additional_fields_to_object()
:準備した応答オブジェクトに登録されたRESTフィールドを追加します。get_fields_for_response()
:_fields
クエリパラメータを検査して、要求された応答フィールドを特定します。get_context_param()
:context
パラメータを取得します。filter_response_by_context()
:提供されたコンテキストパラメータに基づいて応答の形状をフィルタリングします。get_collection_params()
:コレクションエンドポイントに役立つ基本的なパラメータ定義のセットを返します。
get_item
、register_routes
、update_item_permissions_check
のようなエンドポイント固有のメソッドは、抽象クラスによって完全には実装されておらず、自分のクラスで定義する必要があります。
このコントローラーのメソッドの完全なリストについては、WP_REST_Controller
クラスリファレンスページを訪れてください。
重要な点は、WP_REST_Controller
がabstract
クラスとして実装されており、複数のクラスで明確に必要なロジックのみを含んでいることです。継承は、拡張する基底クラスにクラスを結びつけ、考慮が不十分な継承ツリーはエンドポイントの保守を非常に困難にする可能性があります。
例えば、投稿エンドポイントのためのコントローラークラスを作成し(上記の例のように)、カスタム投稿タイプもサポートしたい場合、My_REST_Posts_Controller
をこのように拡張してはいけません:class My_CPT_REST_Controller extends My_REST_Posts_Controller
。代わりに、共有ロジックのためにまったく別の基底コントローラークラスを作成するか、My_REST_Posts_Controller
がすべての利用可能な投稿タイプを処理するようにするべきです。エンドポイントロジックはビジネス要件の変更の影響を受けるため、基底投稿コントローラーを更新するたびに無関係な複数のコントローラーを変更する必要がないようにしたいです。
ほとんどの場合、各エンドポイントコントローラーが実装または拡張できるinterface
またはabstract class
として基底コントローラークラスを作成するか、コアのWordPress RESTクラスのいずれかを直接拡張することをお勧めします。
内部WordPress REST APIクラス
WordPress REST APIは、その内部クラスのために意図的なデザインパターンに従い、インフラストラクチャまたはエンドポイントクラスとして分類できます。
エンドポイントクラスは、WordPressリソースに対してCRUD操作を実行するために必要な機能的ロジックをカプセル化します。WordPressは多くのREST APIエンドポイント(WP_REST_Posts_Controller
など)を公開していますが、上記で説明したように、すべてのエンドポイントは共通の基底コントローラークラスから拡張されます:
WP_REST_Controller
:すべてのWordPressコアエンドポイントの基底クラス。このクラスは、WordPressリソースを操作するための一貫したパターンを表すように設計されています。WP_REST_Controller
を実装するエンドポイントと対話する際、HTTPクライアントは各エンドポイントが一貫した方法で動作することを期待できます。
インフラストラクチャクラスは、エンドポイントクラスをサポートします。データ変換を行うことなく、WordPress REST APIのロジックを処理します。WordPress REST APIは、3つの主要なインフラストラクチャクラスを実装しています:
WP_REST_Server
:WordPress REST APIの主要なコントローラー。ルートはWordPress内でサーバーに登録されます。WP_REST_Server
がリクエストを処理するために呼び出されると、どのルートが呼び出されるかを決定し、ルートコールバックにWP_REST_Request
オブジェクトを渡します。WP_REST_Server
は認証も処理し、リクエストの検証や権限チェックを行うことができます。WP_REST_Request
:リクエストの性質を表すオブジェクト。このオブジェクトには、リクエストヘッダー、パラメータ、メソッド、ルートなどのリクエストの詳細が含まれています。また、リクエストの検証やサニタイズも行うことができます。WP_REST_Response
:応答の性質を表すオブジェクト。このクラスはWP_HTTP_Response
を拡張し、ヘッダー、ボディ、ステータスを含み、リンクメディアを追加するためのadd_link()
や、クエリナビゲーションヘッダーを取得するためのquery_navigation_headers()
のようなヘルパーメソッドを提供します。
API駆動型アプリケーションのほとんどのタイプでは、インフラストラクチャ層を直接拡張または操作する必要はありませんが、自分自身のREST APIエンドポイントを実装している場合、アプリケーションはWP_REST_Controller
を拡張する1つ以上のエンドポイントコントローラークラスから利益を得る可能性があります。