详解 WordPress `register_rest_route()` 函数的源码:如何将 REST API 路由注册到 `WP_REST_Server` 类。

各位观众,大家好! 今天咱们来聊聊 WordPress REST API 的核心之一:register_rest_route() 函数。 别看这函数名字平平无奇,它可是连接 WordPress 后台和前端(或者任何外部应用)的关键桥梁。 咱们的目标是彻底搞懂它的工作原理,就像庖丁解牛一样,把它的源码拆开,一块一块地分析。

一、REST API 的基石:WP_REST_Server

首先,我们要认识一下 WordPress REST API 的总指挥官:WP_REST_Server 类。 这个类负责处理所有的 API 请求,包括路由匹配、权限验证、数据处理等等。 可以把它想象成一个交通枢纽,所有的 API 请求都要经过它来调度。

wp-includes/rest-api/class-wp-rest-server.php 文件中,你可以找到这个类的定义。 它有一个很重要的属性:$endpoints。 这个属性是一个关联数组,存储了所有已注册的 REST API 路由。 键是路由的正则表达式,值是一个包含路由处理函数和其他配置信息的数组。

二、register_rest_route() 函数:路由注册员

register_rest_route() 函数的作用,就是把我们定义的 REST API 路由添加到 WP_REST_Server 类的 $endpoints 属性中。 它的原型如下:

/**
 * Registers a REST route.
 *
 * @since 4.4.0
 *
 * @global WP_REST_Server $wp_rest_server REST server instance.
 *
 * @param string $namespace The namespace for the route.
 * @param string $route     The route to register.
 * @param array  $args      Optional. An array of arguments to configure the route.
 * @return bool True on success, false on failure.
 */
function register_rest_route( $namespace, $route, $args = array() ) {
    global $wp_rest_server;

    if ( ! ( $wp_rest_server instanceof WP_REST_Server ) ) {
        return false;
    }

    return $wp_rest_server->register_route( $namespace, $route, $args );
}

可以看到,register_rest_route() 函数本身只是一个简单的包装器。 它首先检查 $wp_rest_server 全局变量是否是 WP_REST_Server 类的实例,如果不是,就返回 false。 然后,它调用 $wp_rest_server 对象的 register_route() 方法,真正完成路由注册的工作。

三、WP_REST_Server::register_route() 方法:路由注册的核心

现在,让我们深入到 WP_REST_Server::register_route() 方法的源码中,看看它是如何工作的。

/**
 * Registers a REST route.
 *
 * @since 4.4.0
 *
 * @param string $namespace The namespace for the route.
 * @param string $route     The route to register.
 * @param array  $args      Optional. An array of arguments to configure the route.
 * @return bool True on success, false on failure.
 */
public function register_route( $namespace, $route, $args ) {
    // Validate arguments.
    if ( ! is_string( $namespace ) ) {
        _doing_it_wrong( __METHOD__, 'Namespace must be a string.', '4.4.0' );
        return false;
    }

    if ( ! is_string( $route ) ) {
        _doing_it_wrong( __METHOD__, 'Route must be a string.', '4.4.0' );
        return false;
    }

    if ( ! is_array( $args ) ) {
        _doing_it_wrong( __METHOD__, 'Args must be an array.', '4.4.0' );
        return false;
    }

    // Normalize the route.
    if ( empty( $route ) || '/' !== substr( $route, 0, 1 ) ) {
        $route = '/' . $route;
    }

    $route = preg_replace( '#/+#', '/', $route );

    // Normalize the namespace.
    if ( empty( $namespace ) ) {
        $namespace = 'wp/v2';
    }

    $namespace = trim( $namespace, '/' );

    // Add the namespace to the route.
    $full_route = '/' . $namespace . $route;

    // Generate the regex for the route.
    $regex = $this->get_route_regex( $full_route );

    // Normalize the arguments.
    $defaults = array(
        'methods'  => 'GET',
        'callback' => null,
        'args'     => array(),
        'permission_callback' => '__return_true',
    );
    $args = wp_parse_args( $args, $defaults );

    // Validate the arguments.
    if ( ! is_callable( $args['callback'] ) ) {
        _doing_it_wrong( __METHOD__, 'Callback must be callable.', '4.4.0' );
        return false;
    }

    if ( ! is_callable( $args['permission_callback'] ) ) {
        _doing_it_wrong( __METHOD__, 'Permission callback must be callable.', '4.7.0' );
        return false;
    }

    // Store the route.
    if ( ! isset( $this->endpoints[ $regex ] ) ) {
        $this->endpoints[ $regex ] = array();
    }

    $this->endpoints[ $regex ][] = array(
        'methods'  => $this->normalize_methods( $args['methods'] ),
        'callback' => $args['callback'],
        'args'     => $args['args'],
        'permission_callback' => $args['permission_callback'],
    );

    return true;
}

