From d1a4d8c05d4cc9eda3b693a7f77f77a36de4c1fe Mon Sep 17 00:00:00 2001 From: Akshat Date: Sun, 30 Aug 2026 12:56:06 +0530 Subject: [PATCH] fix(templates): nest the REST endpoint so 'schema' survives register_rest_route()'s upgrade step (C) register_rest_route() upgrades a flat args array to multiple endpoints whenever it sees a top-level 'callback' key -- wrapping the *entire* array as a single numerically-indexed entry. Passing 'schema' as a sibling of 'callback' put it inside that entry too, where core's get_data_for_route() never looks for it: OPTIONS requests still returned no schema despite get_item_schema() being fully implemented. Fix: nest the endpoint definition one level deeper and lift 'schema' out as a true route-level option, matching core's own controllers (WP_REST_Settings_Controller et al). Added a regression test that spies on the actual register_rest_route() call and asserts 'schema' is a route option (not nested inside the endpoint) and the endpoint itself is present at the numeric key -- the existing test only asserted the third arg is *an* array, which passes regardless of nesting and could not have caught this. Verified end-to-end against a real --modules rest_api,caching scaffold: composer test -> 13/13 PHPUnit tests (19 assertions, was 12/12 before the regression test), composer lint -> 15/15 files, 0 errors/0 warnings (one reserved-keyword warning from the new test's $namespace closure param, caught by the same lint run, renamed to $route_namespace before landing). --- templates/src/Rest/Rest_Controller.php | 30 ++++++++++++------- templates/tests/Unit/Rest_Controller_Test.php | 23 ++++++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/templates/src/Rest/Rest_Controller.php b/templates/src/Rest/Rest_Controller.php index aa90f83..59a659b 100644 --- a/templates/src/Rest/Rest_Controller.php +++ b/templates/src/Rest/Rest_Controller.php @@ -44,23 +44,31 @@ public function init_hooks(): void { * @return void */ public function register_routes() { + // The endpoint definition is nested one level deeper than 'schema': + // register_rest_route() checks for a top-level 'callback' key and, if + // present, wraps the *whole* array as a single numerically-indexed + // endpoint -- 'schema' included, if it were a sibling of 'callback' + // here instead of one level up. Nesting keeps 'schema' a route-level + // option core can actually find. register_rest_route( $this->namespace, '/' . $this->rest_base, array( - 'methods' => \WP_REST_Server::READABLE, - 'callback' => $this->get_items( ... ), - 'permission_callback' => $this->get_items_permissions_check( ... ), - 'args' => array( - 'param' => array( - 'required' => false, - 'sanitize_callback' => 'sanitize_text_field', - 'validate_callback' => function ( $param ) { - return is_string( $param ); - }, + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => $this->get_items( ... ), + 'permission_callback' => $this->get_items_permissions_check( ... ), + 'args' => array( + 'param' => array( + 'required' => false, + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => function ( $param ) { + return is_string( $param ); + }, + ), ), ), - 'schema' => $this->get_public_item_schema( ... ), + 'schema' => $this->get_public_item_schema( ... ), ) ); } diff --git a/templates/tests/Unit/Rest_Controller_Test.php b/templates/tests/Unit/Rest_Controller_Test.php index a133d81..b059e62 100644 --- a/templates/tests/Unit/Rest_Controller_Test.php +++ b/templates/tests/Unit/Rest_Controller_Test.php @@ -52,6 +52,29 @@ public function test_register_routes(): void { $this->assertTrue( true ); } + /** + * The schema must be a route-level option, not a key inside the endpoint + * definition -- register_rest_route() only upgrades a flat array to + * multiple endpoints when it sees a top-level 'callback' key, and + * otherwise swallows 'schema' into that same numerically-indexed entry, + * where core never looks for it (C). + */ + public function test_register_routes_exposes_schema_as_a_route_option(): void { + Functions\when( '__' )->returnArg(); + + Functions\expect( 'register_rest_route' ) + ->once() + ->andReturnUsing( + function ( $route_namespace, $route, $args ) { + $this->assertArrayHasKey( 'schema', $args, 'schema must be a route option, not inside the endpoint' ); + $this->assertArrayHasKey( 0, $args, 'the endpoint must be nested so core does not swallow schema' ); + return true; + } + ); + + ( new Rest_Controller() )->register_routes(); + } + /** * The permission callback fails closed — it gates on a capability, it does * not blanket-allow.