WordPressでREST APIを使う後編 REST APIの登録と REST コントローラ

API Fetch について
WordPressのブロック開発メモ その11 REST API へのアクセス

基本的な REST API
WordPressでREST APIを使う前編
(σ・ω・)σ WordPressでREST APIを使う後編 REST APIの登録と REST コントローラ

REST API コントローラの実装
WP_REST_Controller メソッド前編
WP_REST_Controller メソッド後編 追加フィールド
WordPress REST API Controllerを実装してみる。

REST APIを公開する。

単純な例

REST APIを公開するにはregister_rest_route()を使います。

第一引数は名前空間で任意ですが名前とバージョンの組み合わせからなります。
例えば名前をkurage-plugin、バージョンを1とするとkurage-plugin/v1になります。
本家では/wordpress/v2が使われてます。

第二引数はルート名。
名前空間が/kurage-plugin/v1でルート名がkurageなら/kurage-plugin/v1/kurageでアクセスできるようになります。
本体の場合は投稿ならposts、固定ページならpagesとなってます。

第三引数にオプションを定義します。

では早速実験していきます。

add_action('rest_api_init', function(){

    register_rest_route(
        'kurage-plugin/v1',
        '/kurage',
        [
            'methods' => 'GET',
            'callback' => function( $response )
            {
                $msg = 'Hello World';
                return rest_ensure_response($msg);
            }
        ]
    );
});

JavaScript 標準のfetch()関数から呼び出す場合。

await (await fetch('/wp-json/kurage-plugin/v1/kurage')).json()

ブロックエディタ開発の環境下で呼び出す場合。

await wp.apiFetch({path: '/kurage-plugin/v1/kurage' });

結果は、

Hello World

ブラウザから実行した場合は以下のようになります。

methodsでHTTPメソッドを指定します。
GETPOST, DELETEなどが指定でき、また配列で複数指定するkともできます。

callbackにはレスポンスを返すコールバックを指定します。
この例では単純に文字列Hello Worldを返します。

戻り値はWP_REST_Response型を返すようにしますが、
rest_ensure_response()を使うと変換してくれます。
すでに値がWP_REST_Responseであればそのまま返すなどの配慮がされてます、

リクエストとHTTPメソッドの定数

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        'methods' => WP_REST_Server::READABLE,
        'callback' => function(WP_REST_Request $request)
        {
            $p1 = $request->get_param('p1');
            $p2 = $request->get_param('p2');

            $msg = "Hello {$p1}, {$p2}!";
            return rest_ensure_response($msg);
        }
    ]
);

以下を実行

await wp.apiFetch({
    path: '/kurage-plugin/v1/kurage?p1=ikasan&p2=takosan'
});

結果

Hello ikasan, takosan!

HTTPメソッドはあらかじめクラス定数で用意してあります。
WP_REST_Server::READABLEGETが設定してたります。
他にも以下の定数が定義してあります。

  • WP_REST_Server::CREATABLEPOST
  • WP_REST_Server::EDITABLEPOST, PUT, PATCH
  • WP_REST_Server::DELETEDELETE

callbackで受け取る引数はWP_REST_Requestクラスのインスタンスです。
WP_REST_Requestget_param()や連想配列としてクエリを取得出来ます。

正規表現

URLの一部に正規表現を使います。

register_rest_route(
    'kurage-plugin/v1',
    '/kurage/(\d+)',
    [
        'methods' => WP_REST_Server::READABLE,
        'callback' => function(WP_REST_Request $r)
        {
            $msg = 'Hello Kurage!';
            return rest_ensure_response($msg);
        },
    ]
);
await wp.apiFetch({path: '/kurage-plugin/v1/kurage/123' });
await wp.apiFetch({path: '/kurage-plugin/v1/kurage/12x3' });

123だと成功しますが、12x3だと失敗します。

正規表現にマッチした部分を取得する

register_rest_route(
    'kurage-plugin/v1',
    '/kurage/(?P<id>\d+)',
    [
        'methods' => WP_REST_Server::READABLE,
        'callback' => function(WP_REST_Request $r)
        {
            $id = $r->get_param('id');
            $msg = "ID: {$id}!";
            return rest_ensure_response($msg);
        },
    ]
);
await wp.apiFetch({path: '/kurage-plugin/v1/kurage/123' });
ID: 123!

POSTを送る

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        'methods' => WP_REST_Server::CREATABLE,
        'callback' => function(WP_REST_Request $request)
        {
            $p1 = $request->get_param('p1');
            $p2 = $request->get_param('p2');

            $msg = "Hello {$p1}, {$p2}!";
            return rest_ensure_response($msg);
        }
    ]
);
await wp.apiFetch({
    path: '/kurage-plugin/v1/kurage'
     method: 'POST',
     data: { p1: 'ikasan', p2: 'takosan' }
});

HTTPメソッドをPOSTにする場合はmethodsPOSTにします。

複数のHTTPメソッドに対応する

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        [
            'methods' => WP_REST_Server::READABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello READ {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            }
        ],

        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello CREATE {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            }
        ]
    ]
);

GETPOSTの二つ分定義しました。

パーミッション

アクセスに制限を設ける

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        [
            'methods' => WP_REST_Server::READABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello READ {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            },
        ],

        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello CREATE {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            },
            'permission_callback' => fn() => current_user_can('dummy_manage_options'),
        ],

    ]
);

GETはそのままアクセスできますが、
POSTはエラーになります。

権限についてはここでは触れません。

current_user_can('dummy_manage_options')

通常はdummy_manage_optionsという権限は存在しないのでエラーになります。

検証

validate_callhackを設定することで値を検証出来ます。
エラーが一つ見つかった時点で終了するのか、複数のエラーを纏めるのかは実装によりますがこの例ではエラーが一つ見つかった時点で終了します。

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello CREATE {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            },

            // 管理者のみOK
            'permission_callback' => fn() => current_user_can('manage_options'),

            // 検証
            'validate_callback' => function($r)
            {
                $p1 = $r->get_param('p1');
                $p2 = $r->get_param('p2');

                if(strlen($p1) < 10)
                {
                    return new WP_Error('kurage_p1_error', 'p1 は10文字以上で!');
                }

                if(strlen($p2) < 10)
                {
                    return new WP_Error('kurage_p1_error', 'p1 は10文字以上で!');
                }

                return true;
            }

        ],

    ]
);

