Користувацькі типи записів (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 ролей.
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 одиночні сторінки віддають 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'.
Формування масиву ярликів, прапорців видимості, підтримуваних фічей та структури 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) безпечно валідує контекст.