这个方法看起来有点长,但其实逻辑并不复杂。 咱们把它分解成几个步骤:

  1. 参数验证:

    • 首先,验证 namespacerouteargs 参数的类型是否正确。 如果类型不正确,就使用 _doing_it_wrong() 函数输出一个警告,并返回 false
  2. 路由和命名空间规范化:

    • 确保路由以 / 开头,并移除多余的 /
    • 如果命名空间为空,则默认为 wp/v2
    • 移除命名空间两侧的 /
    • 将命名空间添加到路由前面,生成完整的路由 full_route
  3. 生成路由正则表达式:

    • 调用 $this->get_route_regex( $full_route ) 方法,将 full_route 转换成一个正则表达式。 这个正则表达式用于匹配实际的 API 请求。
  4. 参数规范化:

    • 使用 wp_parse_args() 函数,将 args 数组与默认值合并。 默认值包括:
      • methods: GET (允许的 HTTP 方法)
      • callback: null (路由处理函数)
      • args: array() (路由参数)
      • permission_callback: __return_true (权限验证函数,默认允许所有访问)
  5. 参数验证:

    • 验证 callbackpermission_callback 参数是否是可调用函数。 如果不是,就使用 _doing_it_wrong() 函数输出一个警告,并返回 false
  6. 存储路由:

    • 将路由信息存储到 $this->endpoints 属性中。 $this->endpoints 是一个关联数组,键是路由的正则表达式,值是一个数组,包含了所有匹配该路由的路由信息。

    • 如果 $this->endpoints[ $regex ] 不存在,则创建一个新的数组。

    • 将包含 methodscallbackargspermission_callback 的路由信息添加到 $this->endpoints[ $regex ] 数组中。

  7. 返回成功:

    • 返回 true,表示路由注册成功。

四、WP_REST_Server::get_route_regex() 方法:正则表达式生成器

WP_REST_Server::get_route_regex() 方法负责将路由字符串转换成正则表达式。 它的源码如下:

/**
 * Retrieves the route regex for a route.
 *
 * @since 4.4.0
 *
 * @param string $route Route to generate the regex for.
 * @return string Regex to match the requested route.
 */
protected function get_route_regex( $route ) {
    $route = preg_replace( '#/+#', '/', $route );
    $route = str_replace( array( '(', ')' ), array( '(', ')' ), $route );
    $route = preg_replace( '#<([a-zA-Z_][a-zA-Z0-9_]*)>#', '(?P<$1>[^/]+)', $route );
    $route = str_replace( '-', '-', $route );

    return '#^' . $route . '$#';
}

这个方法的工作原理如下:

  1. 规范化路由:

    • 移除多余的 /
  2. 转义括号:

    • () 替换成 (),避免正则表达式解析错误。
  3. 参数占位符替换:

    • <参数名> 替换成 (?P<参数名>[^/]+)。 这是一个命名捕获组,用于匹配除 / 之外的任意字符,并将匹配到的值存储到名为 参数名 的变量中。 例如,<id> 会被替换成 (?P<id>[^/]+)
  4. 转义连字符:

    • - 替换成 -,避免正则表达式解析错误。
  5. 添加正则表达式定界符和边界:

    • 在路由字符串的前后添加 #^$,表示正则表达式的开始和结束。