以下のリクエストを行います。

await wp.apiFetch({path: '/kurage-plugin/v1/kurage', method: 'POST', data: { p1: 'ikasan', p2: 'takosan'} });

この場合p1p2がともに条件を満たしません。

複数エラーがあるときはどうしよう?
WP_Errorだけで工夫してみた(もっとましな方法が用意してあるのかもしれません)

'validate_callback' => function($r)
{
    $p1 = $r->get_param('p1');
    $p2 = $r->get_param('p2');

    $error = new WP_Error('kurage_errors', 'エラー一覧');

    if(strlen($p1) < 10)
    {
        $error->add('kurage_p1_error', 'p1 は10文字以上で!');
    }

    if(strlen($p2) < 10)
    {
        $error->add('kurage_p1_error', 'p1 は10文字以上で!');
    }

    // コンストラクタをnewした時点でエラーが一つ存在する
    return count($error->get_error_codes()) > 1 ? $error : true;
}

argsを使った検証

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello CREATE {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            },

            // 管理者のみOK
            'permission_callback' => fn() => current_user_can('manage_options'),

            // 検証
            'args' =>
            [
                'p1' =>
                [
                    'description' => 'P1 parameter',
                    'type' => 'string',
                    'minLength' => 10,
                    'maxLength' => 20
                ],
                'p2' =>
                [
                    'description' => 'P2 parameter',
                    'type' => 'string',
                    'enum' => ['takohachi', '8chan', 'takosuke']
                ]
            ]

        ],

    ]
);

argsの設定で検証出来ます。

descriptionは単にフィールドの説明、
typeは型情報で、例えばstringに数値を渡したり、逆にintegerに文字列を渡すとエラーになります。
minLengthは文字列が指定文字数未満だとエラーになります。
maxLengthは文字列が指定文字数を超えるとエラーになります。
enumはそのリストのな海外も単語であればエラーになります。

argで検証

argsvalidate_callbackを使用することもできます。
受け取る引数が違う点に注意してください。

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello CREATE {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            },

            'args' =>
            [
                'p1' => 
                [
                    'description' => 'P1 parameter',
                    'type' => 'string',
                    'validate_callback' => function($value, $request, $key)
                    {
                        return strlen($value) < 10 ?
                            new WP_Error('kurage_p1', '10文字以上記入してください') : true;
                    },
                ],
                'p2' => 
                [
                    'description' => 'P2 parameter',
                    'type' => 'string',
                    'validate_callback' => function($value, $request, $key)
                    {
                        return strlen($value) < 10 ?
                            new WP_Error('kurage_p2', '10文字以上記入してください') : true;
                    },
                ]
            ]
        ],

    ]
);
await wp.apiFetch({path: '/kurage-plugin/v1/kurage', method: 'POST', data: { p1: 'ikasan', p2: 'takosan'} });

第一引数が受け取った値、第二引数がWP_REST_Request, 第三引数がフィールド名(今回はp1p2)を受け取ります。

argsでサニタイズ

argssanitize_callbackにサニタイズ用のコールバックを指定します。

register_rest_route(
    'kurage-plugin/v1',
    '/kurage',
    [
        [
            'methods' => WP_REST_Server::CREATABLE,
            'callback' => function(WP_REST_Request $request)
            {
                $p1 = $request->get_param('p1');
                $p2 = $request->get_param('p2');

                $msg = "Hello CREATE {$p1}, {$p2}!";
                return rest_ensure_response($msg);
            },

            'args' =>
            [
                'p1' => 
                [
                    'description' => 'P1 parameter',
                    'type' => 'string',
                    'validate_callback' => function($value, $request, $key)
                    {
                        return strlen($value) < 10 ?
                            new WP_Error('kurage_p1', '10文字以上記入してください') : true;
                    },

                    'sanitize_callback' => function($value, $request, $param)
                    {
                        return sanitize_text_field($value);
                    }
                ],
                'p2' => 
                [
                    'description' => 'P2 parameter',
                    'type' => 'string',
                    'validate_callback' => function($value, $request, $key)
                    {
                        return strlen($value) < 10 ?
                            new WP_Error('kurage_p2', '10文字以上記入してください') : true;
                    },

                    'sanitize_callback' => function($value, $request, $param)
                    {
                        return sanitize_text_field($value);
                    }
                ]
            ]
        ],

    ]
);
await wp.apiFetch({
    path: '/kurage-plugin/v1/kurage',
    method: 'POST',
    data: {
        p1: 'ikasan <script>alert("attack")</script>',
        p2: 'takosan <b>8chan</b>'}
    }
);

Hello CREATE ikasan, takosan 8chan!

結果を見るとタグが消滅していることが確認出来ます。

REST Controllerのクラス図

まずはWP_REST_Controllerの手抜きクラス図です。

ググっても解説がほとんどなかったのでソースコードを読んでみました。

大まかには、CRUD系のメソッド、それのパーミッションチェック用のメソッド(CRUDと対)、
REST APIが返すデータの設定、追加フィールドの処理、スキーマやフィルターなどのメソッドが存在します。

登録

register_routes()

register_rest_route()を使ってREST APIの登録を行います。

CRUD

これらのメソッドでアイテムの取得、アイテム一覧の取得、アイテムの新規作成、アイテムの更新、アイテムの削除を行います。
それとセットでパーミッションをチェックするメソッドがあります。

アイテム一覧取得

get_items( $request )
get_items_permissions_check( $request )

対象IDのアイテムの取得

get_item( $request )
get_item_permissions_check( $request )

アイテムの新規作成

create_item( $request )
create_item_permissions_check( $request )

対象IDのアイテムの更新

update_item( $request )
update_item_permissions_check( $request )

対象IDのアイテムの削除

delete_item( $request )
delete_item_permissions_check( $request )

これらは抽象メソッドにはなっておらず、実装は全てWP_Errorインスタンスを返すだけとなっております。
派生クラスではこれらをオーバーライドしCRUD及びそのパーミッションチェックを実装します。

WP_REST_Posts_Controllerの実装を見てもらえれば分かりますが、
register_rest_route()ではこれらのメソッドとルートをマッピングします。
HTTPメソッドとルート(ID有り無しの2通り)の組合せでCRUDします。

[GET] /wp/v2/posts

get_items()でアイテム一覧を取得します。

