diff --git a/src/wp-includes/rest-api/class-wp-rest-server.php b/src/wp-includes/rest-api/class-wp-rest-server.php index 140e14122d81c..63d010c9d8e6e 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -129,6 +129,14 @@ class WP_REST_Server { */ protected $embed_cache = array(); + /** + * Cached route maps keyed by namespace. + * + * @since 6.9.0 + * @var array + */ + protected $route_map_cache = array(); + /** * Stores request objects that are currently being handled. * @@ -973,6 +981,9 @@ public function register_route( $route_namespace, $route, $route_args, $override } else { $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args ); } + + // Invalidate the route map cache when routes change. + $this->route_map_cache = array(); } /** @@ -1004,6 +1015,12 @@ public function register_route( $route_namespace, $route, $route_args, $override * @phpstan-return array> */ public function get_routes( $route_namespace = '' ) { + $cache_key = $route_namespace ? $route_namespace : ''; + + if ( isset( $this->route_map_cache[ $cache_key ] ) ) { + return $this->route_map_cache[ $cache_key ]; + } + $endpoints = $this->endpoints; if ( $route_namespace ) { @@ -1087,6 +1104,8 @@ public function get_routes( $route_namespace = '' ) { */ /** @phpstan-var array> $endpoints */ + $this->route_map_cache[ $cache_key ] = $endpoints; + return $endpoints; } diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 82a53b8191a0c..96c959464b662 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -2080,6 +2080,87 @@ public function test_get_routes_no_namespace_overriding() { $this->assertSame( 204, $response->get_status(), '/test-ns/v1/test' ); } + /** + * @ticket 39473 + * @covers WP_REST_Server::get_routes + */ + public function test_get_routes_caches_result() { + $filter_count = 0; + $counting_filter = static function ( $endpoints ) use ( &$filter_count ) { + ++$filter_count; + return $endpoints; + }; + + add_filter( 'rest_endpoints', $counting_filter ); + + $server = rest_get_server(); + $server->get_routes(); + $server->get_routes(); + $server->get_routes(); + + remove_filter( 'rest_endpoints', $counting_filter ); + + $this->assertSame( 1, $filter_count, 'The rest_endpoints filter should only fire once when get_routes() is called multiple times.' ); + } + + /** + * @ticket 39473 + * @covers WP_REST_Server::get_routes + */ + public function test_get_routes_cache_is_keyed_by_namespace() { + $server = rest_get_server(); + + $all_routes = $server->get_routes(); + $namespaced_routes = $server->get_routes( 'oembed/1.0' ); + + $this->assertNotEquals( $all_routes, $namespaced_routes, 'Cached routes for different namespaces should differ.' ); + + foreach ( $namespaced_routes as $route => $handlers ) { + $this->assertStringStartsWith( '/oembed/1.0', $route ); + } + } + + /** + * @ticket 39473 + * @covers WP_REST_Server::register_route + * @covers WP_REST_Server::get_routes + */ + public function test_get_routes_cache_invalidated_on_register() { + $server = rest_get_server(); + + $routes_before = $server->get_routes(); + + register_rest_route( + 'test-cache-ns', + '/test-cache', + array( + 'methods' => array( 'GET' ), + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + + $routes_after = $server->get_routes(); + + $this->assertArrayNotHasKey( '/test-cache-ns/test-cache', $routes_before ); + $this->assertArrayHasKey( '/test-cache-ns/test-cache', $routes_after ); + } + + /** + * @ticket 39473 + * @covers WP_REST_Server::get_routes + */ + public function test_get_routes_cached_result_matches_uncached() { + $server = rest_get_server(); + + // First call populates cache. + $first_call = $server->get_routes(); + // Second call returns from cache. + $second_call = $server->get_routes(); + + $this->assertSame( $first_call, $second_call, 'Cached routes should be identical to the initial result.' ); + } + /** * @ticket 50244 */