举个例子:

假设我们要注册一个路由 /books/<id>,那么 $this->get_route_regex() 方法会把它转换成如下的正则表达式:

#^/books/(?P<id>[^/]+)$#

这个正则表达式可以匹配 /books/123/books/456 等等。 匹配到的 id 值会被存储到 id 变量中,可以在路由处理函数中通过 $request->get_param( 'id' ) 获取。

五、WP_REST_Server::normalize_methods() 方法:HTTP 方法标准化

WP_REST_Server::normalize_methods() 方法负责将 HTTP 方法字符串转换成一个大写字母的数组。 它的源码如下:

/**
 * Normalizes method names to uppercase.
 *
 * @since 4.7.0
 *
 * @param string|array $methods HTTP methods to normalize.
 * @return array Normalized array of methods in uppercase.
 */
protected function normalize_methods( $methods ) {
    if ( ! is_array( $methods ) ) {
        $methods = array( $methods );
    }

    $methods = array_map( 'strtoupper', $methods );
    return $methods;
}

这个方法的工作原理如下:

  1. 转换为数组:

    • 如果 $methods 不是数组,则将其转换为包含该方法的数组。
  2. 转换为大写:

    • 使用 array_map() 函数和 strtoupper() 函数,将数组中的所有元素转换为大写。
  3. 返回标准化后的数组:

    • 返回包含大写 HTTP 方法的数组。

举个例子:

假设我们传入的 $methods'get,post',那么 $this->normalize_methods() 方法会把它转换成 array( 'GET', 'POST' )

六、一个完整的例子:注册一个自定义 REST API 路由

现在,让我们通过一个完整的例子,来演示如何使用 register_rest_route() 函数注册一个自定义 REST API 路由。

add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/books', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_get_books',
    'permission_callback' => '__return_true',
  ) );

  register_rest_route( 'myplugin/v1', '/books/(?P<id>d+)', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_get_book',
    'args' => array(
      'id' => array(
        'validate_callback' => 'is_numeric',
        'sanitize_callback' => 'absint',
      ),
    ),
    'permission_callback' => '__return_true',
  ) );

  register_rest_route( 'myplugin/v1', '/books', array(
    'methods' => 'POST',
    'callback' => 'my_awesome_create_book',
    'permission_callback' => function() {
      return current_user_can( 'publish_posts' );
    },
    'args' => array(
      'title' => array(
        'required' => true,
        'type' => 'string',
        'description' => 'The title of the book.',
      ),
      'author' => array(
        'type' => 'string',
        'description' => 'The author of the book.',
      ),
    ),
  ) );
} );

function my_awesome_get_books( WP_REST_Request $request ) {
  // 获取所有书籍
  $books = get_posts( array( 'post_type' => 'book' ) );

  $data = array();
  foreach ( $books as $book ) {
    $data[] = array(
      'id' => $book->ID,
      'title' => $book->post_title,
    );
  }

  return $data;
}

function my_awesome_get_book( WP_REST_Request $request ) {
  $id = $request->get_param( 'id' );

  // 获取指定 ID 的书籍
  $book = get_post( $id );

  if ( empty( $book ) ) {
    return new WP_Error( 'book_not_found', 'Book not found', array( 'status' => 404 ) );
  }

  $data = array(
    'id' => $book->ID,
    'title' => $book->post_title,
  );

  return $data;
}

