Skip to content

Commit 27f28fe

Browse files
committed
Add "resolvable" routes for the REST API
1 parent 047ef80 commit 27f28fe

5 files changed

Lines changed: 368 additions & 83 deletions

File tree

src/wp-includes/rest-api.php

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -92,67 +92,86 @@ function register_rest_route( $route_namespace, $route, $args = array(), $overri
9292
);
9393
}
9494

95-
if ( isset( $args['args'] ) ) {
95+
if ( ! is_callable( $args ) && isset( $args['args'] ) ) {
9696
$common_args = $args['args'];
9797
unset( $args['args'] );
9898
} else {
9999
$common_args = array();
100100
}
101101

102-
if ( isset( $args['callback'] ) ) {
102+
if ( is_callable( $args ) || isset( $args['callback'] ) ) {
103103
// Upgrade a single set to multiple.
104104
$args = array( $args );
105105
}
106106

107-
$defaults = array(
108-
'methods' => 'GET',
109-
'callback' => null,
110-
'args' => array(),
111-
);
112-
113107
foreach ( $args as $key => &$arg_group ) {
114108
if ( ! is_numeric( $key ) ) {
115109
// Route option, skip here.
116110
continue;
117111
}
118112

119-
$arg_group = array_merge( $defaults, $arg_group );
120-
$arg_group['args'] = array_merge( $common_args, $arg_group['args'] );
113+
if ( is_callable( $arg_group ) ) {
114+
// Just-in-time resolvable callback, we'll normalize it later.
115+
$arg_group = new WP_REST_Resolvable_Route( $clean_namespace, $route, $arg_group );
116+
continue;
117+
}
118+
119+
$arg_group = normalize_rest_endpoint_options( $clean_namespace, $route, $arg_group, $common_args );
120+
}
121+
122+
$full_route = '/' . $clean_namespace . '/' . trim( $route, '/' );
123+
rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override );
124+
return true;
125+
}
121126

