Controller Classes

This section describes all the PHP classes used in the WP API. They are divided into two types: Infrastructure Classes and Controller Classes (routes, endpoints, resources).

Overview

The WP API uses the Model-View-Controller principle — a standard model in application development. The essence of this model is that the application's logic is divided in such a way that one part can be changed without affecting the other part.

In this scheme, the logic of the WP-API code is divided into independent components (classes):

  • Request class.
  • Controller classes (processing requests and creating responses).
  • Response class (receives the response prepared by the controller and formats it into standard JSON view, sets response headers and response code).

The request and response classes are Infrastructure Classes of the WP API, and generally, they do not need to be worked with. We, the developers, will almost always work with the Controller Class.

The task of the Controller Class is to combine the separate parts of API request processing into a single mechanism. Routes should be created in them, they receive requests, process them, and generate responses. The resource schema is also described there.

The controller concept is adopted within the WP-API to have a standard template for Controller Classes - classes representing resources (endpoints). The template for the controller class is the abstract class WP_REST_Controller. Each controller class should have a similar method scheme, made so that all endpoints have the same names for PHP methods.

Infrastructure Classes and Controller Classes

Infrastructure classes complement controller classes - they handle the API logic but do not perform data transformations. Similarly, controller classes (endpoints) encapsulate the logic necessary for performing CRUD operations with resources but are not related to infrastructure logic. This is the aforementioned "Model-View-Controller" model.

The infrastructure classes include:

  • WP_REST_Server — the fundamental REST API controller that serves requests. For any request to the API, WP_REST_Server is first called to determine which route is requested, and then it passes the route callback to the WP_REST_Request object. WP_REST_Server also performs authentication checks and validation and access checks (validation and permissions).
  • WP_REST_Request — this object contains information about the request, such as request headers, parameters, and method, as well as the route itself. It can also validate and sanitize the request (validation and sanitization).
  • WP_REST_Response — responsible for the response to the request. It sets response headers, status, and response body (JSON). It provides helper methods add_link() (adds links related to the response) and query_navigation_headers() (navigation data in headers).

The controller classes include:

Example of registering a route via the Controller Class

For a detailed description of this example, see the section Creating Routes.

GitHub
// Запускаем наш контроллер и регистрируем маршруты
add_action( 'rest_api_init', 'prefix_register_my_rest_routes' );
function prefix_register_my_rest_routes() {
	$controller = new My_REST_Posts_Controller();
	$controller->register_routes();
}

class My_REST_Posts_Controller extends WP_REST_Controller {

	function __construct(){
		$this->namespace = 'my-namespace/v1';
		$this->rest_base = 'posts';
	}

	function register_routes(){

		register_rest_route( $this->namespace, "/$this->rest_base", [
			[
				'methods'             => 'GET',
				'callback'            => [ $this, 'get_items' ],
				'permission_callback' => [ $this, 'get_items_permissions_check' ],
			],
			'schema' => [ $this, 'get_item_schema' ],
		] );

		register_rest_route( $this->namespace, "/$this->rest_base/(?P<id>[\w]+)", [
			[
				'methods'   => 'GET',
				'callback'  => [ $this, 'get_item' ],
				'permission_callback' => [ $this, 'get_item_permissions_check' ],
			],
			'schema' => [ $this, 'get_item_schema' ],
		] );
	}

	function get_items_permissions_check( $request ){
		if ( ! current_user_can( 'read' ) )
			return new WP_Error( 'rest_forbidden', esc_html__( 'You cannot view the post resource.' ), [ 'status' => $this->error_status_code() ] );

		return true;
	}

	/**
	 * Получает последние посты и отдает их в виде rest ответа.
	 *
	 * @param WP_REST_Request $request Текущий запрос.
	 *
	 * @return WP_Error|array
	 */
	function get_items( $request ){
		$data = [];

		$posts = get_posts( [
			'post_per_page' => 5,
		] );

		if ( empty( $posts ) )
			return $data;

		foreach( $posts as $post ){
			$response = $this->prepare_item_for_response( $post, $request );
			$data[] = $this->prepare_response_for_collection( $response );
		}

		return $data;
	}

	## Проверка права доступа.
	function get_item_permissions_check( $request ){
		return $this->get_items_permissions_check( $request );
	}

	/**
	 * Получает отдельный ресурс.
	 *
	 * @param WP_REST_Request $request Текущий запрос.
	 *
	 * @return array
	 */
	function get_item( $request ){
		$id = (int) $request['id'];
		$post = get_post( $id );

		if( ! $post )
			return array();

		return $this->prepare_item_for_response( $post, $request );
	}

	/**
	 * Собирает данные ресурса в соответствии со схемой ресурса.
	 *
	 * @param WP_Post         $post    Объект ресурса, из которого будут взяты оригинальные данные.
	 * @param WP_REST_Request $request Текущий запрос.
	 *
	 * @return array
	 */
	function prepare_item_for_response( $post, $request ){

		$post_data = [];

		$schema = $this->get_item_schema();

		// We are also renaming the fields to more understandable names.
		if ( isset( $schema['properties']['id'] ) )
			$post_data['id'] = (int) $post->ID;

		if ( isset( $schema['properties']['content'] ) )
			$post_data['content'] = apply_filters( 'the_content', $post->post_content, $post );

		return $post_data;
	}