function my_awesome_create_book( WP_REST_Request $request ) {
    $title = $request->get_param( 'title' );
    $author = $request->get_param( 'author' );

    $post_id = wp_insert_post( array(
        'post_title' => $title,
        'post_content' => '',
        'post_status' => 'publish',
        'post_type' => 'book',
    ) );

    if ( is_wp_error( $post_id ) ) {
        return $post_id;
    }

    if ( ! empty( $author ) ) {
        update_post_meta( $post_id, 'author', $author );
    }

    return rest_ensure_response( array( 'id' => $post_id, 'message' => 'Book created successfully' ) );
}

在这个例子中,我们注册了三个 REST API 路由:

  • GET /myplugin/v1/books: 获取所有书籍。
  • GET /myplugin/v1/books/<id>: 获取指定 ID 的书籍。 <id> 是一个参数占位符,会被替换成 (?P<id>d+) 正则表达式。
  • POST /myplugin/v1/books: 创建一本书籍。

我们还定义了三个路由处理函数:my_awesome_get_books()my_awesome_get_book()my_awesome_create_book()。 这些函数负责处理实际的 API 请求,并返回相应的数据。

对于 GET /myplugin/v1/books/<id> 路由,我们使用了 args 参数来定义 id 参数的验证和清理规则。 validate_callback 用于验证参数的值是否有效,sanitize_callback 用于清理参数的值。

对于 POST /myplugin/v1/books 路由,我们也使用了 args 参数来定义 titleauthor 参数。 required 设置为 true 表示 title 参数是必须的。 type 用于指定参数的类型,description 用于描述参数的含义。 同时,使用了权限验证函数 current_user_can( 'publish_posts' ) 只有拥有 publish_posts 权限的用户才能创建书籍。

七、总结

register_rest_route() 函数是 WordPress REST API 的核心函数之一。 它负责将我们定义的 REST API 路由注册到 WP_REST_Server 类的 $endpoints 属性中。 通过深入理解 register_rest_route() 函数的源码,我们可以更好地掌握 WordPress REST API 的工作原理,从而开发出更加强大和灵活的 API 应用。

函数/方法 作用
register_rest_route() 注册 REST API 路由到 WordPress。
WP_REST_Server 处理所有的 API 请求,包括路由匹配、权限验证、数据处理等等。
$WP_REST_Server->endpoints 存储所有已注册的 REST API 路由的关联数组。
WP_REST_Server::register_route() 真正完成路由注册的工作。 验证参数、规范化路由和命名空间、生成路由正则表达式、规范化参数、存储路由。
WP_REST_Server::get_route_regex() 将路由字符串转换成正则表达式。
WP_REST_Server::normalize_methods() 将 HTTP 方法字符串转换成一个大写字母的数组。
WP_REST_Request 表示一个 REST API 请求。 包含了请求的所有信息,包括请求方法、请求参数、请求头等等。
rest_ensure_response() 确保返回一个 WP_REST_Response 对象。 如果传入的参数不是 WP_REST_Response 对象,则将其转换成 WP_REST_Response 对象。
WP_Error 表示一个错误。 可以用于返回 API 请求的错误信息。
current_user_can() 检查当前用户是否具有指定的权限。
add_action( 'rest_api_init', ... ) 在 WordPress REST API 初始化时执行指定的回调函数。 这是注册 REST API 路由的常用方法。
get_posts() 获取文章。 可以通过参数指定文章类型、文章状态等等。
get_post() 获取指定 ID 的文章。
wp_insert_post() 插入一篇文章。
update_post_meta() 更新文章的自定义字段。
$request->get_param( '参数名' ) WP_REST_Request 对象中获取指定名称的参数的值。
'validate_callback' => 'is_numeric' 参数验证回调函数,用于验证参数的值是否有效。
'sanitize_callback' => 'absint' 参数清理回调函数,用于清理参数的值。
'required' => true 指定参数是否是必须的。
'type' => 'string' 指定参数的类型。
'description' => '...' 描述参数的含义。

希望这次讲座能帮助大家更好地理解 WordPress REST API 的底层机制。 谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注