В корпоративной разработке на WordPress 7.0+ создание пользовательских REST-маршрутов через анонимные замыкания в register_rest_route() считается антипаттерном, подходящим лишь для прототипов. Промышленным стандартом является наследование от базового абстрактного класса ядра WP_REST_Controller.
Архитектура WP_REST_Controller стандартизирует 4 фундаментальных слоя API-инженерии:
get_item_schema()): Контроллер описывает строгую JSON-схему входящих и исходящих данных. На ее основе WP_REST_Server автоматически генерирует правила args (get_endpoint_args_for_item_schema()), выполняя проверку типов (integer, string, enum, format: email | uri | date-time), диапазонов (minimum, maximum) и регулярных выражений (pattern) до запуска бизнес-логики.permission_callback): Каждый HTTP-глагол (READABLE, CREATABLE, EDITABLE, DELETABLE) обязан иметь собственный метод проверки прав (get_items_permissions_check, create_item_permissions_check), возвращающий bool или WP_Error со статусом 401/403.prepare_item_for_response()): Преобразование внутренних моделей БД в публичный DTO-контракт с фильтрацией полей по get_fields_for_response($request), что автоматически включает поддержку параметра ?_fields=....namespace: agentic-lab/v1): Изоляция API по семантическим версиям (v1, v2), позволяющая выпускать обратно несовместимые изменения схемы без поломки мобильных клиентов и внешних интеграций.Кастомный REST-контроллер следует воспринимать как защищенный пограничный шлюз (API Gateway Controller), который принимает неструктурированный HTTP-запрос, прогоняет его через фильтры Rate Limiting, JSON Schema и матрицу прав, и делегирует выполнение чистому доменному сервису.
WP_REST_Controller get_item_schema permission_callback get_fields_for_response Rate Limiting[HTTP POST /wp-json/agentic-lab/v1/leads] (JSON Body: { email, budget, stack })
│
▼
[1. WP_REST_Server: Валидация по get_item_schema()]
├── email: format => 'email' (Автоматическая проверка ядром!)
├── budget: type => 'integer', minimum => 1000, maximum => 500000
└── stack: type => 'array', items => ['type' => 'string']
│ (При ошибке ──► 400 Bad Request rest_invalid_param)
▼
[2. Авторизация & Защита: create_item_permissions_check()]
├── Проверка токена / Nonce / Capabilities
└── Проверка Rate Limiter (не более 5 запросов в минуту с IP)
│ (При отказе ──► 401 / 403 / 429 Too Many Requests)
▼
[3. Выполнение бизнес-логики: create_item(WP_REST_Request)]
├── Чтение уже очищенных параметров ($request->get_param('email'))
└── Вызов доменного сервиса сохранения заявки
│
▼
[4. Формирование ответа: prepare_item_for_response()]
├── Фильтрация полей через get_fields_for_response()
└── Возврат WP_REST_Response (Status: 201 Created)Регистрация маршрута на rest_api_init с автоматической привязкой аргументов валидации из get_item_schema().
Ядро проверяет типы, форматы (email, uri, uuid) и ограничения enum/min/max через rest_validate_value_from_schema().
Выполнение permission_callback: проверка прав пользователя и лимитов частоты обращений (Rate Limiting).
Исполнение основного метода контроллера (get_items / create_item) с гарантированно чистыми и типизированными данными.
Сборка ответа в prepare_item_for_response() с учетом маски _fields и добавлением гиперссылок HAL.
Инициализация свойств $this->namespace и $this->rest_base в конструкторе наследника WP_REST_Controller.
Версия в неймспейсе (v1, v2) гарантирует безопасную эволюцию контракта API.
Описание всех свойств ресурса, их типов (integer, string, enum), форматов (email, uri) и флагов readonly.
Метод get_endpoint_args_for_item_schema() автоматически превращает схему в правила валидации.
Раздельные методы проверки прав для чтения и записи с интеграцией защиты от перебора (Rate Limiting).
Возвращает true либо типизированный WP_Error с кодом 401, 403 или 429.
Получение уже провалидированных параметров через $request->get_param() и вызов доменного слоя.
Никаких обращений к $_POST и никаких вызовов echo/die().
Фильтрация полей через get_fields_for_response() и возврат WP_REST_Response со статусом 200 или 201.
Автоматически обеспечивает поддержку параметра ?_fields и пакетного режима Batch API.
В каком порядке диспетчер ядра WP_REST_Server выполняет проверку параметров и прав доступа при поступлении запроса?
/wp-json/agentic-lab/v1/webhooks/stripe с проверкой криптографической HMAC-подписи (hash_hmac('sha256', ...)) прямо внутри permission_callback./wp-json/agentic-lab/v1/dashboard-summary, отдающий скомпилированный срез данных из Redis за 4 мс.permission_callback проверяет IP/Fingerprint в объектном кэше, возвращая HTTP 429 Too Many Requests при превышении лимита 5 заявок в минуту).get_item_schema(), любой клиент может отправить запрос OPTIONS /wp-json/agentic-lab/v1/leads и получить полную спецификацию всех полей, типов и обязательных параметров.Сопоставим процедурную регистрацию маршрутов с объектно-ориентированной архитектурой WP_REST_Controller:
Ниже представлен эталонный контроллер на PHP 8.4+, наследующий WP_REST_Controller. Он реализует прием и выдачу инженерных лидов с полной декларативной схемой JSON Schema, защитой от перебора (Rate Limiting в permission_callback), поддержкой Batch API и фильтрацией полей _fields.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise REST API Controller
* Description: Типизированный REST-контроллер на базе WP_REST_Controller с JSON Schema и Rate Limiting.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\RestApi;
use WP_Error;
use WP_REST_Controller;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
if (!defined('ABSPATH')) {
exit;
}
final class ProjectInquiryRestController extends WP_REST_Controller
{
private const RATE_LIMIT_MAX_REQUESTS = 5;
private const RATE_LIMIT_WINDOW_SECONDS = 60;
public function __construct()
{
$this->namespace = 'agentic-lab/v1';
$this->rest_base = 'inquiries';
}
public static function register(): void
{
add_action('rest_api_init', static function (): void {
(new self())->register_routes();
});
}
public function register_routes(): void
{
register_rest_route($this->namespace, '/' . $this->rest_base, [
[
'methods' => WP_REST_Server::READABLE,
'callback' => $this->get_items(...),
'permission_callback' => $this->get_items_permissions_check(...),
'args' => $this->get_collection_params(),
],
[
'methods' => WP_REST_Server::CREATABLE,
'callback' => $this->create_item(...),
'permission_callback' => $this->create_item_permissions_check(...),
// Автоматическая генерация правил валидации из нашей JSON Schema!
'args' => $this->get_endpoint_args_for_item_schema(WP_REST_Server::CREATABLE),
'allow_batch' => ['v1' => true],
],
'schema' => $this->get_public_item_schema(...),
]);
}
/**
* Проверка прав на чтение списка заявок: строго для администраторов.
*/
public function get_items_permissions_check($request): bool|WP_Error
{
if (!current_user_can('manage_options')) {
return new WP_Error(
'rest_forbidden_context',
'Доступ к реестру инженерных заявок разрешен только администраторам.',
['status' => rest_authorization_required_code()]
);
}
return true;
}
/**
* Проверка прав на создание заявки: публичный эндпоинт с жестким Rate Limiting по IP.
*/
public function create_item_permissions_check($request): bool|WP_Error
{
$clientIp = sanitize_text_field((string) ($_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'));
$rateKey = 'agentic_rl_' . md5($clientIp);
$attempts = (int) get_transient($rateKey);
if ($attempts >= self::RATE_LIMIT_MAX_REQUESTS) {
return new WP_Error(
'rest_rate_limit_exceeded',
'Превышен лимит запросов. Пожалуйста, повторите попытку через 1 минуту.',
['status' => 429]
);
}
set_transient($rateKey, $attempts + 1, self::RATE_LIMIT_WINDOW_SECONDS);
return true;
}
/**
* Обработка POST-запроса создания новой проектной заявки.
* Все параметры к этому моменту УЖЕ прошли валидацию и очистку по JSON Schema!
*/
public function create_item($request): WP_REST_Response|WP_Error
{
$clientEmail = (string) $request->get_param('client_email');
$projectBudget = (int) $request->get_param('budget_usd');
$architectureTier = (string) $request->get_param('architecture_tier');
$postId = wp_insert_post([
'post_type' => 'case_study',
'post_status' => 'private',
'post_title' => sprintf('Inquiry: %s ($%d)', $clientEmail, $projectBudget),
'meta_input' => [
'_inquiry_email' => $clientEmail,
'_inquiry_budget' => $projectBudget,
'_inquiry_tier' => $architectureTier,
],
], true);
if (is_wp_error($postId)) {
return $postId;
}
$itemData = [
'id' => $postId,
'client_email' => $clientEmail,
'budget_usd' => $projectBudget,
'architecture_tier' => $architectureTier,
'created_at' => gmdate('c'),
];
$response = $this->prepare_item_for_response($itemData, $request);
$response->set_status(201);
$response->header('Location', rest_url(sprintf('%s/%s/%d', $this->namespace, $this->rest_base, $postId)));
return $response;
}
/**
* Подготовка элемента с поддержкой селективной фильтрации полей (?_fields=id,budget_usd).
*
* @param array<string, mixed> $item
*/
public function prepare_item_for_response($item, $request): WP_REST_Response
{
$fields = $this->get_fields_for_response($request);
$data = [];
foreach (['id', 'client_email', 'budget_usd', 'architecture_tier', 'created_at'] as $field) {
if (rest_is_field_included($field, $fields)) {
$data[$field] = $item[$field] ?? null;
}
}
return rest_ensure_response($data);
}
/**
* Декларативная JSON Schema (Draft-04) для автоматической валидации, санитарии и документации.
*
* @return array<string, mixed>
*/
public function get_item_schema(): array
{
if ($this->schema !== null) {
return $this->add_additional_fields_schema($this->schema);
}
$this->schema = [
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'project_inquiry',
'type' => 'object',
'properties' => [
'id' => [
'description' => 'Уникальный идентификатор заявки.',
'type' => 'integer',
'context' => ['view', 'edit'],
'readonly' => true,
],
'client_email' => [
'description' => 'Корпоративный Email заказчика.',
'type' => 'string',
'format' => 'email',
'required' => true,
'context' => ['view', 'edit'],
],
'budget_usd' => [
'description' => 'Оценочный бюджет проекта в USD.',
'type' => 'integer',
'minimum' => 1000,
'maximum' => 1000000,
'required' => true,
'context' => ['view', 'edit'],
],
'architecture_tier' => [
'description' => 'Выбранный стек архитектуры.',
'type' => 'string',
'enum' => ['headless-nextjs', 'hybrid-fse', 'enterprise-mcp'],
'default' => 'headless-nextjs',
'context' => ['view', 'edit'],
],
'created_at' => [
'description' => 'Время регистрации заявки в формате ISO-8601.',
'type' => 'string',
'format' => 'date-time',
'context' => ['view'],
'readonly' => true,
],
],
];
return $this->add_additional_fields_schema($this->schema);
}
}
ProjectInquiryRestController::register();Почему завершение работы REST-контроллера через echo json_encode(...); exit; или wp_send_json(...) является критическим дефектом архитектуры?
В чем суть ошибки: Разработчик ставит 'permission_callback' => '__return_true' на маршрут с методом POST, PUT или DELETE во время локальной отладки и деплоит код на продакшен.
Почему разработчики так делают: Желание быстро убрать ошибку 401 Unauthorized при тестировании из Postman или фронтенда.
Что происходит в продакшене: Автоматизированные ботнеты сканируют схему /wp-json/ (которая открыта публично и содержит список всех зарегистрированных пространств имен и методов), находят открытый POST/DELETE эндпоинт и за минуты заполняют базу данных мусором или удаляют контент.
Как правильно: Даже если эндпоинт предназначен для публичной отправки лидов без логина, permission_callback ОБЯЗАН выполнять проверку Rate Limiting по IP/токену или валидацию криптографической подписи (Turnstile / HMAC).
В чем суть ошибки: Внутри колбэка REST-маршрута разработчик обращается напрямую к $_POST['email'] или file_get_contents('php://input').
Почему разработчики так делают: Привычка к написанию старых обработчиков admin-ajax.php.
Что происходит в продакшене: Во-первых, при отправке запроса с заголовком Content-Type: application/json суперглобальный массив $_POST в PHP всегда пуст! Во-вторых, обращение в обход объекта WP_REST_Request полностью игнорирует результаты работы sanitize_callback и дефолтные значения из args.
Как правильно: Всегда извлекать параметры исключительно через методы объекта запроса: $request->get_param('key'), $request->get_json_params() или $request->get_query_params().
В чем суть ошибки: Завершение работы метода контроллера вызовом echo json_encode($data); exit; или wp_send_json_success($data).
Почему разработчики так делают: Копирование кода из устаревших AJAX-экшенов.
Что происходит в продакшене: Немедленный обрыв процесса через exit ломает весь жизненный цикл WP_REST_Server: не срабатывают фильтры rest_post_dispatch и rest_pre_serve_request, не отправляются CORS-заголовки, отключается проекция полей _fields, падают пакетные запросы Batch API (/wp-json/batch/v1), а в PHPUnit-тестах метод rest_do_request() убивает процесс тест-раннера.
Как правильно: Всегда возвращать объект WP_REST_Response (через return rest_ensure_response($data)) или WP_Error.
В чем суть ошибки: При ошибке бизнес-логики контроллер возвращает return rest_ensure_response(['success' => false, 'error' => 'Invalid ID']) со статус-кодом 200 OK.
Почему разработчики так делают: Попытка изобрести собственный формат ответов поверх стандарта WP REST API.
Что происходит в продакшене: Edge CDN и кэширующий слой Next.js fetch() видят статус 200 OK и кэшируют ответ с ошибкой как валидные данные на часы вперед! Кроме того, стандартные перехватчики ошибок на клиенте не срабатывают.
Как правильно: При любой ошибке возвращать new WP_Error('error_code', 'Сообщение', ['status' => 422]). Диспетчер автоматически выставит правильный HTTP-код и запретит кэширование сбоя.