У корпоративній розробці на 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/inquiries] (JSON Body: { email, budget, tier })
│
▼
[1. WP_REST_Server: Валідація за get_item_schema()]
├── email: format => 'email' (Автоматична перевірка ядром!)
├── budget: type => 'integer', minimum => 1000, maximum => 1000000
└── tier: type => 'string', enum => ['headless-nextjs', 'hybrid-fse', 'enterprise-mcp']
│ (При помилці ──► 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('client_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.
/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/inquiries та отримати повну специфікацію всіх полів, типів та обов'язкових параметрів.Зіставимо процедурну реєстрацію маршрутів з об'єктно-орієнтованою архітектурою 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();У чому суть помилки: Розробник вказує '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-код і заборонить кешування збою.
Ініціалізація властивостей $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.