[POST] /wp/v2/posts

create_item()でアイテムを新規作成します。

[GET] /posts/(?P[\d]+)

get_item()対象IDのアイテムを一つ取得します。

[POST, PUT, PATCH] /posts/(?P[\d]+)

update_item()で対象IDのアイテムを編集します。

[DELETE] /posts/(?P[\d]+)

delete_item()で対象IDのアイテムを削除します。

アイテム系

リクエストから更新用オブジェクトの作成

prepare_item_for_database( $request )

派生クラスでオーバーライドします。

リクエスト(WP_REST_Request)からアイテムの更新用オブジェクト(stdClass)を作成します。
create_item()update_item()から使用します。
追加や更新に必要な値だけプロパティを追加します。

リクエストからtitlemessageを受け取りアイテムのオブジェクトを返す単純な例。

public function prepare_item_for_database( $request )
{
    $o = new stdClass();
    $o->title = $request->get_param('title');
    $o->message = $request->get_param('message');

    return $o;
}

実際はスキーマと照らし合わせて設定するようです。

アイテムとリクエストからレスポンスを作成

prepare_item_for_response( $item, $request )

派生クラスでオーバーライドします。

アイテムオブジェクト(投稿ならWP_Post)とリクエスト(WP_REST_Request)からレスポンス(WP_REST_Response)を返します。
この値がREST APIの結果になります。

取得系からだけでなく、更新系からも使用します。
例えばREST APIで削除や追加更新を実行したらその時点でのアイテムを結果として取得出来るのはこのためです。

public function prepare_item_for_response( $item, $request ){
    $data = [];

    $data['title'] = $item->title;
    $data['message'] = $item->message;

    // 追加フィールドを実装する場合はこのメソッドで`get_callback`を実行させる
    $data = $this->add_additional_fields_to_object( $data, $request );

    // コンテキストによるフィルタリングを行う
    $data = $this->filter_response_by_context( $data, $context );

    // レスポンス化
    $res = rest_ensure_response($data);

    // 必要であれば $res->add_link(...) 等でリンク追加

    return $res;
}

追加フィールドやフィルタリングを考慮すると結構複雑な箇所でもあります。

リンクの追加

prepare_response_for_collection( $response )

レスポンスのデータを配列化し、リンク(_links)を追加して返します。
get_items()内で使用される。

以下は動作の実験です。

// RESTコントローラのインスタンス作成
$controller = new class extends WP_REST_Controller
{

};

// データの作成
$obj = new stdClass();
$obj->myName = 'Kurage';
$obj->myMessage = 'Hello Kurage!';

// レスポンス化とリンクの追加(rest_ensure_response($obj)を使ってもいい)
$response = new WP_REST_Response($obj);
$response->add_link('posts', 'https://kurage/wp-json/wp/v2/posts');
$response->add_link('pages', 'https://kurage/wp-json/wp/v2/pages');

// メソッドにレスポンスを渡す
$data = $controller->prepare_response_for_collection($response);

// さてどうなる?
print_r($data);
Array
(
    [myName] => Kurage
    [myMessage] => Hello Kurage!
    [_links] => Array
        (
            [posts] => Array
                (
                    [0] => Array
                        (
                            [href] => https://kurage/wp-json/wp/v2/posts
                        )

                )

            [pages] => Array
                (
                    [0] => Array
                        (
                            [href] => https://kurage/wp-json/wp/v2/pages
                        )

                )

        )

)

戻り値は配列化されている点に注意です。

内部では引数で渡したレスポンス(WP_REST_Response)からデータ($response->get_data())を取得し配列化、
そのデータに加工されたリンク(_links)を追加して返します。
リンクは同じくレスポンスの$response->get_links()から取得されます。

以下は一般的なget_items()からの利用のされ方です。

$data = [];
foreach($items as $item)
{
    $res = $this->prepare_item_for_response($item);
    $data[] = $this->prepare_response_for_collection($res);
}

return rest_ensure_response($data);

ほとんどの派生クラスで実装してあるのでソースコードを呼んだ方が早いと思います。

スキーマ系

ここでいうスキーマというのはJSONスキーマのことです。
値の型や詳細、検証情報などをJSONで定義したもので、WordPressの範囲外のことです。
PHPに限定されるものでもありません。
なので適当にJSONスキーマを先にググってみてください。

スキーマを取得

get_item_schema()

オーバーライドします。
スキーマを取得します。
結構な頻度で呼ばれるので通常はキャッシュして使うようです。

以下はJSONファイルにスキーマを記述し、REST Controllerのこのメソッドから取得する例です。
普通はPHPでJSONスキーマを記述しますが、JSONファイルを別に作ったほうがIDEのインテリセンスが効いたり便利なのでそちらの方向でいきます。

JSONファイルにJSONスキーマを記述

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "kurage",
    "type": "object",
    "properties": {
        "nickname": {
            "description": "Your Nickname",
            "type": "string",
            "context": [ "view" ]
        },
        "age": {
            "description": "Your Age",
            "type": "integer",
            "context": [ "edit" ]
        }
    }
}

propertiesに任意のプロパティnicknameageを追加しました。
contextはワードプレス側から使用されます。

PHP側の例

$controller = new class extends WP_REST_Controller
{
    public function get_item_schema()
    {
        $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
        return $this->add_additional_fields_schema($schema);
    }

    protected function createSingletonSchema()
    {
        $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
        return json_decode($json, true);
    }
};

print_r($controller->get_item_schema());

結果

Array
(
    [$schema] => http://json-schema.org/draft-04/schema#
    [title] => kurage
    [type] => object
    [properties] => Array
        (
            [nickname] => Array
                (
                    [description] => Your Nickname
                    [type] => string
                    [context] => Array
                        (
                            [0] => view
                        )
                )

            [age] => Array
                (
                    [description] => Your Age
                    [type] => integer
                    [context] => Array
                        (
                            [0] => edit
                        )
                )
        )
)

コードの説明。

protected function createSingletonSchema()
{
    $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
    return json_decode($json, true);
}

PHP側でJSONを構築してもいいのですが、どうせならJSONファイルを別に作ったほうが
VSCodeのインテリセンスも効くし楽なのでそうしました。
作ったJSONファイルはfile_get_contents()で読み込み、json_decode()でPHPの配列に変換します。

public function get_item_schema()
{
    $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
    return $this->add_additional_fields_schema($schema);
}

