WordPress REST API 不仅仅是一组默认路由。它也是创建自定义路由和端点的工具。WordPress 前端提供了一组默认的 URL 映射,但用于创建它们的工具(例如 Rewrites API,以及查询类:WP_Query、WP_User等)也可用于创建您自己的 URL 映射或自定义查询。

本文档详细介绍了如何创建一个全新的完全自定义路由及其端点。我们将首先通过一个简短的示例进行讲解,然后将其扩展为内部使用的完整控制器模式。

基础入门

所以你想为 API 添加自定义端点?太棒了!让我们从一个简单的示例开始。
让我们先从一个看起来像这样的简单函数开始:

<?php
/**
 * 获取某作者的最新文章标题!
 *
 * @param array $data 函数的选项。
 * @return string|null 最新文章的标题,如果没有则为 null。
 */
function my_awesome_func( $data ) {
  $posts = get_posts( array(
    'author' => $data['id'],
  ) );

  if ( empty( $posts ) ) {
    return null;
  }

  return $posts[0]->post_title;
}

要通过 API 使用此功能,我们需要注册一个路由。这会告诉 API 在收到特定请求时调用我们的函数。我们通过名为 register_rest_route 的函数来实现这一点,该函数应在 rest_api_init 的回调中调用,以避免在 API 未加载时执行额外工作。

我们需要向 register_rest_route 传递三件事:命名空间、我们要的路由以及选项。我们稍后再回到命名空间,但现在让我们选择 myplugin/v1。我们要让路由匹配任何包含 /author/{id} 的内容,其中 {id} 是一个整数。

<?php
add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_func',
  ) );
} );

目前,我们只为该路由注册了一个端点。“路由”指的是 URL,而“端点”指的是对应于方法 和 URL 的后端函数(更多信息,请参阅 术语表)。

例如,如果您的站点域名为 example.com 并且保留了 API 路径 wp-json,则完整 URL 将是 `http://example.com/wp-json/myplugin/v1/author/(?P\d+)`。

在不使用美化固定链接的站点上,路由是作为 rest_route 参数添加到 URL 中的。对于上述示例,完整 URL 将是 http://example.com/?rest_route=/myplugin/v1/author/(?P\d+)

每个路由可以有任意数量的端点,对于每个端点,您可以定义允许的 HTTP 方法、用于响应请求的回调函数以及用于创建自定义权限的权限回调函数。此外,您还可以定义请求中允许的字段,并为每个字段指定默认值、清理回调、验证回调以及是否必需。

命名空间

命名空间是端点 URL 的第一部分。它们应用作供应商/包前缀,以防止自定义路由之间发生冲突。命名空间允许两个插件添加同名但功能不同的路由。

通常,命名空间应遵循 vendor/v1 的模式,其中 vendor 通常是您的插件或主题 slug,而 v1 代表 API 的第一个版本。如果您需要因新端点而破坏兼容性,则可以将其升级为 v2。

上述场景(来自两个不同插件的两个同名路由)要求所有供应商都使用唯一的命名空间。如果不这样做,就如同在主题或插件中没有使用供应商函数前缀、类前缀和/或类命名空间一样,这是非常非常糟糕的做法。

使用命名空间的另一个好处是客户端可以检测对您自定义 API 的支持情况。API 索引列出了站点上可用的命名空间:

{
  "name": "WordPress Site",
  "description": "Just another WordPress site",
  "url": "http://example.com/",
  "namespaces": [
    "wp/v2",
    "vendor/v1",
    "myplugin/v1",
    "myplugin/v2",
  ]
}

如果客户端想检查您的 API 是否存在于站点上,他们可以对照此列表进行检查。(更多信息,请参阅 发现指南。)

参数

默认情况下,路由接收从请求传递的所有参数。这些参数合并为一组参数,然后添加到 Request 对象中,该对象作为第一个参数传递给您的端点:

