diff --git a/src/wp-includes/rest-api.php b/src/wp-includes/rest-api.php index 836e0e5ec8a23..d1bc2ae76727f 100644 --- a/src/wp-includes/rest-api.php +++ b/src/wp-includes/rest-api.php @@ -155,6 +155,94 @@ function register_rest_route( $route_namespace, $route, $args = array(), $overri return true; } +/** + * Registers a REST API namespace for lazy loading. + * + * This function registers a namespace that will only load its routes when that namespace is actually requested, + * improving performance by avoiding the loading of unused REST endpoints. + * + * Note: Do not use before the {@see 'rest_api_init'} hook. + * + * @param string $route_namespace Namespace to register for lazy loading. + * Should be unique to your package/plugin and + * include a version (e.g., 'my-plugin/v1'). + * + * @return bool True on success, false on failure. + * + * @since X.X.0 + * + * @example + * // Register a lazy-loaded namespace for a plugin + * add_action( 'rest_api_init', function() { + * register_rest_namespace( 'my-plugin/v1' ); + * } ); + * + * // Then register the actual routes when the namespace loads + * add_action( 'rest_lazy_load_namespace_my-plugin/v1', function() { + * register_rest_route( 'my-plugin/v1', '/posts', array( + * 'methods' => 'GET', + * 'callback' => 'my_plugin_get_posts', + * 'permission_callback' => '__return_true', + * ) ); + * } ); + */ +function register_rest_namespace( $route_namespace ) { + if ( ! is_string( $route_namespace ) ) { + _doing_it_wrong( + __FUNCTION__, + __( 'Namespace must be a string.' ), + 'X.X.0' + ); + + return false; + } + + if ( empty( $route_namespace ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: The namespace that was passed. */ + __( 'Namespaces must be specified. Instead there seems to be an empty namespace \'%s\'.' ), + '' . $route_namespace . '' + ), + 'X.X.0' + ); + + return false; + } + + $clean_namespace = trim( $route_namespace, '/' ); + + if ( $clean_namespace !== $route_namespace ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: %s: The namespace that was passed. */ + __( 'Namespace must not start or end with a slash. Instead namespace \'%s\' seems to contain a slash.' ), + '' . $route_namespace . '' + ), + 'X.X.0' + ); + } + + if ( ! did_action( 'rest_api_init' ) ) { + _doing_it_wrong( + __FUNCTION__, + sprintf( + /* translators: 1: rest_api_init, 2: The namespace that was passed. */ + __( 'REST API namespaces must be registered on the %1$s action. Instead namespace \'%2$s\' was not registered on this action.' ), + 'rest_api_init', + '' . $route_namespace . '' + ), + 'X.X.0' + ); + } + + rest_get_server()->register_lazy_loaded_namespace( $clean_namespace ); + + return true; +} + /** * Registers a new field on an existing WordPress object type. * @@ -858,7 +946,7 @@ function rest_send_allow_header( $response, $server, $request ) { return $response; } - $routes = $server->get_routes(); + $routes = $server->get_routes( '', $server::ROUTE_CONTEXT_LOADED_ONLY ); $allowed_methods = array(); 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 8ffda57f15853..66d290098a62f 100644 --- a/src/wp-includes/rest-api/class-wp-rest-server.php +++ b/src/wp-includes/rest-api/class-wp-rest-server.php @@ -63,6 +63,14 @@ class WP_REST_Server { */ protected $namespaces = array(); + /** + * Lazily-loaded namespaces registered to the server. + * + * @since X.X.0 + * @var array + */ + protected $lazy_namespaces = array(); + /** * Endpoints registered to the server. * @@ -95,6 +103,22 @@ class WP_REST_Server { */ protected $dispatching_requests = array(); + /** + * Route context constant: Load all namespaces including lazy-loaded ones. + * + * @since X.X.0 + * @var string + */ + public const ROUTE_CONTEXT_ALL = 'all'; + + /** + * Route context constant: Only return routes from already-loaded namespaces. + * + * @since X.X.0 + * @var string + */ + public const ROUTE_CONTEXT_LOADED_ONLY = 'loaded_only'; + /** * Instantiates the REST server. * @@ -884,6 +908,92 @@ public function envelope_response( $response, $embed ) { return rest_ensure_response( $envelope ); } + /** + * Register a namespace for lazy loading. + * + * @since X.X.0 + * + * @param string $route_namespace The namespace to register for lazy loading. + */ + public function register_lazy_loaded_namespace( $route_namespace ) { + if ( ! isset( $this->lazy_namespaces[ $route_namespace ] ) ) { + $this->lazy_namespaces[ $route_namespace ] = false; + } + } + + /** + * Load a specific lazy namespace. + * + * @since X.X.0 + * + * @param string $route_namespace The namespace to load. + * @return bool True if loaded, false if not a lazy namespace. + */ + protected function load_lazy_namespace( $route_namespace ) { + if ( ! isset( $this->lazy_namespaces[ $route_namespace ] ) ) { + return false; + } + + if ( $this->lazy_namespaces[ $route_namespace ] ) { + return true; // Already loaded + } + + $this->lazy_namespaces[ $route_namespace ] = true; + + /** + * Fires when a lazy-loaded REST API namespace is being loaded. + * + * This action is triggered when a lazy-loaded namespace needs to have its + * routes registered. Plugins should hook into this action to register + * their routes for the specific namespace being loaded. + * + * The dynamic portion of the hook name, `$route_namespace`, refers to + * the namespace being loaded (e.g., 'my-plugin/v1'). + * + * @since X.X.0 + * + * @example + * // Register routes when your namespace loads + * add_action( 'rest_lazy_load_namespace_my-plugin/v1', function() { + * register_rest_route( 'my-plugin/v1', '/posts', array( + * 'methods' => 'GET', + * 'callback' => 'my_plugin_get_posts', + * 'permission_callback' => '__return_true', + * ) ); + * } ); + */ + do_action( 'rest_lazy_load_namespace_' . $route_namespace ); + + /** + * Fires when any lazy-loaded REST API namespace is being loaded. + * + * This action is triggered after the namespace-specific action + * {@see 'rest_lazy_load_namespace_$route_namespace'} has fired. + * It can be used to perform actions common to all lazy-loaded + * namespaces, such as logging or registering shared dependencies. + * + * @since X.X.0 + * + * @param string $route_namespace The namespace being loaded (e.g., 'my-plugin/v1'). + */ + do_action( 'rest_lazy_load_namespace', $route_namespace ); + + return true; + } + + /** + * Load all lazy namespaces. + * + * @since X.X.0 + */ + protected function load_all_lazy_namespaces() { + foreach ( $this->lazy_namespaces as $namespace => $has_loaded ) { + if ( ! $has_loaded ) { + $this->load_lazy_namespace( $namespace ); + } + } + } + /** * Registers a route to the server. * @@ -946,14 +1056,28 @@ public function register_route( $route_namespace, $route, $route_args, $override * Note that the path regexes (array keys) must have @ escaped, as this is * used as the delimiter with preg_match() * - * @since 4.4.0 - * @since 5.4.0 Added `$route_namespace` parameter. - * * @param string $route_namespace Optionally, only return routes in the given namespace. + * @param string $route_context Optional. Route loading context. Accepts: + * - 'all' (default): Load all namespaces including lazy-loaded ones + * - 'loaded_only': Only return routes from already-loaded namespaces + * * @return array `'/path/regex' => array( $callback, $bitmask )` or * `'/path/regex' => array( array( $callback, $bitmask ), ...)`. + * + * @since 4.4.0 + * @since 5.4.0 Added `$route_namespace` parameter. + * @since X.X.0 Added `$route_context` parameter. */ - public function get_routes( $route_namespace = '' ) { + public function get_routes( $route_namespace = '', $route_context = self::ROUTE_CONTEXT_ALL ) { + if ( self::ROUTE_CONTEXT_LOADED_ONLY !== $route_context ) { + if ( $route_namespace ) { + // Load only the namespace requested + $this->load_lazy_namespace( $route_namespace ); + } else { + $this->load_all_lazy_namespaces(); + } + } + $endpoints = $this->endpoints; if ( $route_namespace ) { @@ -1155,8 +1279,9 @@ protected function match_request_to_handler( $request ) { $with_namespace = array(); - foreach ( $this->get_namespaces() as $namespace ) { - if ( str_starts_with( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) { + $all_registered_namespaces = array_keys( $this->namespaces + $this->lazy_namespaces ); + foreach ( $all_registered_namespaces as $namespace ) { + if ( str_starts_with( trailingslashit( ltrim( $path, '/' ) ), trailingslashit( $namespace ) ) ) { $with_namespace[] = $this->get_routes( $namespace ); } } @@ -1164,7 +1289,8 @@ protected function match_request_to_handler( $request ) { if ( $with_namespace ) { $routes = array_merge( ...$with_namespace ); } else { - $routes = $this->get_routes(); + // Avoid loading all namespaces as none match. + $routes = $this->get_routes( '', self::ROUTE_CONTEXT_LOADED_ONLY ); } foreach ( $routes as $route => $handlers ) { @@ -1357,6 +1483,9 @@ protected function get_json_last_error() { * @return WP_REST_Response The API root index data. */ public function get_index( $request ) { + // Pre-load all namespaces and endpoints. + $this->load_all_lazy_namespaces(); + // General site data. $available = array( 'name' => get_option( 'blogname' ), @@ -1370,7 +1499,7 @@ public function get_index( $request ) { 'show_on_front' => get_option( 'show_on_front' ), 'namespaces' => array_keys( $this->namespaces ), 'authentication' => array(), - 'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ), + 'routes' => $this->get_data_for_routes( $this->get_routes( '', self::ROUTE_CONTEXT_LOADED_ONLY ), $request['context'] ), ); $response = new WP_REST_Response( $available ); @@ -1515,6 +1644,8 @@ protected function add_image_to_index( WP_REST_Response $response, $image_id, $t public function get_namespace_index( $request ) { $namespace = $request['namespace']; + $this->load_lazy_namespace( $namespace ); + if ( ! isset( $this->namespaces[ $namespace ] ) ) { return new WP_Error( 'rest_invalid_namespace', @@ -1524,7 +1655,7 @@ public function get_namespace_index( $request ) { } $routes = $this->namespaces[ $namespace ]; - $endpoints = array_intersect_key( $this->get_routes(), $routes ); + $endpoints = array_intersect_key( $this->get_routes( $namespace, self::ROUTE_CONTEXT_LOADED_ONLY ), $routes ); $data = array( 'namespace' => $namespace, diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 692c363ac7595..a52c578c362fd 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -1377,6 +1377,20 @@ public function test_get_namespace_index() { ), ) ); + $server->register_route( + 'test/example/extended', + '/test/example/extended/some-route', + array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => '__return_true', + ), + array( + 'methods' => WP_REST_Server::DELETABLE, + 'callback' => '__return_true', + ), + ) + ); $server->register_route( 'test/another', '/test/another/route', @@ -1402,6 +1416,7 @@ public function test_get_namespace_index() { // ...and none we don't. $this->assertArrayNotHasKey( '/test/another/route', $data['routes'] ); + $this->assertArrayNotHasKey( '/test/example/extended/extended-route', $data['routes'] ); } public function test_get_namespaces() { @@ -2611,6 +2626,571 @@ public function test_prefers_developer_defined_target_hints() { $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); } + /** + * Verify route matching priority is based on the order of registration. + * + * Documents existing route priority behavior that the lazy loading + * changes to match_request_to_handler() must preserve. + * + * @ticket 63946 + */ + public function test_route_priority_registration_order() { + // Register general route first. + register_rest_route( + 'priority-test/v1', + '/items/(?P[\\w-]+)', + array( + array( + 'methods' => 'GET', + 'callback' => function ( $req ) { + return array( + 'handler' => 'general', + 'id' => $req['id'], + ); + }, + 'permission_callback' => '__return_true', + ), + ) + ); + // Register specific route after general. + register_rest_route( + 'priority-test/v1', + '/items/special', + array( + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'handler' => 'specific' ); + }, + 'permission_callback' => '__return_true', + ), + ) + ); + + // 'special' should match the general route (registered first) + $request = new WP_REST_Request( 'GET', '/priority-test/v1/items/special' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'general', $data['handler'] ); + } + + /** + * Verify route matching priority is based on the order of registration. + * + * Documents existing route priority behavior that the lazy loading + * changes to match_request_to_handler() must preserve. + * + * @ticket 63946 + */ + public function test_route_priority_reverse_registration_order() { + // Register specific route first. + register_rest_route( + 'priority-test/v1', + '/items/special', + array( + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'handler' => 'specific' ); + }, + 'permission_callback' => '__return_true', + ), + ) + ); + // Register general route after specific. + register_rest_route( + 'priority-test/v1', + '/items/(?P[\\w-]+)', + array( + array( + 'methods' => 'GET', + 'callback' => function ( $req ) { + return array( + 'handler' => 'general', + 'id' => $req['id'], + ); + }, + 'permission_callback' => '__return_true', + ), + ) + ); + + $request = new WP_REST_Request( 'GET', '/priority-test/v1/items/special' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'specific', $data['handler'] ); + } + + + /** + * @ticket 63946 + */ + public function test_route_override_parameter_replaces_callback() { + // Register initial route. + register_rest_route( + 'override-test/v1', + '/thing', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'version' => 'original' ); + }, + 'permission_callback' => '__return_true', + ) + ); + // Register again with override. + register_rest_route( + 'override-test/v1', + '/thing', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'version' => 'override' ); + }, + 'permission_callback' => '__return_true', + ), + true + ); + + $request = new WP_REST_Request( 'GET', '/override-test/v1/thing' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'override', $data['version'] ); + } + + /** + * Test basic lazy namespace registration. + * + * @ticket 63946 + * */ + public function test_lazy_namespace_registration() { + $server = rest_get_server(); + + // Register a lazy namespace. + $server->register_lazy_loaded_namespace( 'lazy-test/v1' ); + + // Lazy-loaded namespaces don't appear in get_namespaces() until they are loaded and their routes are registered. + $namespaces = $server->get_namespaces(); + $this->assertNotContains( 'lazy-test/v1', $namespaces ); + } + + /** + * Test lazy namespace loading via action hook. + * + * @ticket 63946 + */ + public function test_lazy_namespace_loading_via_action() { + $server = rest_get_server(); + $server->register_lazy_loaded_namespace( 'lazy-action/v1' ); + + $action_called = false; + $action_namespace = ''; + + // Hook to simulate route registration. + add_action( + 'rest_lazy_load_namespace_lazy-action/v1', + function () use ( &$action_called, &$action_namespace ) { + $action_called = true; + $action_namespace = 'lazy-action/v1'; + + // Register actual routes when lazy loading. + register_rest_route( + 'lazy-action/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'loaded' => true ); + }, + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Trigger lazy loading by requesting routes. + $routes = $server->get_routes( 'lazy-action/v1' ); + + // Action should have been called. + $this->assertTrue( $action_called ); + $this->assertEquals( 'lazy-action/v1', $action_namespace ); + + // Routes should now be available. + $this->assertArrayHasKey( '/lazy-action/v1/test', $routes ); + } + + /** + * Test that lazy loading only happens once per namespace. + * + * @ticket 63946 + */ + public function test_lazy_namespace_loads_only_once() { + $server = rest_get_server(); + $server->register_lazy_loaded_namespace( 'lazy-once/v1' ); + + $load_count = 0; + + add_action( + 'rest_lazy_load_namespace_lazy-once/v1', + function () use ( &$load_count ) { + $load_count++; + register_rest_route( + 'lazy-once/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Call multiple times that should trigger loading. + $server->get_routes( 'lazy-once/v1' ); + $server->get_routes( 'lazy-once/v1' ); + + // Should only load once. + $this->assertEquals( 1, $load_count ); + } + + /** + * Test get_routes() with empty namespace loads all lazy namespaces. + * + * @ticket 63946 + */ + public function test_get_routes_empty_namespace_loads_all_lazy() { + $server = rest_get_server(); + + // Register multiple lazy namespaces. + $server->register_lazy_loaded_namespace( 'lazy-all-1/v1' ); + $server->register_lazy_loaded_namespace( 'lazy-all-2/v1' ); + + $loaded_namespaces = array(); + + add_action( + 'rest_lazy_load_namespace_lazy-all-1/v1', + function () use ( &$loaded_namespaces ) { + $loaded_namespaces[] = 'lazy-all-1/v1'; + register_rest_route( + 'lazy-all-1/v1', + '/test1', + array( + 'methods' => 'GET', + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + } + ); + + add_action( + 'rest_lazy_load_namespace_lazy-all-2/v1', + function () use ( &$loaded_namespaces ) { + $loaded_namespaces[] = 'lazy-all-2/v1'; + register_rest_route( + 'lazy-all-2/v1', + '/test2', + array( + 'methods' => 'GET', + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + } + ); + + $routes = $server->get_routes(); + + // Both namespaces should have been loaded. + $this->assertContains( 'lazy-all-1/v1', $loaded_namespaces ); + $this->assertContains( 'lazy-all-2/v1', $loaded_namespaces ); + $this->assertArrayHasKey( '/lazy-all-1/v1/test1', $routes ); + $this->assertArrayHasKey( '/lazy-all-2/v1/test2', $routes ); + } + + /** + * Test route dispatch triggers lazy loading for matching namespace. + * + * @ticket 63946 + */ + public function test_route_dispatch_triggers_lazy_loading() { + $server = rest_get_server(); + $server->register_lazy_loaded_namespace( 'lazy-dispatch/v1' ); + + $dispatched = false; + + add_action( + 'rest_lazy_load_namespace_lazy-dispatch/v1', + function () use ( &$dispatched ) { + $dispatched = true; + register_rest_route( + 'lazy-dispatch/v1', + '/endpoint', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'success' => true ); + }, + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Make a request that should trigger lazy loading. + $request = new WP_REST_Request( 'GET', '/lazy-dispatch/v1/endpoint' ); + $response = $server->dispatch( $request ); + + $this->assertTrue( $dispatched ); + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertTrue( $data['success'] ); + } + + /** + * Test mixed regular and lazy namespaces work together. + * + * @ticket 63946 + */ + public function test_mixed_regular_and_lazy_namespaces() { + $server = rest_get_server(); + + // Register regular namespace. + register_rest_route( + 'regular/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'type' => 'regular' ); + }, + 'permission_callback' => '__return_true', + ) + ); + + // Register lazy namespace. + $server->register_lazy_loaded_namespace( 'lazy-mixed/v1' ); + + add_action( + 'rest_lazy_load_namespace_lazy-mixed/v1', + function () { + register_rest_route( + 'lazy-mixed/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'type' => 'lazy' ); + }, + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Test regular namespace works. + $request = new WP_REST_Request( 'GET', '/regular/v1/test' ); + $response = $server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'regular', $data['type'] ); + + // Test lazy namespace works. + $request = new WP_REST_Request( 'GET', '/lazy-mixed/v1/test' ); + $response = $server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'lazy', $data['type'] ); + } + + /** + * Test same namespace registered both lazy and regular. + * + * @ticket 63946 + */ + public function test_same_namespace_lazy_and_regular() { + $server = rest_get_server(); + + // Register regular route first. + register_rest_route( + 'shared/v1', + '/regular', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'source' => 'regular' ); + }, + 'permission_callback' => '__return_true', + ) + ); + + // Register lazy namespace with same base. + $server->register_lazy_loaded_namespace( 'shared/v1' ); + + add_action( + 'rest_lazy_load_namespace_shared/v1', + function () { + register_rest_route( + 'shared/v1', + '/lazy', + array( + 'methods' => 'GET', + 'callback' => function () { + return array( 'source' => 'lazy' ); + }, + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Both routes should work. + $request = new WP_REST_Request( 'GET', '/shared/v1/regular' ); + $response = $server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'regular', $data['source'] ); + + $request = new WP_REST_Request( 'GET', '/shared/v1/lazy' ); + $response = $server->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( 'lazy', $data['source'] ); + } + + /** + * Test root index loads all lazy namespaces. + * + * @ticket 63946 + */ + public function test_root_index_loads_all_lazy_namespaces() { + $server = rest_get_server(); + $server->register_lazy_loaded_namespace( 'lazy-index/v1' ); + + $loaded = false; + + add_action( + 'rest_lazy_load_namespace_lazy-index/v1', + function () use ( &$loaded ) { + $loaded = true; + register_rest_route( + 'lazy-index/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Request root index. + $request = new WP_REST_Request( 'GET', '/' ); + $response = $server->dispatch( $request ); + + $this->assertTrue( $loaded ); + $data = $response->get_data(); + $this->assertContains( 'lazy-index/v1', $data['namespaces'] ); + } + + /** + * Test namespace index triggers lazy loading. + * + * @ticket 63946 + */ + public function test_namespace_index_triggers_lazy_loading() { + $server = rest_get_server(); + $server->register_lazy_loaded_namespace( 'lazy-ns-index/v1' ); + + $loaded = false; + + add_action( + 'rest_lazy_load_namespace_lazy-ns-index/v1', + function () use ( &$loaded ) { + $loaded = true; + register_rest_route( + 'lazy-ns-index/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + } + ); + + // Request namespace index. + $request = new WP_REST_Request( 'GET', '/lazy-ns-index/v1' ); + $response = $server->dispatch( $request ); + + $this->assertTrue( $loaded ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertEquals( 'lazy-ns-index/v1', $data['namespace'] ); + $this->assertArrayHasKey( '/lazy-ns-index/v1/test', $data['routes'] ); + } + + /** + * Test lazy namespace doesn't load when not needed. + * + * @ticket 63946 + */ + public function test_lazy_namespace_no_unnecessary_loading() { + $server = rest_get_server(); + $server->register_lazy_loaded_namespace( 'lazy-unused/v1' ); + + $loaded = false; + + add_action( + 'rest_lazy_load_namespace_lazy-unused/v1', + function () use ( &$loaded ) { + $loaded = true; + } + ); + + // Register and request a different namespace. + register_rest_route( + 'other/v1', + '/test', + array( + 'methods' => 'GET', + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + ) + ); + + $request = new WP_REST_Request( 'GET', '/other/v1/test' ); + $response = $server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertFalse( $loaded, 'Lazy namespace should not load when not needed' ); + } + + /** + * Test double registration of same lazy namespace. + * + * @ticket 63946 + */ + public function test_double_lazy_namespace_registration() { + $server = rest_get_server(); + + // Register same namespace twice. + $server->register_lazy_loaded_namespace( 'lazy-double/v1' ); + $server->register_lazy_loaded_namespace( 'lazy-double/v1' ); + + $times_loaded = 0; + add_action( + 'rest_lazy_load_namespace_lazy-double/v1', + function () use ( &$times_loaded ) { + ++$times_loaded; + } + ); + + $server->get_routes(); + + $this->assertEquals( 1, $times_loaded, 'A namespace registered multiple times should only trigger the load action once.' ); + } + public function _validate_as_integer_123( $value, $request, $key ) { if ( ! is_int( $value ) ) { return new WP_Error( 'some-error', 'This is not valid!' );