WP_REST_Controller::get_item_schema()をオーバーライドします。
WP_REST_Controllerにはアクセス修飾子がprotected$schemaプロパティがあります。

スキーマが設定されてなければ作成し、すでに作成されていればそれを使用します。
一度作成したスキーマはリサイクルします。
ちなみにadd_additional_fields_schema($schema)を使って追加フィールドのスキーマをマージします。

オブジェクトのタイプ

$controller = new class extends WP_REST_Controller
{
    public function get_item_schema()
    {
        $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
        return $this->add_additional_fields_schema($schema);
    }

    protected function createSingletonSchema()
    {
        $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
        return json_decode($json, true);
    }

    public function showObjectType()
    {
        echo $this->get_object_type();
    }
};

$schema = $controller->showObjectType();

結果

kurage

これはJSONスキーマのtitleの項目を取得します。

パブリックスキーマ

get_public_item_schema()

スキーマのpropertiesの格設定からarg_optionsを省いて返します。
arg_optionsはのちに出てきます。

コンテキストを取得

get_context_param( $args )
$controller = new class extends WP_REST_Controller
{
    public function get_item_schema()
    {
        $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
        return $this->add_additional_fields_schema($schema);
    }

    protected function createSingletonSchema()
    {
        $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
        return json_decode($json, true);
    }
};

$contexts = $controller->get_context_param();
Array
(
    [description] => このリクエストが作成されたスコープ。レスポンスに含まれるフィールドはスコープにより異なります。
    [type] => string
    [sanitize_callback] => sanitize_key
    [validate_callback] => rest_validate_request_arg
    [enum] => Array
        (
            [0] => view
            [1] => edit
        )

)

enumvieweditが追加されています。
どういうことかというと、JSONスキーマの格プロパティで設定したcontextをかき集めてます。

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "kurage",
    "type": "object",
    "properties": {
        "nickname": {
            "description": "Your Nickname",
            "type": "string",
            "context": [ "view" ]
        },
        "age": {
            "description": "Your Age",
            "type": "integer",
            "context": [ "edit" ]
        }
    }
}

今回の例では、nicknameviewが、ageeditが指定してあるので
それぞれの値vieweditの二つが取得されました。

もしJSONスキーマで以下のような設定をしているとします。

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "kurage",
    "type": "object",
    "properties": {
        "nickname": {
            "description": "Your Nickname",
            "type": "string",
            "context": [ "apple", "banana", "lemon" ]
        },
        "age": {
            "description": "Your Age",
            "type": "integer",
            "context": [ "edit" ]
        }
    }
}

結果は以下のようになります。

Array
(
    [description] => このリクエストが作成されたスコープ。レスポンスに含まれるフィールドはスコープにより異なります。
    [type] => string
    [sanitize_callback] => sanitize_key
    [validate_callback] => rest_validate_request_arg
    [enum] => Array
        (
            [0] => lemon
            [1] => edit
            [2] => banana
            [3] => apple
        )

)

viewがなくなり、変わってlemon, banana, appleが出てきました。
もちろんこれら果物の単語は勝手に付けたのでコンテキストとして意味がありません。

ひな形

get_collection_params()

register_rest_route()argsで使用する値のひな形を返します。

$controller = new class extends WP_REST_Controller
{
    public function get_item_schema()
    {
        $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
        return $this->add_additional_fields_schema($schema);
    }

    protected function createSingletonSchema()
    {
        $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
        return json_decode($json, true);
    }
};

$p = $controller->get_collection_params();
Array
(
    [context] => Array
        (
            [description] => このリクエストが作成されたスコープ。レスポンスに含まれるフィールドはスコープにより異なります。
            [type] => string
            [sanitize_callback] => sanitize_key
            [validate_callback] => rest_validate_request_arg
            [enum] => Array
                (
                    [0] => view
                    [1] => edit
                )

        )

    [page] => Array
        (
            [description] => コレクションの現在のページ。
            [type] => integer
            [default] => 1
            [sanitize_callback] => absint
            [validate_callback] => rest_validate_request_arg
            [minimum] => 1
        )

    [per_page] => Array
        (
            [description] => 結果として返される項目の最大数。
            [type] => integer
            [default] => 10
            [minimum] => 1
            [maximum] => 100
            [sanitize_callback] => absint
            [validate_callback] => rest_validate_request_arg
        )

    [search] => Array
        (
            [description] => 文字列に一致するものに結果を限定します。
            [type] => string
            [sanitize_callback] => sanitize_text_field
            [validate_callback] => rest_validate_request_arg
        )

)

page, per_page, search及び、get_context_param()の戻り値を持つcontextが設定されたひな形的なものを返してるだけですね。

オーバーライドした派生側から呼び出すようです。

public function get_collection_params()
{
    // 派生側から基底のメソッドを呼び出す。
    $query_params = parent::get_collection_params();

    // ...

}

以下はWP_REST_Posts_Controllerから抜粋したget_collection_params()get_endpoint_args_for_item_schema()の典型的な使い方です。

register_rest_route(
    $this->namespace,
    '/' . $this->rest_base,
    array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_items' ),
            'permission_callback' => array( $this, 'get_items_permissions_check' ),
            'args'                => $this->get_collection_params(),
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => array( $this, 'create_item' ),
            'permission_callback' => array( $this, 'create_item_permissions_check' ),
            'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
        ),
        'allow_batch' => $this->allow_batch,
        'schema'      => array( $this, 'get_public_item_schema' ),
    )
);

GETargsで使用されてます。
POSTargsはまた後で出てきます。

コンテキストによるフィルタリング

filter_response_by_context( $data, $context )

第一引数のデータを、スキーマに照らし合わせ、フィルタリングします。

内部ではrest_filter_response_by_context()を呼び出してます。
主にprepare_item_for_response()内で使用されます。

実際にはスキーマとコンテキストの二つからアイテムのデータをフィルタリングします。

$controller = new class extends WP_REST_Controller
{
    public function get_item_schema()
    {
        $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
        return $this->add_additional_fields_schema($schema);
    }

    protected function createSingletonSchema()
    {
        $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
        return json_decode($json, true);
    }
};

$viewContext = $controller->filter_response_by_context(
    ['nickname' => 'Kurako', 'age' => 123],
    'view'
);

$editContext = $controller->filter_response_by_context(
    ['nickname' => 'Kurako', 'age' => 123],
    'edit'
);