122-
if ( ! isset( $arg_group['permission_callback'] ) ) {
127+
/**
128+
* Normalize the options for a single REST API endpoint.
129+
*
130+
* @param string $namespace The route namespace.
131+
* @param string $route The route.
132+
* @param array $endpoint The endpoint options.
133+
* @param array $common_args Common arguments to merge with endpoint-specific arguments.
134+
*/
135+
function normalize_rest_endpoint_options( string $namespace, string $route, array $endpoint, array $common_args = [] ) {
136+
$defaults = array(
137+
'methods' => 'GET',
138+
'callback' => null,
139+
'args' => array(),
140+
);
141+
$endpoint = array_merge( $defaults, $endpoint );
142+
$endpoint['args'] = array_merge( $common_args, $endpoint['args'] );
143+
144+
if ( ! isset( $endpoint['permission_callback'] ) ) {
145+
_doing_it_wrong(
146+
'register_rest_route',
147+
sprintf(
148+
/* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */
149+
__( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ),
150+
'<code>' . $namespace . '/' . trim( $route, '/' ) . '</code>',
151+
'<code>permission_callback</code>',
152+
'<code>__return_true</code>'
153+
),
154+
'5.5.0'
155+
);
156+
}
157+
158+
foreach ( $endpoint['args'] as $arg ) {
159+
if ( ! is_array( $arg ) ) {
123160
_doing_it_wrong(
124-
__FUNCTION__,
161+
'register_rest_route',
125162
sprintf(
126-
/* translators: 1: The REST API route being registered, 2: The argument name, 3: The suggested function name. */
127-
__( 'The REST API route definition for %1$s is missing the required %2$s argument. For REST API routes that are intended to be public, use %3$s as the permission callback.' ),
128-
'<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>',
129-
'<code>permission_callback</code>',
130-
'<code>__return_true</code>'
163+
/* translators: 1: $args, 2: The REST API route being registered. */
164+
__( 'REST API %1$s should be an array of arrays. Non-array value detected for %2$s.' ),
165+
'<code>$args</code>',
166+
'<code>' . $namespace . '/' . trim( $route, '/' ) . '</code>'
131167
),
132-
'5.5.0'
168+
'6.1.0'
133169
);
134-
}
135-
136-
foreach ( $arg_group['args'] as $arg ) {
137-
if ( ! is_array( $arg ) ) {
138-
_doing_it_wrong(
139-
__FUNCTION__,
140-
sprintf(
141-
/* translators: 1: $args, 2: The REST API route being registered. */
142-
__( 'REST API %1$s should be an array of arrays. Non-array value detected for %2$s.' ),
143-
'<code>$args</code>',
144-
'<code>' . $clean_namespace . '/' . trim( $route, '/' ) . '</code>'
145-
),
146-
'6.1.0'
147-
);
148-
break; // Leave the foreach loop once a non-array argument was found.
149-
}
170+
break; // Leave the foreach loop once a non-array argument was found.
150171
}
151172
}
152173

153-
$full_route = '/' . $clean_namespace . '/' . trim( $route, '/' );
154-
rest_get_server()->register_route( $clean_namespace, $full_route, $args, $override );
155-
return true;
174+
return $endpoint;
156175
}
157176

158177
/**
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
<?php
2+
/**
3+
* REST API: WP_REST_Resolvable_Route class
4+
*
5+
* @package WordPress
6+
* @subpackage REST_API
7+
* @since X.X.0
8+
*/
9+
10+
/**
11+
* Just-in-time resolvable route for REST API routes.
12+
*
13+
* @since X.X.0
14+
*/
15+
class WP_REST_Resolvable_Route implements ArrayAccess, IteratorAggregate, Countable {
16+
protected $namespace;
17+
protected $route;
18+
19+
/**
20+
* The callable used to resolve the route.
21+
*
22+
* @since X.X.0
23+
* @var callable
24+
*/
25+
protected $callable;
26+
27+
/**
28+
* The resolved route definition.
29+
*
30+
* @since X.X.0
31+
* @var array|null
32+
*/
33+
protected $resolved = null;
34+
35+
/**
36+
* Constructor.
37+
*
38+
* @since X.X.0
39+
*
40+
* @param callable $closure The callable used to resolve the route. Returns a single route definition.
41+
*/
42+
public function __construct( string $namespace, string $route, callable $closure ) {
43+
$this->namespace = $namespace;
44+
$this->route = $route;
45+
$this->callable = $closure;
46+
}
47+
48+
/**
49+
* Invokes the callable to resolve, if needed.
50+
*
51+
* Routes can only be resolved once, the first time they're used. Any
52+
* subsequent calls will return the same resolved definition, which may
53+
* be modified by reference if needed.
54+
*
55+
* @since X.X.0
56+
*
57+
* @return array The resolved route definition.
58+
*/
59+
public function __invoke() {
60+
if ( ! $this->resolved ) {
61+
$this->resolved = call_user_func( $this->callable );
62+
63+
// Normalize the result.
64+
$this->resolved = normalize_rest_endpoint_options( $this->namespace, $this->route, $this->resolved );
65+
}
66+
return $this->resolved;
67+
}
68+
69+
/**
70+
* Checks a single array key exists in the resolved route definition.
71+
*
72+
* @since X.X.0
73+
*
74+
* @param string $key The key to check.
75+
* @return bool True if the key exists, false otherwise.
76+
*/
77+
public function offsetExists( mixed $k ) : bool {
78+
$this->__invoke();
79+
return isset( $this->resolved[ $k ] );
80+
}
81+
82+
/**
83+
* Gets a single array key from the resolved route definition.
84+
*
85+
* @since X.X.0
86+
*
87+
* @param string $key The key to retrieve.
88+
* @return mixed The value of the key, or null if not set. Returns by reference, so it can be modified if needed.
89+
*/
90+
public function &offsetGet( mixed $k ) : mixed {
91+
$this->__invoke();
92+
return $this->resolved[ $k ];
93+
}
94+
95+
/**
96+
* Sets a single array key in the resolved route definition.
97+
*
98+
* @since X.X.0
99+
*
100+
* @param string $key The key to set.
101+
* @param mixed $value The value to set.
102+
*/
103+
public function offsetSet( mixed $k, mixed $v ) : void {
104+
$this->__invoke();
105+
$this->resolved[ $k ] = $v;
106+
}
107+
108+
/**
109+
* Unsets a single array key in the resolved route definition.
110+
*
111+
* @since X.X.0
112+
*
113+
* @param string $key The key to unset.
114+
*/
115+
public function offsetUnset( mixed $k ) : void {
116+
$this->__invoke();
117+
unset( $this->resolved[ $k ] );
118+
}
119+
120+
/**
121+
* Gets an iterator for the resolved route definition.
122+
*
123+
* @since X.X.0
124+
*
125+
* @return Traversable An iterator for the resolved route definition.
126+
*/
127+
public function getIterator(): Traversable {
128+
$this->__invoke();
129+
return new ArrayIterator( $this->resolved );
130+
}
131+
132+
/**
133+
* Counts the number of elements in the resolved route definition.
134+
*
135+
* @since X.X.0
136+
*
137+
* @return int The number of elements in the resolved route definition.
138+
*/
139+
public function count() : int {
140+
$this->__invoke();
141+
return count( $this->resolved );
142+
}
143+
}

0 commit comments

Comments
 (0)