inpsyde / wp-rest-starter
用于以面向对象方式处理WordPress REST API的入门包。
Requires
- php: ^7.0
- guzzlehttp/psr7: ~1.0
- psr/http-message: ~1.0
Requires (Dev)
- brain/monkey: ^1.4
- mockery/mockery: ~0.9
- phpunit/phpunit: ^6.0
README
用于以面向对象方式处理WordPress REST API的入门包。
介绍
自从WordPress REST API的基础设施和第一组端点合并到核心之后,插件和主题作者加入这个行列是很明显的。这个包为你提供了开始感觉像RESTful的一切。
WP REST Starter 包含了数据类型和业务逻辑的几个接口,并提供适合常见需求的直接实现。你只需要配置你的REST路由和数据结构,然后实现相应的请求处理器。
目录
安装
使用 Composer 安装
$ composer require inpsyde/wp-rest-starter
运行测试
$ vendor/bin/phpunit
需求
此包需要PHP 7或更高版本。
将自定义字段添加到现有资源需要WordPress 4.7或更高版本,或者WP REST API插件。如果您只想定义自定义REST路由,WordPress 4.4或更高版本已经足够。
使用
以下部分将帮助您以面向对象的方式开始使用WordPress REST API。如果您对WordPress REST API的使用还不太熟悉,请参阅REST API手册。
动作
为了通知某些事件,一些提供的类为您提供了自定义动作。以下为每个动作的简要描述以及如何执行动作的代码示例。
wp_rest_starter.register_fields
当使用默认字段注册类Inpsyde\WPRESTStarter\Core\Field\Registry
时,此动作在字段注册之前触发。
参数
$fields
Inpsyde\WPRESTStarter\Common\Field\Collection
:字段集合对象。
用法示例
<?php use Inpsyde\WPRESTStarter\Common\Field\Collection; add_action( 'wp_rest_starter.register_fields', function ( Collection $fields ) { // Remove a specific field from all post resources. $fields->delete( 'post', 'some-field-with-sensible-data' ); } );
wp_rest_starter.register_routes
当使用默认的路由注册类 Inpsyde\WPRESTStarter\Core\Route\Registry
时,此操作会在注册路由之前触发。
参数
$routes
Inpsyde\WPRESTStarter\Common\Route\Collection
:路由集合对象。$namespace
string
:命名空间。
用法示例
<?php use Inpsyde\WPRESTStarter\Common\Route\Collection; use Inpsyde\WPRESTStarter\Core\Route\Options; use Inpsyde\WPRESTStarter\Core\Route\Route; add_action( 'wp_rest_starter.register_routes', function ( Collection $routes, string $namespace ) { if ( 'desired-namespace' !== $namespace ) { return; } // Register a custom REST route in the desired namespace. $routes->add( new Route( 'some-custom-route/maybe-even-with-arguments', Options::with_callback( 'some_custom_request_handler_callback' ) ) ); }, 10, 2 );
注册简单自定义路由
以下简单的示例代码展示了如何注册一个自定义路由,具有读取和创建单个资源(s)的端点。
<?php use Inpsyde\WPRESTStarter\Core\Route\Collection; use Inpsyde\WPRESTStarter\Core\Route\Options; use Inpsyde\WPRESTStarter\Core\Route\Registry; use Inpsyde\WPRESTStarter\Core\Route\Route; use Inpsyde\WPRESTStarter\Factory; add_action( 'rest_api_init', function() { // Create a new route collection. $routes = new Collection(); // Optional: Create a permission callback factory. $permission = new Factory\PermissionCallback(); $endpoint_base = 'some-data-type'; // Set up the request handler. /** @var $handler Inpsyde\WPRESTStarter\Common\Endpoint\RequestHandler */ $handler = new Some\Custom\ReadRequestHandler( /* ...args */ ); // Optional: Set up an according endpoint $args object. /** @var $handler Inpsyde\WPRESTStarter\Common\Arguments */ $args = new Some\Custom\ReadArguments(); // Register a route for the READ endpoint. $routes->add( new Route( $endpoint_base . '(?:/(?P<id>\d+))?', Options::from_arguments( $handler, $args ) ) ); // Set up the request handler. /** @var $handler Inpsyde\WPRESTStarter\Common\Endpoint\RequestHandler */ $handler = new Some\Custom\CreateRequestHandler( /* ...args */ ); // Optional: Set up an according endpoint $args object. /** @var $handler Inpsyde\WPRESTStarter\Common\Arguments */ $args = new Some\Custom\CreateArguments(); // Register a route for the CREATE endpoint. $routes->add( new Route( $endpoint_base, Options::from_arguments( $handler, $args, WP_REST_Server::CREATABLE, [ // Optional: Set a callback to check permission. 'permission_callback' => $permission->current_user_can( 'edit_posts', 'custom_cap' ), ] ) ) ); // Register all routes in your desired namespace. ( new Registry( 'some-namespace-here' ) )->register_routes( $routes ); } );
注册复杂自定义路由
以下是一个更完整(因此更复杂)的自定义路由注册示例。通过使用相应的模式对象来描述资源的性质。端点模式对象和请求处理器都了解其他方为其单个资源注册的额外字段。响应对象也包含链接(如果支持,则是紧凑的)。
<?php use Inpsyde\WPRESTStarter\Core\Endpoint; use Inpsyde\WPRESTStarter\Core\Field; use Inpsyde\WPRESTStarter\Core\Request; use Inpsyde\WPRESTStarter\Core\Response; use Inpsyde\WPRESTStarter\Core\Route\Collection; use Inpsyde\WPRESTStarter\Core\Route\Options; use Inpsyde\WPRESTStarter\Core\Route\Registry; use Inpsyde\WPRESTStarter\Core\Route\Route; use Inpsyde\WPRESTStarter\Factory; add_action( 'rest_api_init', function() { $namespace = 'some-namespace-here'; // Create a new route collection. $routes = new Collection(); // Optional: Create a field access object. $field_access = new Field\Access(); // Optional: Create a request field processor object. $request_field_processor = new Request\FieldProcessor( $field_access ); // Optional: Create an endpoint schema field processor object. $schema_field_processor = new Endpoint\FieldProcessor( $field_access ); // Create a permission callback factory. $permission = new Factory\PermissionCallback(); // Create a response data access object. $response_data_access = new Response\LinkAwareDataAccess(); // Create a response factory. $response_factory = new Factory\ResponseFactory(); // Set up a field-aware schema object. /** @var $schema Inpsyde\WPRESTStarter\Common\Endpoint\Schema */ $schema = new Some\Endpoint\Schema( $schema_field_processor ); $base = $schema->title(); // Optional: Set up a formatter taking care of data preparation. $formatter = new Some\Endpoint\Formatter( $schema, $namespace, new Response\SchemaAwareDataFilter( $schema ), $response_factory, $response_data_access ); // Register a route for the READ endpoint. $routes->add( new Route( $base . '(?:/(?P<id>\d+))?', Options::from_arguments( new Some\Endpoint\ReadRequestHandler( $maybe_some_external_api, $formatter, $schema, $request_field_processor, $response_factory ), new Some\Endpoint\ReadEndpointArguments() )->set_schema( $schema ) ) ); // Register a route for the CREATE endpoint. $routes->add( new Route( $base, Options::from_arguments( new Some\Endpoint\CreateRequestHandler( $maybe_some_external_api, $formatter, $schema, $request_field_processor, $response_factory ), new Some\Endpoint\CreateEndpointArguments(), WP_REST_Server::CREATABLE, [ // Optional: Set a callback to check permission. 'permission_callback' => $permission->current_user_can( 'edit_posts', 'custom_cap' ), ] )->set_schema( $schema ) ) ); // Optional: Register a route for the endpoint schema. $routes->add( new Route( $base . '/schema', Options::with_callback( [ $schema, 'definition' ] ) ) ); // Register all routes in your desired namespace. ( new Registry( $namespace ) )->register_routes( $routes ); } );
将自定义字段添加到现有端点的响应中
以下示例展示了如何将两个额外字段注册到目标资源的所有响应对象中。当然,创建响应的相关代码必须意识到额外的字段。当代码使用 WP_REST_Controller
类或本包中提供的(自定义)字段处理器接口实现时,就会发生这种情况。
<?php use Inpsyde\WPRESTStarter\Core\Field\Collection; use Inpsyde\WPRESTStarter\Core\Field\Field; use Inpsyde\WPRESTStarter\Core\Field\Registry; add_action( 'rest_api_init', function() { // Create a new field collection. $fields = new Collection(); // Optional: Set up the field reader. /** @var $reader Inpsyde\WPRESTStarter\Common\Field\Reader */ $reader = new Some\Field\Reader(); // Optional: Set up the field updater. /** @var $updater Inpsyde\WPRESTStarter\Common\Field\Updater */ $updater = new Some\Field\Updater(); // Optional: Create a field schema. /** @var $schema Inpsyde\WPRESTStarter\Common\Schema */ $schema = new Some\Field\Schema(); // Create a readable and updatable field for some resource. $field = new Field( 'has_explicit_content' ); $field->set_get_callback( $reader ); $field->set_update_callback( $updater ); $field->set_schema( $schema ); // Add the field. $fields->add( 'some-data-type', $field ); // Set up the field reader. /** @var $reader Inpsyde\WPRESTStarter\Common\Field\Reader */ $reader = new Other\Field\Reader(); // Create another read-only field for some resource. $field = new Field( 'is_long_read' ); $field->set_get_callback( $reader ); // Add the field. $fields->add( 'some-data-type', $field ); // Register all fields. ( new Registry() )->register_fields( $fields ); } );
示例端点模式实现
以下端点模式实现了解其他方为当前资源注册的字段。通过注入(或默认)端点模式字段处理器对象,将所有注册的模式感知字段的数据添加到模式属性中。
<?php use Inpsyde\WPRESTStarter\Common\Endpoint\FieldProcessor; use Inpsyde\WPRESTStarter\Common\Endpoint\Schema; use Inpsyde\WPRESTStarter\Core; class SomeEndpointSchema implements Schema { /** * @var FieldProcessor */ private $field_processor; /** * @var string */ private $title ='some-data-type'; /** * Constructor. Sets up the properties. * * @param FieldProcessor $field_processor Optional. Field processor object. Defaults to null. */ public function __construct( FieldProcessor $field_processor = null ) { $this->field_processor = $field_processor ?? new Core\Endpoint\FieldProcessor(); } /** * Returns the properties of the schema. * * @return array Properties definition. */ public function properties(): array { $properties = [ 'id' => [ 'description' => __( "The ID of the data object.", 'some-text-domain' ), 'type' => 'integer', 'context' => [ 'view', 'edit' ], ], ]; return $this->field_processor->add_fields_to_properties( $properties, $this->title ); } /** * Returns the schema definition. * * @return array Schema definition. */ public function definition(): array { return [ '$schema' => 'https://json-schema.fullstack.org.cn/draft-04/schema#', 'title' => $this->title, 'type' => 'object', 'properties' => $this->properties(), ]; } /** * Returns the schema title. * * @return string Schema title. */ public function title(): string { return $this->title; } }
示例端点参数实现
端点参数实现很简单,在大多数情况下只需要一个返回硬编码数组的单方法。以下代码还包含一个验证回调,在失败时返回一个 WP_Error
对象。
<?php use Inpsyde\WPRESTStarter\Common\Arguments; use Inpsyde\WPRESTStarter\Factory\ErrorFactory; class SomeEndpointArguments implements Arguments { /** * @var ErrorFactory */ private $error_factory; /** * Constructor. Sets up the properties. * * @since 1.0.0 * * @param ErrorFactory $error_factory Optional. Error factory object. Defaults to null. */ public function __construct( ErrorFactory $error_factory = null ) { $this->error_factory = $error_factory ?? new ErrorFactory(); } /** * Returns the arguments in array form. * * @return array[] Arguments array. */ public function to_array(): array { return [ 'id' => [ 'description' => __( "The ID of a data object.", 'some-text-domain' ), 'type' => 'integer', 'minimum' => 1, 'required' => true, 'validate_callback' => function ( $value ) { if ( is_numeric( $value ) ) { return true; } return $this->error_factory->create( [ 'no_numeric_id', __( "IDs have to be numeric.", 'some-text-domain' ), [ 'status' => 400, ], ] ); }, ], 'type' => [ 'description' => __( "The type of the data object.", 'some-text-domain' ), 'type' => 'string', 'default' => 'foo', ], ]; } }
示例请求处理器实现
此(更新)请求处理器了解额外的字段。它使用外部API来处理数据。数据准备由专门的格式化器完成。
<?php use Inpsyde\WPRESTStarter\Common\Endpoint; use Inpsyde\WPRESTStarter\Common\Request\FieldProcessor; use Inpsyde\WPRESTStarter\Core; use Inpsyde\WPRESTStarter\Factory\ResponseFactory; use Some\Endpoint\Formatter; use Some\External\API; class SomeRequestHandler implements Endpoint\RequestHandler { /** * @var API */ private $api; /** * @var FieldProcessor */ private $field_processor; /** * @var Formatter */ private $formatter; /** * @var string */ private $object_type; /** * @var ResponseFactory */ private $response_factory; /** * Constructor. Sets up the properties. * * @param API $api API object. * @param Formatter $formatter Formatter object. * @param Endpoint\Schema $schema Optional. Schema object. Defaults to null. * @param FieldProcessor $field_processor Optional. Field processor object. Defaults to null. * @param ResponseFactory $response_factory Optional. Response factory object. Defaults to null. */ public function __construct( API $api, Formatter $formatter, Endpoint\Schema $schema = null, FieldProcessor $field_processor = null, ResponseFactory $response_factory = null ) { $this->api = $api; $this->formatter = $formatter; $this->object_type = $schema ? $schema->title() : ''; $this->field_processor = $field_processor ?? new Core\Request\FieldProcessor(); $this->response_factory = $response_factory ?? new ResponseFactory(); } /** * Handles the given request object and returns the according response object. * * @param WP_REST_Request $request Request object. * * @return WP_REST_Response Response. */ public function handle_request( WP_REST_Request $request ): WP_REST_Response { $id = $request['id']; // Update the according object data by using the injected data API. if ( ! $this->api->update_data( $id, $request->get_body_params() ) ) { // Ooops! Send an error response. return $this->response_factory->create( [ [ 'code' => 'could_not_update', 'message' => __( "The object could not be updated.", 'some-text-domain' ), 'data' => $request->get_params(), ], 400, ] ); } // Get the (updated) data from the API. $data = $this->api->get_data( $id ); // Set the request context. $context = $request['context'] ?? 'view'; // Prepare the data for the response. $data = $this->formatter->format( $data, $context ); // Update potential fields registered for the resource. $this->field_processor->update_fields_for_object( $data, $request, $this->object_type ); // Add the data of potential fields registered for the resource. $data = $this->field_processor->add_fields_to_object( $data, $request, $this->object_type ); // Send a response object containing the updated data. return $this->response_factory->create( [ $data ] ); } }
示例字段模式实现
字段的模式不过是一个数组形式的定义。
<?php use Inpsyde\WPRESTStarter\Common\Schema; class SomeFieldSchema implements Schema { /** * Returns the schema definition. * * @return array Schema definition. */ public function definition(): array { return [ 'description' => __( "Whether the object contains explicit content.", 'some-text-domain' ), 'type' => 'boolean', 'context' => [ 'view', 'edit' ], ]; } }
示例字段读取器实现
以下字段读取器实现使用全局回调来获取字段值。您也可以注入API对象并使用提供的方法。
<?php use Inpsyde\WPRESTStarter\Common\Field\Reader; class SomeFieldReader implements Reader { /** * Returns the value of the field with the given name of the given object. * * @param array $object Object data in array form. * @param string $field_name Field name. * @param WP_REST_Request $request Request object. * @param string $object_type Optional. Object type. Defaults to empty string. * * @return mixed Field value. */ public function get_value( array $object, string $field_name, WP_REST_Request $request, string $object_type = '' ) { if ( empty( $object['id'] ) ) { return false; } return (bool) some_field_getter_callback( $object['id'], $field_name ); } }
示例字段更新器实现
以下字段更新器实现使用全局回调来更新字段值。您也可以注入一个API对象并使用提供的方法。注入的权限回调(如果有),在更新字段之前用于检查权限。
<?php use Inpsyde\WPRESTStarter\Common\Field\Updater; class SomeFieldUpdater implements Updater { /** * @var callable */ private $permission_callback; /** * Constructor. Sets up the properties. * * @param callable $permission_callback Optional. Permission callback. Defaults to null. */ public function __construct( $permission_callback = null ) { if ( is_callable( $permission_callback ) ) { $this->permission_callback = $permission_callback; } } /** * Updates the value of the field with the given name of the given object to the given value. * * @param mixed $value New field value. * @param object $object Object data. * @param string $field_name Field name. * @param WP_REST_Request $request Optional. Request object. Defaults to null. * @param string $object_type Optional. Object type. Defaults to empty string. * * @return bool Whether or not the field was updated successfully. */ public function update_value( $value, $object, string $field_name, WP_REST_Request $request = null, string $object_type = '' ): bool { if ( $this->permission_callback && ! ( $this->permission_callback )() ) { return false; } if ( empty( $object->id ) ) { return false; } return some_field_updater_callback( $object->id, $field_name, (bool) $value ); } }
示例格式化器实现
将请求处理和响应数据准备分开处理是个好主意。因此,以下代码显示了可能的格式化器,尽管它实际上不是这个包的一部分。
<?php use Inpsyde\WPRESTStarter\Common\Endpoint\Schema; use Inpsyde\WPRESTStarter\Common\Response\DataAccess; use Inpsyde\WPRESTStarter\Common\Response\DataFilter; use Inpsyde\WPRESTStarter\Core\Response\LinkAwareDataAccess; use Inpsyde\WPRESTStarter\Core\Response\SchemaAwareDataFilter; use Inpsyde\WPRESTStarter\Factory\ResponseFactory; class SomeFormatter { /** * @var string */ private $link_base; /** * @var array */ private $properties; /** * @var DataAccess */ private $response_data_access; /** * @var DataFilter */ private $response_data_filter; /** * @var ResponseFactory */ private $response_factory; /** * Constructor. Sets up the properties. * * @param Schema $schema Schema object. * @param string $namespace Namespace. * @param DataFilter $response_data_filter Optional. Response data filter object. Defaults to null. * @param ResponseFactory $response_factory Optional. Response factory object. Defaults to null. * @param DataAccess $response_data_access Optional. Response data access object. Defaults to null. */ public function __construct( Schema $schema, string $namespace, DataFilter $response_data_filter = null, ResponseFactory $response_factory = null, DataAccess $response_data_access = null ) { $this->properties = $schema->properties(); $this->link_base = $namespace . '/' . $schema->title(); $this->response_data_filter = $response_data_filter ?? new SchemaAwareDataFilter( $schema ); $this->response_factory = $response_factory ?? new ResponseFactory(); $this->response_data_access = $response_data_access ?? new LinkAwareDataAccess(); } /** * Returns a formatted representation of the given data. * * @param array[] $raw_data Raw data. * @param string $context Optional. Request context. Defaults to 'view'. * * @return array The formatted representation of the given data. */ public function format( array $raw_data, string $context = 'view' ): array { $data = array_reduce( $raw_data, function ( array $data, array $set ) use ( $context ) { $item = [ 'id' => (int) ( $set['id'] ?? 0 ), 'name' => (string) ( $set['name'] ?? '' ), 'redirect' => (bool) ( $set['redirect'] ?? false ), ]; $item = $this->response_data_filter->filter_data( $item, $context ); $response = $this->get_response_with_links( $item, $set ); $data[] = $this->response_data_access->get_data( $response ); return $data; }, [] ); return $data; } /** * Returns a response object with the given data and all relevant links. * * @param array $data Response data. * @param array $set Single data set. * * @return WP_REST_Response The response object with the given data and all relevant links. */ private function get_response_with_links( array $data, array $set ): WP_REST_Response { $links = []; if ( isset( $set['id'] ) ) { $links['self'] = [ 'href' => rest_url( $this->link_base . '/' . absint( $set['id'] ) ), ]; } $links['collection'] = [ 'href' => rest_url( $this->link_base ), ]; $response = $this->response_factory->create( [ $data ] ); $response->add_links( $links ); return $response; } }
PSR-7
在PHP世界中,关于HTTP消息有一个标准(推荐):PSR-7。尽管有Calypso、Gutenberg以及JavaScript代码库的不断发展,WordPress仍然是用PHP编写的。那么,不是很好吗?不是可以像PHP世界的其他人那样做吗?难道没有一种方法可以利用所有现有的PSR-7中间件吗?
当然有!从3.1.0版本开始,WP REST Starter附带增强型、PSR-7兼容的WordPress REST请求和响应类,每个类都实现了相应的PSR-7 HTTP消息接口。使用这些类可以使您将现有的PSR-7中间件集成到您的RESTful WordPress项目中。
创建PSR-7兼容的WordPress REST请求
如果您对PSR-7兼容的WordPress REST请求对象感兴趣,当然可以自己创建一个新实例。您可以这样做,所有参数都是可选的
use Inpsyde\WPRESTStarter\Core\Request\Request; $request = new Request( $method, $route, $attributes );
然而,这不太可能,因为您通常不希望自己定义任何基于请求的数据,...因为它们已经包含在当前请求中。 :) 更可能的情况是,您想使现有的WordPress请求对象PSR-7兼容,如下所示
use Inpsyde\WPRESTStarter\Core\Request\Request; // ... $request = Request::from_wp_request( $request );
创建PSR-7兼容的WordPress REST响应
对于请求,您也可以自己创建一个新的响应对象。同样,所有参数都是可选的。
use Inpsyde\WPRESTStarter\Core\Response\Response; $response = new Response( $data, $status, $headers );
虽然与请求相比这可能有更多意义,但通常的情况是将现有的WordPress响应对象使PSR-7兼容,这可以这样做
use Inpsyde\WPRESTStarter\Core\Response\Response; // ... $response = Response::from_wp_response( $response );
使用PSR-7兼容的WordPress HTTP消息
一旦您使WordPress HTTP消息PSR-7兼容,您就可以将其传递到您的PSR-7中间件堆栈。由于您可以做几乎所有事情,以下示例只是做事情的一种方式。
// Hook into your desired filter. add_filter( 'rest_post_dispatch', function ( \WP_HTTP_Response $response, \WP_REST_Server $server, \WP_REST_Request $request ) { $logger = ( new Logger( 'access' ) )->pushHandler( new ErrorLogHandler() ); // Set up your middleware stack. $middlewares = [ Middleware::ResponseTime(), Middleware::ClientIp()->remote(), Middleware::Uuid(), Middleware::AccessLog( $logger )->combined(), ]; // Set up a middleware dispatcher. $dispatcher = ( new RelayBuilder() )->newInstance( $middlewares ); // Dispatch the request. return $dispatcher( Request::from_wp_request( $request ), Response::from_wp_response( $response ) ); }, 0, 3 );
许可
版权(c)2017 Inpsyde GmbH
此代码根据MIT许可授权。