print_r($viewContext);
print_r($editContext);

結果。

Array
(
    [nickname] => Kurako
)
Array
(
    [age] => 123
)

どういうことかというと、JSONスキーマを見てください。

{
    "$schema": "http://json-schema.org/draft-04/schema#",
    "title": "kurage",
    "type": "object",
    "properties": {
        "nickname": {
            "description": "Your Nickname",
            "type": "string",
            "context": [ "view" ]
        },
        "age": {
            "description": "Your Age",
            "type": "integer",
            "context": [ "edit" ]
        }
    }
}

そのうえで第二引数がviewの場合を見ていきます。

$viewContext = $controller->filter_response_by_context(
    ['nickname' => 'Kurako', 'age' => 123],
    'view'
);

第一引数の連想配列のキーを、スキーマのpropertiesで設定したプロパティと照らし合わせます。
プロパティのcontextのリスト中に、第二引数のviewが含まれないものは除外されます。

viewnicknameには含まれますが、ageには含まれないのでageは除外されます。

逆にeditを指定するとageには含まれますがnicknameには含まれないので'nickname`は除外されます。

例えば、

/wp/v2/kurage?_context=view

上記のURLがあったとすると、
アイテムのデータからcontextviewを含まないageが外されて取得されるようにすることが出来ます。
詳しくはprepare_item_for_response()の項を見てください。

スキーマ系

スキーマをargsに適用

get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE )

スキーマから、スキーマで予言されているキーワードに含まれる設定をregister_rest_route()argsで使用するよう変換します。
内部でrest_get_endpoint_args_for_schema()を呼び出してます。

$controller = new class extends WP_REST_Controller
{
    public function get_item_schema()
    {
        $schema = $this->schema ?? $this->schema = $this->createSingletonSchema();
        return $this->add_additional_fields_schema($schema);
    }

    protected function createSingletonSchema()
    {
        $json = file_get_contents(dirname(__FILE__) . '/kurage-worker-ex-schema.json');
        return json_decode($json, true);
    }
};

$p = $controller->get_endpoint_args_for_item_schema();

$controller->get_endpoint_args_for_item_schema();の戻り値は以下のようになります。

Array
(
    [nickname] => Array
        (
            [validate_callback] => rest_validate_request_arg
            [sanitize_callback] => rest_sanitize_request_arg
            [description] => Your Nickname
            [type] => string
        )

    [age] => Array
        (
            [validate_callback] => rest_validate_request_arg
            [sanitize_callback] => rest_sanitize_request_arg
            [description] => Your Age
            [type] => integer
        )

)

以下はWP_REST_Posts_Controllerから抜粋したget_collection_params()get_endpoint_args_for_item_schema()の典型的な使い方です。

register_rest_route(
    $this->namespace,
    '/' . $this->rest_base,
    array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_items' ),
            'permission_callback' => array( $this, 'get_items_permissions_check' ),
            'args'                => $this->get_collection_params(),
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => array( $this, 'create_item' ),
            'permission_callback' => array( $this, 'create_item_permissions_check' ),
            'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
        ),
        'allow_batch' => $this->allow_batch,
        'schema'      => array( $this, 'get_public_item_schema' ),
    )
);

もう少しスキーマについて深堀していきます。

rest_get_allowed_schema_keywords()

以下の関数はスキーマの有効なキーワード一覧を取得します。

rest_get_allowed_schema_keywords()
title
description
default
type
format
enum
items
properties
additionalProperties
patternProperties
minProperties
maxProperties
minimum
maximum
exclusiveMinimum
exclusiveMaximum
multipleOf
minLength
maxLength
pattern
minItems
maxItems
uniqueItems
anyOf
oneOf

rest_get_endpoint_args_for_schema()

get_endpoint_args_for_item_schema()は内部的にこのメソッドを呼び出しています。

スキーマで設定したプロパティ中からrest_get_allowed_schema_keywords()のリストに含まれるプロパティを列挙します。
また以下の二つのプロパティが初期値として設定されます。

validate_callbackrest_validate_request_arg
sanitize_callbackrest_sanitize_request_arg

$schema = [
    '$schema' => 'http://json-schema.org/draft-04/schema#',
    'title' => 'kurage',
    'type' => 'object',
    'properties' => [
        'p1' => [
            'description' => 'P 1 param',
            'type' => ['string', 'null'],
            'enum' => ['a', 'b', 'c'],
            'context' => ['view', 'edit', 'default'],
            'xxx' => 'X',
            'yyy' => 'Y',
            'zzz' => 'Z'

        ],
        'p2' => [

        ]
    ]

];

$args = rest_get_endpoint_args_for_schema(
    $schema,
    'POST'
);

p1についてみていきましょう。

validate_callback及びsanitize_callbackが自動的に追加されます。
またdescription及びtype, enumはリストに含まれるので追加されます。
ただしcontext及びxxx, yyy, zzzはリストに含まれないので追加されません。

p2についてはvalidate_callbacksanitize_callbackが追加されていだけだと確認できます。

arg_options

スキーマプロパティーの中にarg_optionsが入っていると特殊な動きをします。
arg_optionsの一覧ががマージされます。

$schema = [
    '$schema' => 'http://json-schema.org/draft-04/schema#',
    'title' => 'kurage',
    'type' => 'object',
    'properties' => [
        'p1' => [
            'description' => 'P 1 param',
            'type' => ['string', 'null'],
            'enum' => ['a', 'b', 'c'],
            'context' => ['view', 'edit', 'default'],
            'xxx' => 'X',
            'yyy' => 'Y',
            'zzz' => 'Z',
            'arg_options' => [
                'maxLength' => 100,
                'minLength' => 20,
                'www' => 123
            ]

        ],
        'p2' => [

        ]
    ]

];

$args = rest_get_endpoint_args_for_schema(
    $schema,
    'POST'
);

p1を見るとarg_optionsで定義したものはリストに関係なくマージされます。
maxLength及びminLengthはリストに含まれますが、wwwは含まれません。
すべてマージされていることが確認出来ます。

ただし、すべてマージ出来るのは第二引数がPOSTの時のみで、
それ以外だとrequireddefaultが無効になります。

これを踏まえて簡単なコントローラを実装してみます。

