WordPress中文开发手册

添加自定义端点

WordPress REST API不仅仅是一组默认路由。 它也是创建自定义路由和端点的工具。 WordPress前端提供了一组默认的URL映射,但是用于创建它们的工具(例如Rewrites API以及查询类:WP_Query,WP_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',
  ) );
} );

现在,我们只注册路由的一个端点。 (“Route”是URL,而“endpoint”是与之相对应的方法和URL),如果您的站点域是example.com,并且您保留了API路径的wp-json,那么完整的URL将是http://example.com/wp-json/myplugin/v1/author/(?P\d+)。每个路由都可以有任意数量的端点,对于每个端点,您可以定义允许的HTTP方法,用于响应该请求的回调函数和用于创建自定义权限的权限回调。此外,您可以在请求中定义允许的字段,并且每个字段指定默认值,清理回调,验证回调以及该字段是否必需。

命名空间

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

命名空间一般应遵循供应商/ v1的模式,供应商通常是您的插件或主题,而v1代表API的第一个版本。如果您需要破坏与新端点的兼容性,那么您可以将其限制到v2。

上述情况,两个具有相同名称的路由,来自两个不同的插件,要求所有供应商使用唯一的命名空间。如果没有这样做的话,就不会在主题或插件中使用供应商功能前缀,类前缀和/或类名称空间,这是非常_doing_it_wrong。

使用命名空间的另一个好处是客户端可以检测到对您的自定义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主体作为关联数组。

参数被定义为每个端点的键参数中的映射(在回调选项旁边)。此映射使用该键的参数的名称,其值为该参数的选项的映射。该数组可以包含默认的key,必需的,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核心解决这个问题。

我们也可以使用像'sanitize_callback'=>'absint'这样的东西,但验证会抛出一个错误,让客户了解他们做错了什么。当您宁愿更改正在输入的数据而不是抛出错误(例如无效的HTML)时,消毒是有用的。

返回值

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

像任何其他WordPress功能一样,您也可以返回WP_Error实例。该错误信息将传递给客户端,以及500个内部服务错误状态代码。您可以通过将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( 'awesome_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不会转换为WP_REST_Response以允许正确的错误处理。)

权限回调

您还可以注册端点的权限回调。这是一个功能,它检查用户是否可以在调用真正的回调之前执行动作(读取,更新等)。这样就可以让API告知客户他们可以在给定的URL上执行什么操作,而无需首先尝试该请求。

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

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

继续我们以前的例子,我们可以使它只有编辑或以上才能查看作者的数据。我们可以在这里检查一些不同的功能,但最好的是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对象作为第一个参数,因此如果需要,您可以根据请求参数进行检查。

##控制器模式

控制器模式是使用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_Request
   */
  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_Request
   */
  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_Request
   */
  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',
      ),
    );
  }
}