添加自定义端点

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

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

基础知识

所以您想向 API 添加自定义端点?极好的!让我们从一个简单的例子开始。
让我们从一个看起来像这样的简单函数开始:

<?php
/**
 * Grab latest post title by an author!
 *
 * @param array $data Options for the function.
 * @return string|null Post title for the latest,
 * or null if none.
 */
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+)

每个路由可以有任意数量的端点,对于每个端点,您可以定义允许的 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 ) {
  // You can access parameters via direct array access on the object:
  $param = $request['some_param'];

  // Or via the helper method:
  $param = $request->get_param( 'some_param' );

  // You can get the combined, merged set of parameters:
  $parameters = $request->get_params();

  // The individual sets of parameters are also available, if needed:
  $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();

  // Uploads aren't merged in, but can be accessed separately:
  $parameters = $request->get_file_params();
}

(要准确了解参数是如何合并的,请查看 的来源WP_REST_Request::get_parameter_order();基本顺序是正文、查询、URL,然后是默认值。)

通常,您将获得未更改的每个参数。但是,您可以在注册路由时注册参数,这样您就可以对这些参数进行清理和验证。

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

args参数被定义为每个端点(在您的选项旁边)的键中的映射callback。该映射使用键参数的名称,值是该参数的选项映射。default该数组可以包含、requiredsanitize_callback的键validate_callback

  • default: 如果未提供,则用作参数的默认值。
  • required:如果定义为 true,并且没有为该参数传递任何值,将返回错误。如果设置了默认值则没有效果,因为参数总是有一个值。
  • validate_callback:用于传递将传递参数值的函数。如果值有效,该函数应返回 true,否则返回 false。
  • sanitize_callback:用于传递一个函数,该函数用于在将参数值传递给主回调之前对其进行清理。

使用sanitize_callbackandvalidate_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 核心中解决这个问题。

我们也可以改用类似的东西'sanitize_callback' => 'absint',但验证会抛出错误,让客户了解他们做错了什么。当您宁愿更改输入的数据而不是抛出错误(例如无效的 HTML)时,清理很有用。

返回值

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

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

<?php
/**
 * Grab latest post title by an author!
 *
 * @param array $data Options for the function.
 * @return string|null Post title for the latest,
 * or null if none.
 */
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' );

// Create the response object
$response = new WP_REST_Response( $data );

// Add a custom status code
$response->set_status( 201 );

// Add a custom header
$response->header( 'Location', 'http://example.com/' );

包装现有回调时,您应该始终使用rest_ensure_response()返回值。这将获取从端点返回的原始数据,并自动将其转换为WP_REST_Response适合您的数据。(请注意,它WP_Error不会转换为 aWP_REST_Response以允许正确的错误处理。)

重要的是,REST API 路由的回调应该_始终_返回数据;它不应该尝试发送响应主体本身。这确保了 REST API 服务器进行的额外处理,如处理链接/嵌入、发送标头等……发生。换句话说,不要调用die( wp_json_encode( $data ) );or wp_send_json( $data )。从WordPress 5.5开始,如果在 REST API 请求期间使用函数系列,_doing_it_wrong则会发出通知。wp_send_json()

使用 REST API 时,从回调中 返回WP_REST_Response或WP_Error对象。

权限回调

您还必须为端点注册权限回调。这是一个函数,用于在调用真正的回调之前检查用户是否可以执行操作(阅读、更新等)。这允许 API 告诉客户端他们可以在给定的 URL 上执行什么操作,而无需先尝试请求。

此回调可以再次在您的选项permission_callback旁边的端点选项中注册为, 。callback此回调应返回布尔值或WP_Error实例。如果此函数返回 true,将处理响应。如果返回false,则返回默认错误信息,请求不会继续处理。如果它返回 a 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未提供a ,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_postrest_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 {

  /**
   * Register the routes for the objects of the 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' ),
    ) );
  }

  /**
   * Get a collection of items
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|WP_REST_Response
   */
  public function get_items( $request ) {
    $items = array(); //do a query, call another class, etc
    $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 );
  }

  /**
   * Get one item from the collection
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|WP_REST_Response
   */
  public function get_item( $request ) {
    //get parameters from request
    $params = $request->get_params();
    $item = array();//do a query, call another class, etc
    $data = $this->prepare_item_for_response( $item, $request );

    //return a response or error based on some conditional
    if ( 1 == 1 ) {
      return new WP_REST_Response( $data, 200 );
    } else {
      return new WP_Error( 'code', __( 'message', 'text-domain' ) );
    }
  }

  /**
   * Create one item from the collection
   *
   * @param WP_REST_Request $request Full data about the 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 ) );
  }

  /**
   * Update one item from the collection
   *
   * @param WP_REST_Request $request Full data about the 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 ) );
  }

  /**
   * Delete one item from the collection
   *
   * @param WP_REST_Request $request Full data about the 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 ) );
  }

  /**
   * Check if a given request has access to get items
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function get_items_permissions_check( $request ) {
    //return true; <--use to make readable by all
    return current_user_can( 'edit_something' );
  }

  /**
   * Check if a given request has access to get a specific item
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function get_item_permissions_check( $request ) {
    return $this->get_items_permissions_check( $request );
  }

  /**
   * Check if a given request has access to create items
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function create_item_permissions_check( $request ) {
    return current_user_can( 'edit_something' );
  }

  /**
   * Check if a given request has access to update a specific item
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function update_item_permissions_check( $request ) {
    return $this->create_item_permissions_check( $request );
  }

  /**
   * Check if a given request has access to delete a specific item
   *
   * @param WP_REST_Request $request Full data about the request.
   * @return WP_Error|bool
   */
  public function delete_item_permissions_check( $request ) {
    return $this->create_item_permissions_check( $request );
  }

  /**
   * Prepare the item for create or update operation
   *
   * @param WP_REST_Request $request Request object
   * @return WP_Error|object $prepared_item
   */
  protected function prepare_item_for_database( $request ) {
    return array();
  }

  /**
   * Prepare the item for the REST response
   *
   * @param mixed $item WordPress representation of the item.
   * @param WP_REST_Request $request Request object.
   * @return mixed
   */
  public function prepare_item_for_response( $item, $request ) {
    return array();
  }

  /**
   * Get the query params for collections
   *
   * @return array
   */
  public function get_collection_params() {
    return array(
      'page'     => array(
        'description'       => 'Current page of the collection.',
        'type'              => 'integer',
        'default'           => 1,
        'sanitize_callback' => 'absint',
      ),
      'per_page' => array(
        'description'       => 'Maximum number of items to be returned in result set.',
        'type'              => 'integer',
        'default'           => 10,
        'sanitize_callback' => 'absint',
      ),
      'search'   => array(
        'description'       => 'Limit results to those matching a string.',
        'type'              => 'string',
        'sanitize_callback' => 'sanitize_text_field',
      ),
    );
  }
}