$controller = new class extends WP_REST_Controller
{
    public function register_routes()
    {
        $args = $this->get_endpoint_args_for_item_schema('POST');

        register_rest_route(
            '/kurage-plugin/v1',
            '/inoshishi',
            [
                [
                    'methods' => 'POST',
                    'callback' => function($request)
                    {
                        return 'xxx';
                    },
                    'args' => $args
                ]
            ]
        );

    }

    public function get_item_schema()
    {
        return [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'kurage',
            'type' => 'object',
            'properties' => [
                'p1' => [
                    'description' => 'P 1 param',
                    'type' => ['string', 'null'],
                    'enum' => ['a', 'b', 'c'],
                    'context' => ['view', 'edit', 'default'],
                    'xxx' => 'X',
                    'yyy' => 'Y',
                    'zzz' => 'Z',
                    'arg_options' => [
                        'maxLength' => 100,
                        'minLength' => 20,
                        'www' => 123,
                        'required' => true,
                        'default' => 'Hello'
                    ]

                ],
                'p2' => [

                ]
            ]
        ];
    }

};

$controller->register_routes();
await wp.apiFetch({
    path:'/kurage-plugin/v1/inoshishi',
    method: 'POST',
    data: { p1: '123', p2: 'b' }
})
p1 は最低20文字以上である必要があります。

とりあえarg_optionsminLengthが機能していたことがわかります。

register_rest_route()のschemaパラメータ

register_rest_route()のパラメータのschemaはどんな状況で呼び出されるのでしょう?
この値が使用されるタイミングを調べるのめっちゃ苦労したのですが・・・。

WP_REST_Posts_Controller::register_routes()での設定を参考にします。

register_rest_route(
    $this->namespace,
    '/' . $this->rest_base,
    array(
        array(
            'methods'             => WP_REST_Server::READABLE,
            'callback'            => array( $this, 'get_items' ),
            'permission_callback' => array( $this, 'get_items_permissions_check' ),
            'args'                => $this->get_collection_params(),
        ),
        array(
            'methods'             => WP_REST_Server::CREATABLE,
            'callback'            => array( $this, 'create_item' ),
            'permission_callback' => array( $this, 'create_item_permissions_check' ),
            'args'                => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ),
        ),
        'allow_batch' => $this->allow_batch,
        'schema'      => array( $this, 'get_public_item_schema' ),
    )
);

schemaの項目はWP_REST_Controller::get_public_item_schema()を参照するように設定されています。
ではそのメソッドの実装を見てみます。

public function get_public_item_schema() {

    $schema = $this->get_item_schema();

    if ( ! empty( $schema['properties'] ) ) {
        foreach ( $schema['properties'] as &$property ) {
            unset( $property['arg_options'] );
        }
    }

    return $schema;
}

このメソッドはスキーマのプロパティからarg_optionsを省くだけの単純なメソッドです。
このメソッドにブレークポイントを張って、REST APIからた叩いても引っかからなかったんですね。

await wp.apiFetch({ path:'/wp/v2/posts' })

一体どの場面で呼び出されているのだろうと。

単純でした。
特定の投稿の編集ページを開いたら引っかかりました。

どうやらREST APIで投稿(/wp/v2/posts)では引っかからず、
管理画面からpost.phpにアクセスすると引っかかりました。

REST APIを直接たたいた時と、投稿の編集ページでは違いがあるんですね。
コールスタック(REST APIを叩くときはWP_REST_Posts_Controller::get_item()あたりに張る)を見るとある特徴がわかりました。

共通点はWP_REST_Server::dispatch()でして、
ここではrest_pre_dispatchフィルターが実行されます。
フィルタにはrest_handle_options_request()関数が登録してあります。

この関数は通常は何もせず戻り値を返すだけですが、
特定の条件ででWP_REST_Server::get_data_for_route()を実行します。

// 第三引数はコンテキストで help を渡してます
$data = $handler->get_data_for_route( $route, $endpoints, 'help' );

特定の条件とはHTTPメソッドがOPTIONSで、
なぜだかわかりませんがトップレベルにコールバックを指定してないことです。

このことを踏まえもう一度REST APIを叩いてみましょう。

await wp.apiFetch({path:'/wp/v2/posts', method: 'OPTIONS' })

今度は引っかかりました。
そして結果はというと・・・。

get_item_schema()のスキーマからarg_optionsが消された結果が返ってきました。

なるほど、HTTPメソッドをOPTIONSにした時に取得するスキーマの設定なんですね。

コア開発者以外知る必要ないんでしょうがblock_editor_rest_api_preload()関数を見ると、単にwp.apiFetchにキャッシュさせるために呼び出されているようです。

このようにOPTIONSで取得したスキーマはOPTIONSに配置されています。

ブラウザを見るとここで取得されたデータがJSONシリアライズされて埋め込まれていることがわかります。

以下は注意点です。

register_rest_route(
    $this->namespace,
    '/' . $this->rest_base,
    [

        // この場所でコールバックを指定したら`schema`は意味なし
        // 'callback' => ...,

        'allow_batch' => $this->allow_batch,
        'schema'      => array( $this, 'get_public_item_schema' ),
    ]
);

追加フィールド系

追加フィールドの実装はだいぶ複雑なので飛ばしてください。
追加フィールドの実装自体コア開発者が実装するもので、一般の開発者が知っていてもいみなさそう。
私のようなコードを理解しないと気が済まない人ようです。

get_additional_fields()

追加フィールドを取得します。
第一引数はオブジェクトタイプ(今回はkurage)を渡しますが、スキーマを設定している場合はそちらから取得されるので省略できます。

register_rest_field(
    'kurage',
    'ikasan',
    [
        'get_callback' => function()
        {
            return '墨はくどー';
        },
        'update_callback' => function($value)
        {
            $v = $value;
        }
    ]
);

register_rest_field(
    'kurage',
    'takosan',
    [
        'get_callback' => function()
        {
            return 'タコ殴りだべー!';
        },
        'update_callback' => function($value)
        {
            $v = $value;
        }
    ]
);