<?php
function my_awesome_func( WP_REST_Request $request ) {
  // 您可以通过直接访问对象的数组来访问参数:
  $param = $request['some_param'];

  // 或者通过辅助方法:
  $param = $request->get_param( 'some_param' );

  // 您可以获取合并后的参数集:
  $parameters = $request->get_params();

  // 如果需要,也可以单独获取各个参数集:
  $parameters = $request->get_url_params();
  $parameters = $request->get_query_params();
  $parameters = $request->get_body_params();
  $parameters = $request->get_json_params();
  $parameters = $request->get_default_params();

  // 上传文件未合并,但可以单独访问:
  $parameters = $request->get_file_params();
}

(要了解参数是如何合并的,请查看 WP_REST_Request::get_parameter_order() 的源代码;基本顺序是 body、query、URL,然后是 defaults。)

通常情况下,您会收到所有未经修改的参数。但是,您可以在注册路由时注册您的参数,这样您就可以对这些参数运行清理和验证。

如果请求设置了 Content-type: application/json 头部且正文中包含有效的 JSON,get_json_params() 将返回解析后的 JSON 正文作为关联数组。

参数是在每个端点的 args 键下定义为映射的(位于您的 callback 选项旁边)。此映射使用参数的名称作为键,值为该参数的选项映射。此数组可以包含 default、required、sanitize_callback 和 validate_callback 的键。

  • default:如果未提供参数,则用作参数的默认值。
  • required:如果定义为 true 且未为该参数传递值,则将返回错误。如果设置了默认值,则此设置无效,因为该参数始终会有值。
  • validate_callback:用于传递一个函数,该函数将接收参数的值。如果值有效,该函数应返回 true,否则返回 false。
  • sanitize_callback:用于传递一个函数,在将参数值传递给主回调之前使用该函数对值进行清理。

使用 sanitize_callback 和 validate_callback 可以让主回调仅处理请求,并使用 WP_REST_Response 类准备要返回的数据。通过使用这两个回调,在处理时可以安全地假设您的输入是有效且安全的。

利用我们之前的示例,我们可以确保传入的参数始终是一个数字:

<?php
add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_func',
    'args' => array(
      'id' => array(
        'validate_callback' => function($param, $request, $key) {
          return is_numeric( $param );
        }
      ),
    ),
  ) );
} );

您也可以将函数名传递给 validate_callback,但直接传递某些函数(如 is_numeric)不仅会发出关于向其传递了额外参数的警告,还会返回 NULL,导致回调函数被用无效数据调用。我们希望 最终在 WordPress core 中解决此问题。

我们也可以使用类似 'sanitize_callback' => 'absint' 的方式,但验证将抛出错误,使客户端了解他们做错了什么。当您更希望更改输入数据而不是抛出错误时(例如无效的 HTML),清理是非常有用的。

返回值

您的回调调用后,返回值将被转换为 JSON 并返回给客户端。这允许您返回任何形式的数据。在我们上面的示例中,我们返回的是字符串或 null,API 会自动处理并将它们转换为 JSON。

像任何其他 WordPress 函数一样,您也可以返回一个 WP_Error 实例。此错误信息将连同 500 Internal Service Error 状态码一起传递给客户端。您可以通过在 WP_Error 实例数据中将 status 选项设置为代码(例如,对于不良输入数据设置为 400)来进一步自定义您的错误。
利用我们之前的示例,我们现在可以返回一个错误实例:

<?php
/**
 * 获取某作者的最新帖子标题!
 *
 * @param array $data 函数的选项。
 * @return string|null 最新帖子的标题,
 * 如果没有则返回 null。
 */
function my_awesome_func( $data ) {
  $posts = get_posts( array(
    'author' => $data['id'],
  ) );

  if ( empty( $posts ) ) {
    return new WP_Error( 'no_author', 'Invalid author', array( 'status' => 404 ) );
  }

  return $posts[0]->post_title;
}

当作者没有任何属于他们的帖子时,这将向客户端返回 404 Not Found 错误:

HTTP/1.1 404 Not Found

[{
   "code": "no_author",
   "message": "Invalid author",
   "data": { "status": 404 }
}]