	/**
	 * Подготавливает ответ отдельного ресурса для добавления его в коллекцию ресурсов.
	 *
	 * @param WP_REST_Response $response Response object.
	 *                                   
	 * @return array|mixed Response data, ready for insertion into collection data.
	 */
	function prepare_response_for_collection( $response ){

		if ( ! ( $response instanceof WP_REST_Response ) ){
			return $response;
		}

		$data = (array) $response->get_data();
		$server = rest_get_server();

		if ( method_exists( $server, 'get_compact_response_links' ) ){
			$links = call_user_func( [ $server, 'get_compact_response_links' ], $response );
		}
		else {
			$links = call_user_func( [ $server, 'get_response_links' ], $response );
		}

		if ( ! empty( $links ) ){
			$data['_links'] = $links;
		}

		return $data;
	}

	## Схема ресурса.
	function get_item_schema(){
		$schema = [
			// показывает какую версию схемы мы используем - это draft 4
			'$schema'    => 'http://json-schema.org/draft-04/schema#',
			// определяет ресурс который описывает схема
			'title'      => 'vehicle',
			'type'       => 'object',
			// в JSON схеме нужно указывать свойства в атрибуете 'properties'.
			'properties' => [
				'id' => [
					'description' => 'Unique identifier for the object.',
					'type'        => 'integer',
					'context'     => [ 'view', 'edit', 'embed' ],
					'readonly'    => true,
				],
				'vin' => [
					'description' => 'VIN code of vehicle.',
					'type'        => 'string',
				],
				// TODO добавить поля
				// []
			],
		];

		return $schema;
	}

	## Устанавливает HTTP статус код для авторизации.
	function error_status_code(){
		return is_user_logged_in() ? 403 : 401;
	}

}

Principles of Controller Classes in WP API

Controller classes solve two problems when developing endpoints:

  • Lack of namespaces — WordPress does not use PHP namespaces (to support PHP 5.2). Wrapping a group of PHP functions that describe an endpoint in a class helps avoid conflicts with function names. For example, if we named a function get_items() and another developer did the same, the site would stop working due to a fatal PHP error.
  • Structure agreement — Once developers understand the structure of a controller class, they can quickly understand the structure of a controller class written by someone else.

For controller classes, developers have created a special template: the abstract class WP_REST_Controller. It is recommended to create your route controllers based on it. For example, all endpoint classes in WP extend WP_REST_Controller, such as:

class WP_REST_Posts_Controller extends WP_REST_Controller {
	// ...
}

Methods of the template class WP_REST_Controller:

Method Method Description
register_routes() called when the class instance is first created and registers routes.
get_items() retrieves a collection of resources (posts, categories, etc.).
get_item() retrieves a single resource (post, category, etc.). If the resource does not exist, HTTP status 404 will be returned. If there is no permission to view the resource, the status will be 403.
create_item() creates a new resource. If the creation is successful, WP_REST_Response will return HTTP status 201 and a link to the created resource. If creation fails, an appropriate error code will be returned in the HTTP header.
update_item() updates an existing resource.
delete_item() deletes an existing resource. If deletion fails, an appropriate error code will be returned in the HTTP header.
get_items_permissions_check() before calling the callback function, checks if the current request has permission to retrieve the collection of resources.
get_item_permissions_check() before calling the callback function, checks if the current request has permission to retrieve a single resource.
create_item_permissions_check() before calling the callback function, checks if the current request has permission to create a resource.
update_item_permissions_check() before calling the callback function, checks if the current request has permission to update a resource.
delete_item_permissions_check() before calling the callback function, checks if the current request has permission to delete a resource.
prepare_item_for_response() modifies the resource data to fit the response schema.
prepare_response_for_collection() prepare_item_for_response() returns a WP_REST_Response object. This is a helper function that collects such responses into a common resource collection and returns this collection as a response.
filter_response_by_context() filters the resource data according to the specified context parameter.
get_item_schema() retrieves the resource schema object.

Note on Class Inheritance

Do not abuse class inheritance. For example: if we wrote a controller class for the posts endpoint (the example above) and also want to support custom post types, we should not extend our My_REST_Posts_Controller class like this:

class My_CPT_REST_Controller extends My_REST_Posts_Controller {
	...
}

Instead, we should either create a separate controller class or make My_REST_Posts_Controller handle all available post types. The reason is that the parent class (the one being inherited from) may change, and our subclasses depend on it, resulting in headaches. Therefore, if a common structure for classes is needed, it is necessary to create a base controller class in the form of an interface or abstract class and then use it as a basis for subclasses. This approach of an abstract class is used in many WordPress REST API endpoints. For example, classes like WP_REST_Posts_Controller, WP_REST_Terms_Controller, etc., extend the abstract class WP_REST_Controller.