コントローラークラス

2020.03.26 2020.03.26

TOPICS

翻訳元記事はこちらです。

概要

新しい REST ルートを登録するには、リクエストがどのように実行されるか、パーミッションチェックがどのように適用されるか、 リソースのスキーマがどのように生成されるかといったエンドポイントの動作を制御するために、 いくつかのコールバック関数を指定する必要があります。
これらすべてのメソッドを、名前空間やクラスをラップせずに通常の PHP ファイルで宣言することも可能ですが、 その方法で宣言されたすべての関数は同じグローバルスコープ内に共存します。
get_items() のようなエンドポイントロジックに共通の関数名を使用することにしていて、 他のプラグイン (あるいは自分のプラグインの別のエンドポイント) も同じ名前の関数を登録している場合、関数 get_items() が二重に宣言されているために PHP は致命的なエラーで失敗してしまいます。

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 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;
    }
 
    /**
     * Grabs the five most recent posts and outputs them 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.
     *
     * @param WP_REST_Request $request Current request.
     */
    public function get_item_schema( $request ) {
        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 コア内のすべてのエンドポイントコントローラは、WP_REST_Controller という抽象的なコントローラクラスを拡張しています。このクラスを拡張することで、以下のような便利なメソッドにアクセスすることができます。

get_itemregister_routesupdate_item_permissions_check などのエンドポイント固有のメソッドは、抽象クラスでは完全に実装されていないため、独自のクラスで定義する必要があります。

このコントローラのメソッドの完全なリストは WP_REST_Controller クラスリファレンスページを参照ください。

WP_REST_Controller は抽象クラスとして実装されており、複数のクラスで明らかに必要とされるロジックのみが含まれていることに注意が必要です。
継承は、あなたのクラスを拡張したベースクラスに結合し、継承ツリーの考慮が不十分な場合には、エンドポイントのメンテナンスが非常に難しくなります。

例として、(上の例のような) posts エンドポイント用のコントローラクラスを作成し、 カスタム投稿タイプもサポートしたい場合は、 My_REST_Posts_Controller を次のように拡張すべきではありません。
class My_CPT_REST_Controller extends My_REST_Posts_Controller
その代わりに、共有ロジック用に完全に別のベースコントローラクラスを作成するか、 My_REST_Posts_Controller で利用可能なすべての投稿タイプを扱うようにします。
エンドポイントロジックはビジネス要件の変化に左右されますので、ベースのポストコントローラを更新するたびに無関係なコントローラをいくつも変更する必要はありません。

ほとんどの場合、エンドポイントコントローラのそれぞれが実装したり拡張したりできるインターフェイスや抽象クラスとしてベースコントローラクラスを作成するか、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() などのヘルパーメソッドを提供します。

しかし、独自の REST API エンドポイントを実装している場合、アプリケーションは WP_REST_Controller を拡張したエンドポイントコントローラクラスの恩恵を受けることになるでしょう。