对于更高级的用法,您可以返回一个 WP_REST_Response 对象。该对象“封装”了正常的正文数据,但允许您返回自定义状态码或自定义头信息。您还可以为响应添加链接。使用此功能最快的方式是通过构造函数:

<?php
$data = array( 'some', 'response', 'data' );

// 创建响应对象
$response = new WP_REST_Response( $data );

// 添加自定义状态码
$response->set_status( 201 );

// 添加自定义头信息
$response->header( 'Location', 'http://example.com/' );

在封装现有的回调函数时,您始终应对返回值使用 rest_ensure_response()。这会将端点返回的原始数据自动转换为 WP_REST_Response。(注意:WP_Error 不会被转换为 WP_REST_Response,以便进行正确的错误处理。)

重要的是,REST API 路由的回调函数应始终返回数据;它不应尝试自行发送响应体。这确保了 REST API 服务器执行的额外处理(如处理链接/嵌入、发送头信息等)能够正常进行。换句话说,不要调用 die( wp_json_encode( $data ) ); 或 wp_send_json( $data )。自 WordPress 5.5 起,如果在 REST API 请求期间使用了 wp_send_json() 系列函数,将发出 _doing_it_wrong 通知。

在使用 REST API 时,从您的回调函数返回 WP_REST_Response 或 WP_Error 对象。

权限回调

您还必须为端点注册一个权限回调函数。这是一个在调用实际回调之前检查用户是否可以执行操作(读取、更新等)的函数。这使得 API 能够在无需先尝试请求的情况下,告知客户端他们可以对给定 URL 执行哪些操作。

此回调可以注册为 permission_callback,同样位于端点选项中您的 callback 选项旁边。此回调应返回布尔值或 WP_Error 实例。如果此函数返回 true,则处理响应。如果返回 false,将返回默认错误消息,并且请求不会继续处理。如果返回 WP_Error,则该错误将返回给客户端。

权限回调在远程身份验证之后运行,该过程会设置当前用户。这意味着您可以使用 current_user_can 来检查已认证的用户是否具有执行相应操作的适当权限,或者基于当前用户 ID 进行任何其他检查。在可能的情况下,应始终使用 current_user_can;与其检查用户是否已登录(身份验证),不如检查他们是否可以执行该操作(授权)。

一旦注册了 permission_callback,您将需要对其请求进行身份验证(例如通过包含 nonce 参数),否则将收到 rest_forbidden 错误。有关更多详细信息,请参阅身份验证。

继续使用我们之前的示例,我们可以使其仅允许编辑者及以上级别查看该作者数据。我们可以在此处检查多种不同的权限,但最佳选择是 edit_others_posts,这实际上是编辑者的核心职责。为此,我们只需要一个回调:

<?php
add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_func',
    'args' => array(
      'id' => array(
        'validate_callback' => 'is_numeric'
      ),
    ),
    'permission_callback' => function () {
      return current_user_can( 'edit_others_posts' );
    }
  ) );
} );

请注意,权限回调还将 Request 对象作为第一个参数接收,因此如果需要,您可以基于请求参数进行检查。

自 WordPress 5.5 起,如果未提供 permission_callback,REST API 将发出 _doing_it_wrong 通知。

myplugin/v1/author 的 REST API 路由定义缺少必需的 permission_callback 参数。对于旨在公开的 REST API 路由,请使用 __return_true 作为权限回调。

如果您的 REST API 端点是公开的,您可以使用 __return_true 作为权限回调。

<?php
add_action( 'rest_api_init', function () {
  register_rest_route( 'myplugin/v1', '/author/(?P<id>\d+)', array(
    'methods' => 'GET',
    'callback' => 'my_awesome_func',
    'permission_callback' => '__return_true',
  ) );
} );

发现

如果您想为您的自定义端点启用资源发现,可以使用 rest_queried_resource_route 过滤器来实现。例如,考虑一个包含您的自定义资源 ID 的自定义查询变量 my-route。以下代码片段将在使用 my-route 查询变量时添加一个发现链接。

