Skip to content

Commit a4dd08f

Browse files
committed
REST API: Cache route maps in WP_REST_Server::get_routes().
`get_routes()` is called multiple times per request (route matching, schema generation, OPTIONS handling). The result is deterministic between route registrations, so this commit memoizes the per-namespace map on the server instance and busts the cache whenever `register_route()` mutates `$this->endpoints`. Adds tests covering: * `rest_endpoints` filter fires once across repeated calls. * Cache keyed by namespace. * Cache invalidation when a new route is registered. * Cached and uncached results are identical. Note for reviewers: caching the post-`rest_endpoints`-filter result means filters added after the first call to `get_routes()` no longer take effect for that request. This matches the perf goal but is a documented filter-contract change worth weighing. See #39473.
1 parent baa9f53 commit a4dd08f

2 files changed

Lines changed: 100 additions & 0 deletions

File tree

src/wp-includes/rest-api/class-wp-rest-server.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,14 @@ class WP_REST_Server {
8787
*/
8888
protected $embed_cache = array();
8989

90+
/**
91+
* Cached route maps keyed by namespace.
92+
*
93+
* @since 6.9.0
94+
* @var array
95+
*/
96+
protected $route_map_cache = array();
97+
9098
/**
9199
* Stores request objects that are currently being handled.
92100
*
@@ -924,6 +932,9 @@ public function register_route( $route_namespace, $route, $route_args, $override
924932
} else {
925933
$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
926934
}
935+
936+
// Invalidate the route map cache when routes change.
937+
$this->route_map_cache = array();
927938
}
928939

929940
/**
@@ -949,6 +960,12 @@ public function register_route( $route_namespace, $route, $route_args, $override
949960
* `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
950961
*/
951962
public function get_routes( $route_namespace = '' ) {
963+
$cache_key = $route_namespace ? $route_namespace : '';
964+
965+
if ( isset( $this->route_map_cache[ $cache_key ] ) ) {
966+
return $this->route_map_cache[ $cache_key ];
967+
}
968+
952969
$endpoints = $this->endpoints;
953970

954971
if ( $route_namespace ) {
@@ -1016,6 +1033,8 @@ public function get_routes( $route_namespace = '' ) {
10161033
}
10171034
}
10181035

1036+
$this->route_map_cache[ $cache_key ] = $endpoints;
1037+
10191038
return $endpoints;
10201039
}
10211040

tests/phpunit/tests/rest-api/rest-server.php

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2012,6 +2012,87 @@ public function test_get_routes_no_namespace_overriding() {
20122012
$this->assertSame( 204, $response->get_status(), '/test-ns/v1/test' );
20132013
}
20142014

2015+
/**
2016+
* @ticket 39473
2017+
* @covers WP_REST_Server::get_routes
2018+
*/
2019+
public function test_get_routes_caches_result() {
2020+
$filter_count = 0;
2021+
$counting_filter = static function ( $endpoints ) use ( &$filter_count ) {
2022+
++$filter_count;
2023+
return $endpoints;
2024+
};
2025+
2026+
add_filter( 'rest_endpoints', $counting_filter );
2027+
2028+
$server = rest_get_server();
2029+
$server->get_routes();
2030+
$server->get_routes();
2031+
$server->get_routes();
2032+
2033+
remove_filter( 'rest_endpoints', $counting_filter );
2034+
2035+
$this->assertSame( 1, $filter_count, 'The rest_endpoints filter should only fire once when get_routes() is called multiple times.' );
2036+
}
2037+
2038+
/**
2039+
* @ticket 39473
2040+
* @covers WP_REST_Server::get_routes
2041+
*/
2042+
public function test_get_routes_cache_is_keyed_by_namespace() {
2043+
$server = rest_get_server();
2044+
2045+
$all_routes = $server->get_routes();
2046+
$namespaced_routes = $server->get_routes( 'oembed/1.0' );
2047+
2048+
$this->assertNotEquals( $all_routes, $namespaced_routes, 'Cached routes for different namespaces should differ.' );
2049+
2050+
foreach ( $namespaced_routes as $route => $handlers ) {
2051+
$this->assertStringStartsWith( '/oembed/1.0', $route );
2052+
}
2053+
}
2054+
2055+
/**
2056+
* @ticket 39473
2057+
* @covers WP_REST_Server::register_route
2058+
* @covers WP_REST_Server::get_routes
2059+
*/
2060+
public function test_get_routes_cache_invalidated_on_register() {
2061+
$server = rest_get_server();
2062+
2063+
$routes_before = $server->get_routes();
2064+
2065+
register_rest_route(
2066+
'test-cache-ns',
2067+
'/test-cache',
2068+
array(
2069+
'methods' => array( 'GET' ),
2070+
'callback' => '__return_true',
2071+
'permission_callback' => '__return_true',
2072+
)
2073+
);
2074+
2075+
$routes_after = $server->get_routes();
2076+
2077+
$this->assertArrayNotHasKey( '/test-cache-ns/test-cache', $routes_before );
2078+
$this->assertArrayHasKey( '/test-cache-ns/test-cache', $routes_after );
2079+
}
2080+
2081+
/**
2082+
* @ticket 39473
2083+
* @covers WP_REST_Server::get_routes
2084+
*/
2085+
public function test_get_routes_cached_result_matches_uncached() {
2086+
$server = rest_get_server();
2087+
2088+
// First call populates cache.
2089+
$first_call = $server->get_routes();
2090+
// Second call returns from cache.
2091+
$second_call = $server->get_routes();
2092+
2093+
$this->assertSame( $first_call, $second_call, 'Cached routes should be identical to the initial result.' );
2094+
}
2095+
20152096
/**
20162097
* @ticket 50244
20172098
*/

0 commit comments

Comments
 (0)