К нам пришла задача: есть сайт на WordPress с каталогом курсов, и нужно отдавать данные в мобильное приложение. Стандартный REST API WordPress отдаёт посты, но у нас были ACF-поля, кастомная таксономия, связанная таблица с расписанием и куча условной логики. Плагин ACF to REST API тупо отдавал все поля, включая служебные, а фильтровать на клиенте 500 записей — плохая идея.
Решили написать свои эндпоинты в functions.php дочерней темы. Без плагинов, без лишних зависимостей.
Базовый кастомный эндпоинт
Регистрация эндпоинта через register_rest_route — это стандартный механизм WordPress, не требующий плагинов:
add_action('rest_api_init', function () {
register_rest_route('myapp/v1', '/courses', [
'methods' => 'GET',
'callback' => 'myapp_get_courses',
'permission_callback' => '__return_true',
'args' => [
'category' => [
'default' => '',
'sanitize_callback' => 'sanitize_text_field',
],
'page' => [
'default' => 1,
'sanitize_callback' => 'absint',
],
'per_page' => [
'default' => 20,
'sanitize_callback' => 'absint',
],
],
]);
});Обратите внимание на sanitize_callback — WordPress автоматически очистит входящие параметры. Не доверяйте данным от клиента, даже если это ваш мобильный приложени.
Callback с фильтрацией и пагинацией
Сам коллбэк — это где живёт вся логика. Мы фильтруем по ACF-полю «сложность», по кастомной таксономии и по дате начала курса:
function myapp_get_courses(WP_REST_Request $request) {
$category = $request->get_param('category');
$page = $request->get_param('page');
$per_page = min($request->get_param('per_page'), 50);
$cache_key = 'myapp_courses_' . md5(serialize([
'category' => $category,
'page' => $page,
'per_page' => $per_page,
]));
$cached = get_transient($cache_key);
if ($cached !== false) {
return new WP_REST_Response($cached, 200);
}
$args = [
'post_type' => 'course',
'posts_per_page' => $per_page,
'paged' => $page,
'post_status' => 'publish',
'orderby' => 'meta_value',
'meta_key' => 'start_date',
'order' => 'ASC',
];
if (!empty($category)) {
$args['tax_query'] = [
[
'taxonomy' => 'course_category',
'field' => 'slug',
'terms' => $category,
],
];
}
$query = new WP_Query($args);
$courses = [];
foreach ($query->posts as $post) {
$courses[] = [
'id' => $post->ID,
'title' => $post->post_title,
'slug' => $post->post_name,
'excerpt' => wp_strip_all_tags($post->post_excerpt),
'difficulty' => get_field('difficulty', $post->ID),
'duration' => get_field('duration', $post->ID),
'start_date' => get_field('start_date', $post->ID),
'price' => (float) get_field('price', $post->ID),
'image' => get_the_post_thumbnail_url($post->ID, 'medium'),
'instructor' => [
'name' => get_field('instructor_name', $post->ID),
'bio' => get_field('instructor_bio', $post->ID),
],
];
}
$response = [
'data' => $courses,
'total' => (int) $query->found_posts,
'pages' => (int) $query->max_num_pages,
];
set_transient($cache_key, $response, 5 * MINUTE_IN_SECONDS);
return new WP_REST_Response($response, 200);
}Кэширование через Transients API
Каждый запрос к БД с WP_Query — это несколько SQL-запросов. Для каталога из 200 курсов, запрашиваемого каждые 5 секунд мобильными пользователями, это стало проблемой. Transients API WordPress — простой способ кэшировать результаты в таблице wp_options.
Ключ кэша генерируется из хэша параметров, поэтому разные фильтры получают разные кэши. При обновлении курса инвалидируем кэш:
add_action('save_post_course', function ($post_id) {
global $wpdb;
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_myapp_courses_%'"
);
$wpdb->query(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_myapp_courses_%'"
);
});Простой, но эффективный подход. Для высоконагруженных проектов лучше использовать Redis через объектный кэш WordPress, но transients справляются для moderate-нагрузки.
Безопасность и пермишены
Все публичные эндпоинты используют permission_callback равный __return_true. Но если нужны защищённые данные, делаем проверку nonce или JWT-токен:
'permission_callback' => function () {
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if (empty($header) || !str_starts_with($header, 'Bearer ')) {
return new WP_Error(
'unauthorized',
'Отсутствует токен авторизации',
['status' => 401]
);
}
$token = substr($header, 7);
return myapp_validate_token($token);
},Итог: кастомные REST-эндпоинты в WordPress без плагинов — это реальность, и это работает отлично. Главное — не прятать бизнес-логику в шаблоны, правильно кэшировать и не забывать про санитизацию входных данных.
Пока нет комментариев
Будьте первым, кто оставит комментарий к этой статье