function my_plugin_rest_queried_resource_route( $route ) {
    $id = get_query_var( 'my-route' );
    if ( ! $route && $id ) {
        $route = '/my-ns/v1/items/' . $id;
    }

    return $route;
}
add_filter( 'rest_queried_resource_route', 'my_plugin_rest_queried_resource_route' );

注意:如果您的端点描述的是自定义文章类型或自定义分类法,您很可能应该使用 rest_route_for_post 或 rest_route_for_term 过滤器。

控制器模式

控制器模式是与 API 配合处理复杂端点的最佳实践。

建议您在阅读本节之前先阅读“扩展内部类”。这样做将使您熟悉默认路由使用的模式,这是最佳实践。虽然不要求用于处理请求的类必须扩展 WP_REST_Controller 类或其子类,但这样做可以让您继承这些类中完成的工作。此外,您可以放心地根据所使用的控制器方法遵循最佳实践。

从根本上说,控制器只是一组名称通用的方法,用于匹配 REST 约定,以及一些方便的辅助函数。控制器在 register_routes 方法中注册其路由,通过 get_items、get_item、create_item、update_item 和 delete_item 响应请求,并具有名称相似的权限检查方法。遵循此模式可确保您不会遗漏端点中的任何步骤或功能。

要使用控制器,您需要首先对基础控制器进行子类化。这会为您提供一组基础方法,供您添加自己的行为。

子类化控制器后,我们需要实例化该类以使其生效。这应在钩入 rest_api_init 的回调函数内完成,这样可以确保仅在需要时才实例化该类。标准的控制器模式是在此回调中调用 $controller->register_routes(),该类随后可以在其中注册其端点。

示例

以下是“入门级”自定义路由:

<?php

class Slug_Custom_Route extends WP_REST_Controller {