$controller = new class extends WP_REST_Controller
{
    public function register_routes()
    {
        $args = $this->get_endpoint_args_for_item_schema('POST');

        register_rest_route(
            '/kurage-plugin/v1',
            '/kurage',
            [
                [
                    'methods' => 'POST',
                    'callback' => function($request)
                    {
                        return 'xxx';
                    },
                    'args' => $args
                ]
            ]
        );
    }

    public function myExec()
    {
        // 第一引数を指定しなかったらスキーマのtitleがしようされる。
        $fields = $this->get_additional_fields();

        echo $fields;
    }

    public function get_item_schema()
    {
        return [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'kurage',
            'type' => 'object',
            'properties' => [
                'p1' => [
                    'description' => 'P 1 param',
                    'type' => ['string', 'null']
                ],
                'p2' => [
                    'description' => 'P 2 param',
                    'type' => ['string', 'null'],
                ]
            ]
        ];
    }

};

$controller->register_routes();
$controller->myExec();

$fields = $this->get_additional_fields();

王ジェクトタイプがkurageの追加フィールドの情報takosanikasanを取得出来ました。

また追加フィールドの登録は

$args = $this->get_endpoint_args_for_item_schema('POST');

を呼び出す前に設定されていないといろいろまずいことがあります。
追加プロパティをマージする際にコードが三すくみになってしまいます。

add_additional_fields_schema( $schema )

スキーマプロパティに追加フィールドのスキーマをマージします。

register_rest_field(
    'kurage',
    'ikasan',
    [
        'get_callback' => function()
        {
            return '墨はくどー';
        },
        'update_callback' => function($value)
        {
            $v = $value;
        },

        // ここ注目
        'schema' => [
            'description' => 'Ika value',
            'required' => true,
            'type' => ['string', 'null']
        ]
    ]
);

register_rest_field(
    'kurage',
    'takosan',
    [
        'get_callback' => function()
        {
            return 'タコ殴りだべー!';
        },
        'update_callback' => function($value)
        {
            $v = $value;
        },

        // ここ注目
        'schema' => [
            'description' => 'Tako value',
            'required' => true,
            'type' => ['string', 'null']
        ]
    ]
);

$controller = new class extends WP_REST_Controller
{
    public function register_routes()
    {
        $args = $this->get_endpoint_args_for_item_schema('POST');

        /**
         * 
         *  この部分で追加フィールドの登録を行ってはいけない。
         *  上のget_endpoint_args_for_item_schema()は内部get_item_schema()を取得します。
         *  ところがget_item_schema()で追加フィールドのスキーマをマージするコードを書いていると、
         *  追加フィールドを追加する前に追加フィールドのスキーマを取得するという矛盾が生まれます。
         * 
         */

        register_rest_route(
            '/kurage-plugin/v1',
            '/kurage',
            [
                [
                    'methods' => 'POST',
                    'callback' => function($request)
                    {
                        return rest_ensure_response( 'xxx' );
                    },
                    'permission_callback' => fn() => true,
                    'args' => $args
                ]
            ]
        );
    }

    public function get_item_schema()
    {
        $schema = [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'kurage',
            'type' => 'object',
            'properties' => [
                'p1' => [
                    'description' => 'P 1 param',
                    'type' => ['string', 'null']
                ],
                'p2' => [
                    'description' => 'P 2 param',
                    'type' => ['string', 'null'],
                ]
            ]
        ];

        $newSchema = $this->add_additional_fields_schema($schema);

        return $newSchema;
    }

};

$controller->register_routes();
$schema = $controller->get_item_schema();

get_item_schema()を見てください。
この最後あたりにブレークポイントを張って$newSchemaの中身を見てみます。

ちなみに以下はvar_dump()で得られたスキーマを吐いた結果です。

array(4) {
  ["$schema"]=>
  string(39) "http://json-schema.org/draft-04/schema#"
  ["title"]=>
  string(6) "kurage"
  ["type"]=>
  string(6) "object"
  ["properties"]=>
  array(4) {
    ["p1"]=>
    array(2) {
      ["description"]=>
      string(9) "P 1 param"
      ["type"]=>
      array(2) {
        [0]=>
        string(6) "string"
        [1]=>
        string(4) "null"
      }
    }
    ["p2"]=>
    array(2) {
      ["description"]=>
      string(9) "P 2 param"
      ["type"]=>
      array(2) {
        [0]=>
        string(6) "string"
        [1]=>
        string(4) "null"
      }
    }
    ["ikasan"]=>
    array(3) {
      ["description"]=>
      string(9) "Ika value"
      ["required"]=>
      bool(true)
      ["type"]=>
      array(2) {
        [0]=>
        string(6) "string"
        [1]=>
        string(4) "null"
      }
    }
    ["takosan"]=>
    array(3) {
      ["description"]=>
      string(10) "Tako value"
      ["required"]=>
      bool(true)
      ["type"]=>
      array(2) {
        [0]=>
        string(6) "string"
        [1]=>
        string(4) "null"
      }
    }
  }
}

kurage本来のプロパティはp1p2だけですが、
add_additional_fields_schema()にそのスキーマを渡すとkurageの追加フィールドのikasantakosanがマージされているのがわかります。

ikasantakosanについてはregister_rest_field()のオプションのschema項目を見てください。

ikasantakosanrequiredtrueなので必須です。
以下がエラーになるのが確認できます。

await wp.apiFetch({
    path:'/kurage-plugin/v1/kurage',
    method: 'POST',
    data: { p1: '123', p2: 'b' }
})

まずikasantakosanが設定されてないとエラーが出たら期待した結果です。

以下は成功します。

await wp.apiFetch({
    path:'/kurage-plugin/v1/kurage',
    method: 'POST',
    data: {
        p1: '123',
        p2: 'b',
        takosan: 'takotako',
        ikasan: 'ikaika'
    }
})

add_additional_fields_to_object( $prepared, $request )

アイテムを取得する際に追加フィールドの値を設定するためのメソッドです。
基本的にprepare_item_for_response()内で実行します。

register_rest_field(
    'kurage',
    'ikasan',
    [
        'get_callback' => function()
        {
            return '墨はくどー';
        },
        'update_callback' => function($value)
        {
            $v = $value;
        },
        'schema' => [
            'description' => 'Ika value',
            'required' => true,
            'type' => ['string', 'null']
        ]
    ]
);

register_rest_field(
    'kurage',
    'takosan',
    [
        'get_callback' => function()
        {
            return 'タコ殴りだべー!';
        },
        'update_callback' => function($value)
        {
            $v = $value;
        },
        'schema' => [
            'description' => 'Tako value',
            'required' => true,
            'type' => ['string', 'null']
        ]
    ]
);

