Пользовательские типы записей (Custom Post Types, CPT) — это базовый механизм расширения реляционной схемы данных WordPress. Под капотом CPT не создают новые таблицы в базе данных, а проецируются на универсальную таблицу wp_posts, где тип сущности детерминируется строковым дискриминатором в столбце post_type.
Регистрация типа записей через функцию register_post_type() запускает комплексный конвейер инициализации в ядре:
$wp_post_types: ядро создает экземпляр класса WP_Post_Type и сохраняет его в глобальном массиве $wp_post_types[$post_type].WP_Rewrite): если параметр rewrite активен, ядро регистрирует структуру постоянных ссылок (Permalinks), маску конечных точек (ep_mask) и правила перенаправления для архивных (has_archive) и одиночных (single) страниц.show_in_rest => true ядро регистрирует REST-маршруты /wp/v2/<rest_base> на базе контроллера WP_REST_Posts_Controller, что делает тип записей доступным для блочного редактора Gutenberg, мобильных клиентов и Headless-фронтенда на Next.js.Capabilities Mapping): связывание примитивных прав доступа пользователя с контекстными мета-правами конкретной записи через метод map_meta_cap().Регистрация CPT должна производиться строго на системном хуке init. Вызов до этого момента (например, на plugins_loaded) ломает локализацию ярлыков и маппинг прав ролей, а вызов после init (на wp_loaded) приводит к невозможности распознавания маршрутов парсером WP::parse_request().
Архитектуру CPT необходимо воспринимать как декларативный контракт между схемой данных, подсистемой авторизации и интерфейсом рендеринга:
WP_Post_Type map_meta_cap ep_mask template_lock Hierarchical CPT Penalty[register_post_type('property', $args)] (на хуке 'init')
│
▼
[1. Инстанцирование WP_Post_Type] ──> Сохранение в $GLOBALS['wp_post_types']['property']
│
▼
[2. Регистрация в WP_Rewrite]
├── add_rewrite_tag('%property%', '([^/]+)', 'property=')
├── Генерация rewrite rules: 'property/([^/]+)/?$' => 'index.php?property=$matches[1]'
└── Архив: 'properties/?$' => 'index.php?post_type=property'
│
▼
[3. Регистрация REST API Эндпоинтов]
└── WP_REST_Posts_Controller ──> /wp/v2/properties (GET, POST, PUT, DELETE)
│
▼
[4. Подключение к Template Hierarchy]
├── Одиночная страница: single-property.php -> single.php -> singular.php -> index.php
└── Архив: archive-property.php -> archive.php -> index.php
│
▼
[5. Маппинг Прав Доступа (Capabilities)]
└── map_meta_cap('edit_post', $user_id, $post_id) ──> Проверка ролей пользователяОбъявление меток (labels), аргументов видимости, схемы URL (rewrite) и поддерживаемых компонентов (supports).
Создание инстанса WP_Post_Type, регистрация в $wp_post_types, привязка к системным таксономиям.
Генерация внутренних правил регулярных выражений WP_Rewrite и назначение битовой маски конечных точек.
Инициализация WP_REST_Posts_Controller, регистрация JSON-схемы сущности, активация редактора Gutenberg.
Построение динамической матрицы прав пользователя через map_meta_cap и проверку capabilities ролей.
Формирование массива ярлыков, флагов видимости, поддерживаемых фичей и структуры URL.
Используйте строгие проверки: hierarchical => false для больших каталогов.
Передача аргументов в register_post_type(). Создание инстанса WP_Post_Type в памяти.
Никогда не регистрируйте CPT на хуках plugins_loaded или after_setup_theme.
Генерация внутренних регулярных выражений для парсера URL и создание эндпоинтов.
Сброс правил (flush_rewrite_rules) выполняется строго при активации плагина.
Создание эндпоинтов /wp/v2/<rest_base> для интеграции с Gutenberg и Next.js.
Флаг show_in_rest активирует современный редактор блоков.
Связывание прав capability_type с ролями администратора, редактора и кастомными ролями.
Проверка current_user_can('edit_post', $id) безопасно валидирует контекст.
На каком системном этапе жизненного цикла WordPress допустимо вызывать register_post_type() и как данные сохраняются в СУБД?
capability_type с разделением прав (менеджер видит только сделки своего отдела, аудитор имеет права read_private_posts без возможности модификации, супервайзер подтверждает публикации).template и блокировкой template_lock => 'all', гарантирующая, что контент-менеджер заполняет строго определенные поля блоков Gutenberg, не нарушая дизайн-систему страницы.register_post_status) — например, under_review, legal_approved, expired — с автоматической валидацией перехода между состояниями.При проектировании архитектуры сущностей необходимо четко понимать, когда использовать CPT, а когда переходить на альтернативные структуры данных:
Стандартная ошибка разработчиков — указание 'capability_type' => 'book' без последующего назначения прав ролям. В WordPress права делятся на две категории:
wp_user_roles у каждой роли (например, edit_books, edit_others_books, publish_books, read_private_books).current_user_can('edit_post', $post_id).Если параметр 'map_meta_cap' => true, ядро автоматически транслирует вызов edit_post в проверку примитивных прав: принадлежит ли пост текущему пользователю (edit_posts) или это чужой пост (edit_others_posts).
Ниже представлен production-ready класс на PHP 8.4+, реализующий безопасную регистрацию CPT с блочным шаблоном Gutenberg, строгими типами, безопасной инициализацией прав ролей и предотвращением деградации ЧПУ.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise CPT Registrar
* Description: Строгий оркестратор регистрации CPT с безопасной матрицей прав и Gutenberg-шаблонами.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\PostTypes;
use WP_Error;
if (!defined('ABSPATH')) {
exit;
}
final readonly class CaseStudyPostType
{
public const POST_TYPE = 'case_study';
public const CAPABILITY_TYPE = 'case_study';
public static function init(): void
{
add_action('init', self::register(...), 10);
}
public static function register(): void
{
$labels = [
'name' => 'Кейсы',
'singular_name' => 'Кейс',
'menu_name' => 'Кейсы компании',
'name_admin_bar' => 'Кейс',
'add_new' => 'Добавить кейс',
'add_new_item' => 'Добавить новый кейс',
'new_item' => 'Новый кейс',
'edit_item' => 'Редактировать кейс',
'view_item' => 'Просмотреть кейс',
'all_items' => 'Все кейсы',
'search_items' => 'Искать кейсы',
'not_found' => 'Кейсов не найдено',
'not_found_in_trash' => 'В корзине кейсов не найдено',
];
// Фиксированный шаблон Gutenberg для соблюдения дизайн-системы
$blockTemplate = [
['core/heading', ['level' => 2, 'placeholder' => 'Цели и задачи проекта...']],
['core/paragraph', ['placeholder' => 'Опишите исходную проблему заказчика...']],
['core/columns', [], [
['core/column', [], [
['core/heading', ['level' => 3, 'content' => 'Стек технологий']],
['core/list', []]
]],
['core/column', [], [
['core/heading', ['level' => 3, 'content' => 'Результаты (KPI)']],
['core/paragraph', ['placeholder' => '+140% конверсия...']]
]]
]],
['core/quote', ['placeholder' => 'Отзыв технического директора заказчика...']]
];
$args = [
'labels' => $labels,
'description' => 'Портфолио инженерных кейсов и выполненных проектов компании',
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => [
'slug' => 'cases',
'with_front' => false,
'feeds' => true,
'pages' => true,
],
'has_archive' => 'cases',
'hierarchical' => false, // Защита от деградации производительности
'menu_position' => 20,
'menu_icon' => 'dashicons-portfolio',
'show_in_rest' => true, // Активирует Gutenberg и REST API
'rest_base' => 'cases',
'supports' => ['title', 'editor', 'thumbnail', 'excerpt', 'revisions', 'custom-fields'],
// Строгая авторизационная модель
'capability_type' => [self::CAPABILITY_TYPE, self::CAPABILITY_TYPE . 's'],
'map_meta_cap' => true,
// Блочный шаблон и политика фиксации
'template' => $blockTemplate,
'template_lock' => false, // 'all' для полной блокировки структуры
];
register_post_type(self::POST_TYPE, $args);
}
/**
* Программная инициализация прав для ролей при активации плагина.
* Не выполняется на каждом хите!
*/
public static function grantCapabilities(): void
{
$adminRole = get_role('administrator');
$editorRole = get_role('editor');
$caps = [
"edit_" . self::CAPABILITY_TYPE,
"read_" . self::CAPABILITY_TYPE,
"delete_" . self::CAPABILITY_TYPE,
"edit_" . self::CAPABILITY_TYPE . "s",
"edit_others_" . self::CAPABILITY_TYPE . "s",
"publish_" . self::CAPABILITY_TYPE . "s",
"read_private_" . self::CAPABILITY_TYPE . "s",
"delete_" . self::CAPABILITY_TYPE . "s",
"delete_private_" . self::CAPABILITY_TYPE . "s",
"delete_published_" . self::CAPABILITY_TYPE . "s",
"delete_others_" . self::CAPABILITY_TYPE . "s",
"edit_private_" . self::CAPABILITY_TYPE . "s",
"edit_published_" . self::CAPABILITY_TYPE . "s",
];
foreach ($caps as $cap) {
$adminRole?->add_cap($cap);
$editorRole?->add_cap($cap);
}
}
}
// Запуск инициализации
CaseStudyPostType::init();
// Безопасный сброс rewrite-правил СТРОГО в момент активации плагина
register_activation_hook(__FILE__, function(): void {
CaseStudyPostType::register();
CaseStudyPostType::grantCapabilities();
flush_rewrite_rules(false); // false = мягкий сброс без модификации .htaccess
});Команда зарегистрировала CPT каталога товаров на 50 000 элементов с параметрами 'hierarchical' => true и добавила flush_rewrite_rules() в хук init. К каким последствиям в продакшене это приведет?
Почему разработчики так делают: После регистрации CPT одиночные страницы отдают 404. Не разобравшись в архитектуре WP_Rewrite, разработчик добавляет flush_rewrite_rules() в конец функции регистрации CPT.
Что происходит в продакшене: Функция flush_rewrite_rules() компилирует тысячи регулярных выражений маршрутов и перезаписывает тяжелую опцию rewrite_rules в таблице wp_options. Это вызывает постоянную инвалидацию кэша опций, тяжелые дисковые операции записи и деградацию времени ответа сервера (TTFB) на 200–600 мс для каждого посетителя сайта, а при параллельной нагрузке приводит к дедлокам в MySQL.
Как правильно: Вызывать сброс правил исключительно один раз — в хуке активации плагина (register_activation_hook) либо при ручном сохранении настроек в админке:
register_activation_hook(__FILE__, function(): void {
my_register_cpt();
flush_rewrite_rules(false); // Мягкий сброс без записи в .htaccess
});Почему разработчики так делают: Желание получить древовидную структуру URL вида catalog.com/catalog/parent-category/child-item/ по аналогии со стандартными страницами (page).
Что происходит в продакшене: При включении hierarchical => true ядро принудительно строит дерево вложенности для административного интерфейса. На странице редактирования в метабоксе атрибутов вызывается функция wp_dropdown_pages(), которая загружает все существующие записи CPT в оперативную память PHP. Если в каталоге 20 000+ элементов, сервер падает по Fatal Error (Memory Exhausted) при открытии админки. Кроме того, поиск по URL требует сканирования цепочки родителей, что замедляет генерацию SQL.
Как правильно: Оставлять CPT плоским ('hierarchical' => false), а иерархическую структуру категорий и фильтров выстраивать через кастомные таксономии (register_taxonomy), которые оптимизированы для деревьев отношений.
Почему разработчики так делают: Разработчик указывает уникальный тип прав 'capability_type' => 'document' для безопасности, ожидая, что администратор сайта автоматически сможет управлять записями.
Что происходит в продакшене: Ни одна роль (включая administrator) не имеет прав на новый CPT. Пункт меню CPT полностью исчезает из панели управления, а прямой переход по /wp-admin/edit.php?post_type=document выдает ошибку «Вам не разрешено просматривать эту страницу» (HTTP 403).
Как правильно: Программно выдавать сгенерированные права ролям через объект WP_Roles при активации плагина, либо использовать 'map_meta_cap' => true в паре с базовыми типами ('capability_type' => 'post'), если глубокая изоляция ролей не требуется.
Почему разработчики так делают: Копирование старых сниппетов кода эпохи WordPress 4.x, где этот параметр отсутствовал.
Что происходит в продакшене: Записи нового CPT открываются в устаревшем Classic Editor (TinyMCE). Блочный редактор Gutenberg, блочные шаблоны, библиотека компонентов и кастомные метаполя через Block Bindings API блокируются. Кроме того, запись невозможно прочитать через REST API и Headless-интерфейсы.
Как правильно: Всегда явно указывать 'show_in_rest' => true и задавать удобный REST slug через 'rest_base' => 'my_items'.