  /**
   * 注册控制器对象的路线。
   */
  public function register_routes() {
    $version = '1';
    $namespace = 'vendor/v' . $version;
    $base = 'route';
    register_rest_route( $namespace, '/' . $base, array(
      array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => array( $this, 'get_items' ),
        'permission_callback' => array( $this, 'get_items_permissions_check' ),
        'args'                => array(

        ),
      ),
      array(
        'methods'             => WP_REST_Server::CREATABLE,
        'callback'            => array( $this, 'create_item' ),
        'permission_callback' => array( $this, 'create_item_permissions_check' ),
        'args'                => $this->get_endpoint_args_for_item_schema( true ),
      ),
    ) );
    register_rest_route( $namespace, '/' . $base . '/(?P<id>[\d]+)', array(
      array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => array( $this, 'get_item' ),
        'permission_callback' => array( $this, 'get_item_permissions_check' ),
        'args'                => array(
          'context' => array(
            'default' => 'view',
          ),
        ),
      ),
      array(
        'methods'             => WP_REST_Server::EDITABLE,
        'callback'            => array( $this, 'update_item' ),
        'permission_callback' => array( $this, 'update_item_permissions_check' ),
        'args'                => $this->get_endpoint_args_for_item_schema( false ),
      ),
      array(
        'methods'             => WP_REST_Server::DELETABLE,
        'callback'            => array( $this, 'delete_item' ),
        'permission_callback' => array( $this, 'delete_item_permissions_check' ),
        'args'                => array(
          'force' => array(
            'default' => false,
          ),
        ),
      ),
    ) );
    register_rest_route( $namespace, '/' . $base . '/schema', array(
      'methods'  => WP_REST_Server::READABLE,
      'callback' => array( $this, 'get_public_item_schema' ),
    ) );
  }

  /**
   * 获取集合中的项目列表
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|WP_REST_Response
   */
  public function get_items( $request ) {
    $items = array(); //执行查询,调用其他类等
    $data = array();
    foreach( $items as $item ) {
      $itemdata = $this->prepare_item_for_response( $item, $request );
      $data[] = $this->prepare_response_for_collection( $itemdata );
    }

    return new WP_REST_Response( $data, 200 );
  }

  /**
   * 从集合中获取单个项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|WP_REST_Response
   */
  public function get_item( $request ) {
    //从请求中获取参数
    $params = $request->get_params();
    $item = array();//执行查询,调用其他类等
    $data = $this->prepare_item_for_response( $item, $request );

    //根据某些条件返回响应或错误
    if ( 1 == 1 ) {
      return new WP_REST_Response( $data, 200 );
    } else {
      return new WP_Error( 'code', __( 'message', 'text-domain' ) );
    }
  }

  /**
   * 从集合中创建一个项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|WP_REST_Response
   */
  public function create_item( $request ) {
    $item = $this->prepare_item_for_database( $request );

    if ( function_exists( 'slug_some_function_to_create_item' ) ) {
      $data = slug_some_function_to_create_item( $item );
      if ( is_array( $data ) ) {
        return new WP_REST_Response( $data, 200 );
      }
    }

    return new WP_Error( 'cant-create', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
  }

  /**
   * 更新集合中的一个项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|WP_REST_Response
   */
  public function update_item( $request ) {
    $item = $this->prepare_item_for_database( $request );

    if ( function_exists( 'slug_some_function_to_update_item' ) ) {
      $data = slug_some_function_to_update_item( $item );
      if ( is_array( $data ) ) {
        return new WP_REST_Response( $data, 200 );
      }
    }

    return new WP_Error( 'cant-update', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
  }

  /**
   * 删除集合中的一个项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|WP_REST_Response
   */
  public function delete_item( $request ) {
    $item = $this->prepare_item_for_database( $request );

    if ( function_exists( 'slug_some_function_to_delete_item' ) ) {
      $deleted = slug_some_function_to_delete_item( $item );
      if ( $deleted ) {
        return new WP_REST_Response( true, 200 );
      }
    }

    return new WP_Error( 'cant-delete', __( 'message', 'text-domain' ), array( 'status' => 500 ) );
  }

  /**
   * 检查给定请求是否有权限获取项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|bool
   */
  public function get_items_permissions_check( $request ) {
    //return true; <--使用此选项使所有用户可读
    return current_user_can( 'edit_something' );
  }

  /**
   * 检查给定请求是否有权限获取特定项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|bool
   */
  public function get_item_permissions_check( $request ) {
    return $this->get_items_permissions_check( $request );
  }

  /**
   * 检查给定请求是否有权限创建项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|bool
   */
  public function create_item_permissions_check( $request ) {
    return current_user_can( 'edit_something' );
  }

  /**
   * 检查给定请求是否有权限更新特定项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|bool
   */
  public function update_item_permissions_check( $request ) {
    return $this->create_item_permissions_check( $request );
  }

  /**
   * 检查给定请求是否有权限删除特定项目
   *
   * @param WP_REST_Request $request 请求的完整数据。
   * @return WP_Error|bool
   */
  public function delete_item_permissions_check( $request ) {
    return $this->create_item_permissions_check( $request );
  }

  /**
   * 准备用于创建或更新操作的项目
   *
   * @param WP_REST_Request $request 请求对象
   * @return WP_Error|object $prepared_item
   */
  protected function prepare_item_for_database( $request ) {
    return array();
  }

  /**
   * 准备用于 REST 响应的项目
   *
   * @param mixed $item WordPress 表示的项目。
   * @param WP_REST_Request $request 请求对象。
   * @return mixed
   */
  public function prepare_item_for_response( $item, $request ) {
    return array();
  }

  /**
   * 获取集合的查询参数
   *
   * @return array
   */
  public function get_collection_params() {
    return array(
      'page'     => array(
        'description'       => '集合的当前页码。',
        'type'              => 'integer',
        'default'           => 1,
        'sanitize_callback' => 'absint',
      ),
      'per_page' => array(
        'description'       => '结果集中返回的最大项目数。',
        'type'              => 'integer',
        'default'           => 10,
        'sanitize_callback' => 'absint',
      ),
      'search'   => array(
        'description'       => '限制结果仅匹配字符串。',
        'type'              => 'string',
        'sanitize_callback' => 'sanitize_text_field',
      ),
    );
  }
}