$controller = new class extends WP_REST_Controller
{
    public function register_routes()
    {
        $args = $this->get_endpoint_args_for_item_schema('POST');

        register_rest_route(
            '/kurage-plugin/v1',
            '/kurage',
            [
                [
                    'methods' => 'POST',
                    'callback' => function($request)
                    {
                        return rest_ensure_response( 'xxx' );
                    },
                    'permission_callback' => fn() => true,
                    'args' => $args
                ]
            ]
        );

        // [GET] /kurage-plugin/kurage/(?P<id>\d+) を追加
        register_rest_route(
            '/kurage-plugin/v1',
            '/kurage/(?P<id>\d+)',
            [
                [
                    'methods' => 'GET',
                    'callback' => [$this, 'get_item']
                ],

                'args' => [
                    'description' => 'kurage ID',
                    'type' => 'integer'
                ]
            ]
        );
    }

    public function get_item($request)
    {
        $id = $request->get_param('id');

        // DBからデータを取ったつもり
        $o = new stdClass();
        $o->p1 = 'p1 value';
        $o->p2 = 'p2 value';

        // アイテムの前準備
        $data = $this->prepare_item_for_response($o, $request);

        return rest_ensure_response($data);
    }

    public function prepare_item_for_response($item, $request)
    {
        // アイテムを配列に戻す
        $data = [];

        $data['p1'] = $item->p1;
        $data['p2'] = $item->p2;

        // 追加フィールドの値を追加
        $data = $this->add_additional_fields_to_object($data, $request);

        return rest_ensure_response($data);
    }

    public function get_item_schema()
    {
        $schema = [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'kurage',
            'type' => 'object',
            'properties' => [
                'p1' => [
                    'description' => 'P 1 param',
                    'type' => ['string', 'null']
                ],
                'p2' => [
                    'description' => 'P 2 param',
                    'type' => ['string', 'null'],
                ]
            ]
        ];

        $newSchema = $this->add_additional_fields_schema($schema);

        return $newSchema;
    }

};

$controller->register_routes();
await wp.apiFetch({path:'/kurage-plugin/v1/kurage/123' })

kurageアイテムの結果(p1, p2)に
追加プロパティ(ikasan, takosan)がマージされていることが確認できます。

これら追加プロパティの値はregister_rest_field()で設定したget_callbackコールバックを呼び出します。

$data = $this->add_additional_fields_to_object($data, $request);

protected function update_additional_fields_for_object( $object, $request )

create_item()update_item()から呼び出します。
今回はcreate_item()だけ実装。

$database = [];

function setDb($key, $value)
{
    global $database;
    $database[$key] = $value;
}

function getDb($key)
{
    global $database;
    return $database[$key];
}

register_rest_field(
    'kurage',
    'ikasan',
    [
        'get_callback' => function()
        {
            return getDb('ikasan');
        },
        'update_callback' => function($value)
        {
            setDb('ikasan', $value);
        },
        'schema' => [
            'description' => 'Ika value',
            'required' => true,
            'type' => ['string', 'null']
        ]
    ]
);

register_rest_field(
    'kurage',
    'takosan',
    [
        'get_callback' => function()
        {
            return getDb('takosan');
        },
        'update_callback' => function($value)
        {
            setDb('takosan', $value);
        },
        'schema' => [
            'description' => 'Tako value',
            'required' => true,
            'type' => ['string', 'null']
        ]
    ]
);

$controller = new class extends WP_REST_Controller
{
    public function register_routes()
    {
        $args = $this->get_endpoint_args_for_item_schema('POST');

        register_rest_route(
            '/kurage-plugin/v1',
            '/kurage',
            [
                [
                    'methods' => 'POST',
                    'callback' => [$this, 'create_item'],
                    'permission_callback' => fn() => true,
                    'args' => $args
                ]
            ]
        );

    }

    public function create_item( $request )
    {
        // アイテムのオブジェクトを取得
        $kurage = $this->prepare_item_for_database($request);
        if( is_wp_error($kurage) )
        {
            return $kurage;
        }

        // $kurageをDBに保存

        // 登録した`update_callback`を実行
        $r = $this->update_additional_fields_for_object($kurage, $request);
        if( is_wp_error($r) )
        {
            return $r;
        }

        // レスポンス作成
        $response = $this->prepare_item_for_response($kurage, $request);
        $response = rest_ensure_response($response);

        $response->set_status(201);

        return $response;
    }

    protected function prepare_item_for_database($request)
    {
        $o = new stdClass;
        $o->p1 = $request->get_param('p1');
        $o->p2 = $request->get_param('p2');
        return $o;
    }

    public function prepare_item_for_response($item, $request)
    {
        // アイテムを配列に戻す
        $data = [];

        $data['p1'] = $item->p1;
        $data['p2'] = $item->p2;

        // 追加フィールドの値を追加
        $data = $this->add_additional_fields_to_object($data, $request);

        return rest_ensure_response($data);
    }

    public function get_item_schema()
    {
        $schema = [
            '$schema' => 'http://json-schema.org/draft-04/schema#',
            'title' => 'kurage',
            'type' => 'object',
            'properties' => [
                'p1' => [
                    'description' => 'P 1 param',
                    'type' => ['string', 'null']
                ],
                'p2' => [
                    'description' => 'P 2 param',
                    'type' => ['string', 'null'],
                ]
            ]
        ];

        $newSchema = $this->add_additional_fields_schema($schema);

        return $newSchema;
    }

};

$controller->register_routes();
await wp.apiFetch({
    path:'/kurage-plugin/v1/kurage',
    method: 'POST',
    data: {
        p1: '123',
        p2: 'b',
        takosan: 'takotako',
        ikasan: 'ikaika'
    }
})

以下の例はkurageの追加フィールド(ikasan及びtakosan)のupdate_callbackを実行します。

$r = $this->update_additional_fields_for_object($kurage, $request);

今回は永続化をイメージするためsetDb()getD()を用意しました。
もちろん永続化されるわけもなく、また特定のアイテムのIDに紐づけられるものでもありません。
あくまでこのメソッドの動作を確認するためだけのコードです。

その他

  • sanitize_slug( $slug )

内部でsanitize_title()を実行して返します。

BlockEditor certificate css DataGrid Docker Gutenberg Hyper-V iframe MUI openssl PHP React ReduxToolkit REST ubuntu WordPress オレオレ認証局 フレームワーク