スキーマ

2020.03.25 2020.03.25

TOPICS

概要

スキーマとは、データがどのように構造化されているかを示すメタデータのことです。
ほとんどのデータベースは、より構造化された方法でデータを推論できるように、何らかのスキーマを実装しています。WordPress REST API は JSON スキーマを利用してデータの構造化を処理します。
スキーマを使わずにエンドポイントを実装することもできますが、多くのことを見逃してしまいます。何が一番適しているかはあなた次第です。

JSONスキーマ

まず、JSONについて少しお話ししましょう。JSONとは、JavaScriptのオブジェクトに似た人間が読めるデータ形式です。JSONはJavaScript Object Notationの略です。
JSONの人気は爆発的に高まっており、データ構造の世界を席巻しているようです。WordPress REST API は JSON スキーマとして知られる JSON の特別な仕様を使用しています。
JSON Schemaについて詳しく知りたい方は、JSON Schemaのサイトと、このわかりやすいJSON Schemaの紹介をご覧ください。
スキーマは、テストの改善、発見性の向上、全体的な構造の改善など、多くの利点をもたらしてくれます。JSON データの塊を見てみましょう。

{
    "shouldBeArray": 'LOL definitely not an array',
    "shouldBeInteger": ['lolz', 'you', 'need', 'schema'],
    "shouldBeString": 123456789
}

JSON パーサーはそのデータを問題なく通過し、それが有効な JSON であるため、何も文句を言いません。
クライアントとサーバーはデータや期待されるものについて何も知らず、ただJSONを見ているだけです。
スキーマを実装することで、実際にコードベースを単純化することができます。スキーマはデータの構造をより良くするのに役立ち、アプリケーションが WordPress REST API とのインタラクションについてより簡単に推論できるようになります。
WordPress REST API はスキーマの使用を強制するものではありませんが、スキーマの使用を推奨しています。スキーマデータをAPIに組み込むには、リソースのスキーマと登録された引数のスキーマの2つの方法があります。

リソーススキーマ

リソースのスキーマは特定のオブジェクトにどのようなフィールドが存在するかを示します。ルートを登録する際には、ルートのリソーススキーマを指定することもできます。単純なコメントスキーマが JSON スキーマの PHP 表現でどのように見えるか見てみましょう。

// Register our routes.
function prefix_register_my_comment_route() {
    register_rest_route( 'my-namespace/v1', '/comments', array(
        // Notice how we are registering multiple endpoints the 'schema' equates to an OPTIONS request.
        array(
            'methods'  => 'GET',
            'callback' => 'prefix_get_comment_sample',
        ),
        // Register our schema callback.
        'schema' => 'prefix_get_comment_schema',
    ) );
}
 
add_action( 'rest_api_init', 'prefix_register_my_comment_route' );
 
/**
 * Grabs the five most recent comments and outputs them as a rest response.
 *
 * @param WP_REST_Request $request Current request.
 */
function prefix_get_comment_sample( $request ) {
    $args = array(
        'number' => 5,
    );
    $comments = get_comments( $args );
 
    $data = array();
 
    if ( empty( $comments ) ) {
        return rest_ensure_response( $data );
    }
 
    foreach ( $comments as $comment ) {
        $response = prefix_rest_prepare_comment( $comment, $request );
        $data[] = prefix_prepare_for_collection( $response );
    }
 
    // Return all of our comment response data.
    return rest_ensure_response( $data );
}
 
/**
 * Matches the comment data to the schema we want.
 *
 * @param WP_Comment $comment The comment object whose response is being prepared.
 */
function prefix_rest_prepare_comment( $comment, $request ) {
    $comment_data = array();
 
    $schema = prefix_get_comment_schema();
 
    // We are also renaming the fields to more understandable names.
    if ( isset( $schema['properties']['id'] ) ) {
        $comment_data['id'] = (int) $comment->comment_ID;
    }
 
    if ( isset( $schema['properties']['author'] ) ) {
        $comment_data['author'] = (int) $comment->user_id;
    }
 
    if ( isset( $schema['properties']['content'] ) ) {
        $comment_data['content'] = apply_filters( 'comment_text', $comment->comment_content, $comment );
    }
 
    return rest_ensure_response( $comment_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.
 */
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;
}
 
/**
 * Get our sample schema for comments.
 */
function prefix_get_comment_schema() {
    $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'                => 'comment',
        '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,
            ),
            'author' => array(
                'description'  => esc_html__( 'The id of the user object, if author was a user.', 'my-textdomain' ),
                'type'         => 'integer',
            ),
            'content' => array(
                'description'  => esc_html__( 'The content for the object.', 'my-textdomain' ),
                'type'         => 'string',
            ),
        ),
    );
 
    return $schema;
}

お気づきのように、各コメントリソースが指定したスキーマにマッチするようになりました。この切り替えは prefix_rest_prepare_comment() で行いました。
リソースのスキーマを作成し、OPTIONSリクエストを行うことで、このスキーマを表示することができるようになりました。
なぜこれが役に立つのでしょうか?他の言語、例えば JavaScript にデータを解釈させ、エンドポイントからのデータを検証させたい場合、JavaScript はデータがどのように構造化されているかを知る必要があります。
スキーマを提供することで、他の制作者や自分自身が一貫した方法でエンドポイントの上に構築できるようになります。

