Контент: типы записей, таксономии, поля, выборки, формы

Свой тип записей

Отзывы, услуги, вакансии, товары — всё это типы записей, как «Записи» и «Страницы». Регистрируют их в событии init:

add_action('init', function () {
    register_post_type('service', [
        'labels' => ['name' => 'Услуги', 'singular_name' => 'Услуга', 'add_new' => 'Добавить услугу', 'count' => 'услуга|услуги|услуг'],
        'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'revisions'],
        'has_archive' => true,           // список всех услуг по адресу /uslugi
        'rewrite_slug' => 'uslugi',      // адреса: /uslugi и /uslugi/{ярлык}
        'menu_icon' => 'grid',
        'menu_position' => 26,
        'admin_slug' => 'services',      // раздел админки: /admin/services
    ]);
});

Сразу появляются раздел в меню админки, список, редактор и адреса на сайте.

Параметр По умолчанию Что значит
labels из label / имени подписи: name, singular_name, menu_name, all_items, add_new, add_new_item, edit_item, not_found, count (`одна
public true есть ли у записей страницы на сайте (false — только в админке, например отзывы для блока)
hierarchical false могут ли записи быть вложенными, как страницы
supports title, editor, author, revisions что есть в редакторе: title, editor, author, thumbnail, excerpt, revisions, page-attributes (родитель и порядок), custom-fields
capability_type post права: post → edit_posts, publish_posts…; своё значение service → edit_services, publish_services… (их нужно выдать ролям)
show_in_menu, menu_position, menu_icon true, 25, pin пункт меню админки; значки — pin, page, image, star, grid, list, users, mail, home, plug…
admin_slug имя типа адрес раздела в админке
rewrite_slug имя типа начало адреса на сайте (null — без префикса)
has_archive false страница-список всех записей типа

Шаблоны на сайте — single-service.blade.php, archive-service.blade.php (см. Темы).

Таксономии

register_taxonomy('service_category', 'service', [
    'labels' => ['name' => 'Виды услуг', 'singular_name' => 'Вид услуги'],
    'hierarchical' => true,               // как рубрики (с вложенностью); false — как метки
    'rewrite_slug' => 'vid-uslugi',
]);

Параметры: labels, hierarchical, public, show_in_menu, rewrite_slug, capabilities (manage_terms — кто управляет терминами, по умолчанию manage_categories; assign_terms — кто ставит их записям, по умолчанию edit_posts). Встроенные таксономии: category (рубрики) и post_tag (метки).

Записи в коде

Все записи всех типов — модель Cms\Models\Post (таблица posts), это обычная модель Eloquent:

use Cms\Models\Post;

$services = Post::ofType('service')->published()->orderBy('menu_order')->get();
$post = get_post(15);

$post->displayTitle();            // заголовок (или «(без названия)»)
$post->permalink();               // адрес на сайте
$post->contentHtml();             // текст для вывода (блоки → HTML, фильтр the_content)
$post->excerptText(30);           // отрывок
$post->termsOf('category');       // рубрики записи
$post->thumbnailUrl('medium');    // изображение записи

Статусы: publish, future (запланирована), draft, pending, private. Удаление в корзину — «мягкое» (delete()), насовсем — forceDelete().

Права на конкретную запись: current_user_can('edit_post', $post), 'delete_post', 'read_post' — с учётом автора, статуса и роли.

Выборки для шаблонов

get_posts(['type' => 'post', 'number' => 3, 'category' => 'novosti']);       // свежие новости
get_posts(['type' => 'service', 'number' => 20, 'orderby' => 'menu_order', 'order' => 'asc']);
get_terms('category', ['hide_empty' => true, 'orderby' => 'count']);

get_posts() — только опубликованные: type (post), number (5, до 100), category, tag (ярлык или id, с вложенными рубриками), exclude (id), orderby (date, title, menu_order, random), order (desc/asc). get_terms() — hide_empty (true), number (0 — все), orderby (name/count), parent; у каждого термина есть posts_count.

Метаполя

Любые дополнительные данные записи (цена, оценка, адрес) — метаполя. Значение может быть строкой, числом или массивом.

update_post_meta($post, '_price', 1500);
$price = get_post_meta($post, '_price', 0);
delete_post_meta($post, '_price');

Ключ с подчёркиванием в начале (_price) — служебный: его не видно в блоке «Произвольные поля», его меняет только ваш код.

Поля в редакторе (метабоксы)

Свой блок полей в редакторе записи добавляют в событии add_meta_boxes — оно срабатывает для каждой открытой записи:

add_action('add_meta_boxes', function ($postType, $post) {
    if ($postType->name === 'service') {
        add_meta_box('service-price', 'Цена', 'myplugin::price-box', 'service', 'side', 10);
    }
}, 10, 2);

add_meta_box($id, $title, $callback, $screen, $context, $priority):

  • $callback — имя шаблона (получит $post и $postType) или функция fn ($post, $postType) => …, возвращающая шаблон или HTML;
  • $screen — тип записей или список типов ('*' — все);
  • $context — normal (под текстом) или side (правая колонка); $priority — порядок (меньше — выше).

Сохранение — в событии save_post_{тип} (или save_post для всех типов). Права на запись CMS уже проверила:

add_action('save_post_service', function ($post, $request) {
    if ($request->has('price')) {
        $data = $request->validate(['price' => ['nullable', 'integer', 'min:0']]);
        update_post_meta($post, '_price', (int) ($data['price'] ?? 0));
    }
}, 10, 2);

Редактор сохраняет запись без перезагрузки страницы: после сохранения содержимое каждого блока полей заменяется свежим HTML с сайта (блок, в котором есть <script>, остаётся как был). Поэтому обработчики событий вешайте на document (делегирование) или заново после события cms:boxes-updated на форме #post-form. Автосохранение (раз в минуту) сохраняет только заголовок, текст и отрывок и save_post не вызывает; запись может впервые появиться как автосохранённый черновик, и тогда её первое обычное сохранение придёт с $update = true.

Проверяйте, что поле пришло ($request->has): если блок полей убрали, событие всё равно срабатывает. Стандартный блок можно убрать — в том же событии, но позже ядра: add_action('add_meta_boxes', fn () => remove_meta_box('postexcerpt'), 20); (блоки ядра: postexcerpt, postimagediv, pageparentdiv, seodiv, postcustom, authordiv, revisionsdiv). Полный пример — плагин «Отзывы».

Изображение записи и медиатека

has_post_thumbnail($post);
get_post_thumbnail_url($post, 'medium');
set_post_thumbnail($post, $mediaId);
media_url($mediaId, 'large');                  // адрес файла медиатеки (размеры: thumbnail, medium, large, full)
add_image_size('card', 600, 400, true, 'Карточка');   // свой размер (для файлов, загруженных после этого)

Какие файлы можно загружать — фильтр upload_types, размеры уменьшенных копий — фильтр image_sizes. Файлы — модель Cms\Models\Media, события add_attachment, edit_attachment, delete_attachment.

Формы

Формы собирают в разделе «Формы», на страницы ставят блоком «Форма», в шаблон темы — {{ cms_form(1) }}. Для плагинов:

// Своя обработка заявки: в CRM, в Telegram…
add_action('form_submitted', function ($form, $entry) {
    $phone = $entry->value('phone');           // значение поля по имени (или подписи)
}, 10, 2);

// Поменять поля при выводе формы: например, подсказку в поле телефона
add_filter('form_fields', fn (array $fields, $form) => array_map(
    fn (array $field) => $field['type'] === 'tel' ? [...$field, 'placeholder' => '+7 (___) ___-__-__'] : $field,
    $fields,
), 10, 2);

// Кому отправлять письма о заявках
add_filter('form_recipients', fn (array $emails, $form) => [...$emails, 'sales@example.ru'], 10, 2);

Фильтр form_fields меняет только вывод формы (подписи, подсказки, порядок); какие поля проверяются и сохраняются, задаётся в настройках формы. Типы полей: text, email, tel, textarea, select, radio, checkbox, number, date. Модели — Cms\Models\Form и Cms\Models\FormEntry (поля заявки — в $entry->data вместе с подписями). Письма уходят так, как настроено в «Настройки → Почта».