スキーマは機械で読めるデータを提供するので、潜在的にJSONを読むことができるものは何でも、どんなデータを見ているのかを理解することができます。
https://ourawesomesite.com/wp-json/GET リクエストをして API インデックスを見ると、API のスキーマが返されるので、他の人が私たちのデータを解釈するためのクライアントライブラリを書けるようになります。
スキーマデータを読み込むこのプロセスはディスカバリーと呼ばれています。リソースにスキーマを提供すると、そのルートへの OPTIONS リクエストを介してそのリソースを発見可能にします。
リソーススキーマの公開はスキーマパズルの一部に過ぎません。登録された引数にもスキーマを使いたいと思います。

引数のスキーマ

エンドポイントのリクエスト引数を登録する際に、JSON Schemaを使って引数が何であるべきかというデータを提供してくれることもあります。
これにより、エンドポイントの拡張に合わせて再利用できる検証ライブラリを書くことができます。スキーマは前もっての作業が多くなりますが、今後長く使用する本番アプリケーションを書こうとしているのであれば、間違いなくスキーマの使用を検討すべきです。引数スキーマとバリデーションを使用する例を見てみましょう。

// Register our routes.
function prefix_register_my_arg_route() {
    register_rest_route( 'my-namespace/v1', '/schema-arg', array(
        // Here we register our endpoint.
        array(
            'methods'  => 'GET',
            'callback' => 'prefix_get_item',
            'args' => prefix_get_endpoint_args(),
        ),
    ) );
}
 
// Hook registration into 'rest_api_init' hook.
add_action( 'rest_api_init', 'prefix_register_my_arg_route' );
 
/**
 * Returns the request argument `my-arg` as a rest response.
 *
 * @param WP_REST_Request $request Current request.
 */
function prefix_get_item( $request ) {
    // If we didn't use required in the schema this would throw an error when my arg is not set.
    return rest_ensure_response( $request['my-arg'] );
}
 
/**
 * Get the argument schema for this example endpoint.
 */
function prefix_get_endpoint_args() {
    $args = array();
 
    // Here we add our PHP representation of JSON Schema.
    $args['my-arg'] = array(
        'description'       => esc_html__( 'This is the argument our endpoint returns.', 'my-textdomain' ),
        'type'              => 'string',
        'validate_callback' => 'prefix_validate_my_arg',
        'sanitize_callback' => 'prefix_sanitize_my_arg',
        'required'          => true,
    );
 
    return $args;
}
 
/**
 * Our validation callback for `my-arg` parameter.
 *
 * @param mixed           $value   Value of the my-arg parameter.
 * @param WP_REST_Request $request Current request object.
 * @param string          $param   The name of the parameter in this case, 'my-arg'.
 */
function prefix_validate_my_arg( $value, $request, $param ) {
    $attributes = $request->get_attributes();
 
    if ( isset( $attributes['args'][ $param ] ) ) {
        $argument = $attributes['args'][ $param ];
        // Check to make sure our argument is a string.
        if ( 'string' === $argument['type'] && ! is_string( $value ) ) {
            return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%1$s is not of type %2$s', 'my-textdomain' ), $param, 'string' ), array( 'status' => 400 ) );
        }
    } else {
        // This code won't execute because we have specified this argument as required.
        // If we reused this validation callback and did not have required args then this would fire.
        return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%s was not registered as a request argument.', 'my-textdomain' ), $param ), array( 'status' => 400 ) );
    }
 
    // If we got this far then the data is valid.
    return true;
}
 
/**
 * Our santization callback for `my-arg` parameter.
 *
 * @param mixed           $value   Value of the my-arg parameter.
 * @param WP_REST_Request $request Current request object.
 * @param string          $param   The name of the parameter in this case, 'my-arg'.
 */
function prefix_sanitize_my_arg( $value, $request, $param ) {
    $attributes = $request->get_attributes();
 
    if ( isset( $attributes['args'][ $param ] ) ) {
        $argument = $attributes['args'][ $param ];
        // Check to make sure our argument is a string.
        if ( 'string' === $argument['type'] ) {
            return sanitize_text_field( $value );
        }
    } else {
        // This code won't execute because we have specified this argument as required.
        // If we reused this validation callback and did not have required args then this would fire.
        return new WP_Error( 'rest_invalid_param', sprintf( esc_html__( '%s was not registered as a request argument.', 'my-textdomain' ), $param ), array( 'status' => 400 ) );
    }
 
    // If we got this far then something went wrong don't use user input.
    return new WP_Error( 'rest_api_sad', esc_html__( 'Something went terribly wrong.', 'my-textdomain' ), array( 'status' => 500 ) );
}

上の例では、'my-arg'という名前を使うことを避けています。これらの検証関数とサニタイズ関数は、スキーマを指定した文字列でなければならない他の引数にも使うことができます。
コードベースやエンドポイントが増えるにつれ、スキーマはコードの軽量化と保守性を維持するのに役立ちます。スキーマがなければ検証やサニタイズはできますが、どの関数が何を検証すべきかを追跡するのは難しくなります。
リクエスト引数にスキーマを追加することで、引数スキーマをクライアントに公開することもできるので、バリデーションライブラリをクライアントサイドで構築することができます。

スキーマを使うことに抵抗がある場合でも、引数ごとにバリデート/サニタイズコールバックを用意することは可能です。

概要

しかし、保守性があり、発見可能で、簡単に拡張可能なエンドポイントが必要な場合は、スキーマを使用することが不可欠です。スキーマは、人間とコンピュータの両方のためにエンドポイントをドキュメント化するのにも役立ちます。