Користувацькі таксономії (Custom Taxonomies) у WordPress — це нормалізована реляційна підсистема класифікації та фасетного групування сутностей (WP_Post, WP_User, кастомних об'єктів). На відміну від таблиці метаданих wp_postmeta (побудованої за неіндексованим текстовим патерном Entity-Attribute-Value), архітектура таксономій спроєктована навколо трьох спеціалізованих таблиць із жорсткими цілочисельними B-Tree індексами:
wp_terms (Словник унікальних лексем): Зберігає базову ідентифікацію терма (term_id BIGINT UNSIGNED, name VARCHAR(200), slug VARCHAR(200)). Індексована за slug та name.wp_term_taxonomy (Контекст класифікації): Пов'язує лексему term_id з конкретною таксономією (taxonomy VARCHAR(32)), зберігає опис, батьківський елемент ієрархії (parent BIGINT UNSIGNED) та денормалізований лічильник прив'язаних об'єктів (count BIGINT).wp_term_relationships (Матриця зв'язків багато-до-багатьох): Ультракомпактна сполучна таблиця з трьох цілочисельних стовпців (object_id, term_taxonomy_id, term_order). Її первинний ключ PRIMARY KEY (object_id, term_taxonomy_id) та зворотний індекс KEY term_taxonomy_id (term_taxonomy_id) дозволяють рушію InnoDB виконувати перетини мільйонів зв'язків у пам'яті (Index-Only Scan) за 1–3 мілісекунди.wp_termmeta (Атрибути терма): Розширює терми додатковими полями (іконки брендів, SEO-метадані, колірні токени) з автоматичним пакетним передзавантаженням через update_term_meta_cache().Реєстрація таксономії через register_taxonomy() на хуку init створює екземпляр класу WP_Taxonomy у глобальному реєстрі $wp_taxonomies, компілює правила ЧПУ у WP_Rewrite та ініціалізує контролер WP_REST_Terms_Controller для роботи в блоковому редакторі Gutenberg та Headless-додатках.
Архітектуру таксономій слід сприймати як індексований граф зв'язків між документами (object_id) та вузлами класифікатора (term_taxonomy_id), де SQL-компілятор WP_Tax_Query будує перетини множин без читання важких таблиць контенту.
[wp_posts] (ID: 1050)
│
│ 1. JOIN за цілочисельним індексом (object_id)
▼
[wp_term_relationships]
├── object_id: 1050 ◄──► term_taxonomy_id: 42 (PRIMARY KEY)
│
│ 2. JOIN за первинним ключем (term_taxonomy_id)
▼
[wp_term_taxonomy]
├── term_taxonomy_id: 42
├── term_id: 18
├── taxonomy: 'tech_stack'
├── parent: 0
└── count: 340 (денормалізований кеш кількості записів)
│
├───────────────────────────────┐
▼ ▼
[wp_terms] [wp_termmeta]
├── term_id: 18 ├── term_id: 18
├── name: 'TypeScript 5.8' ├── meta_key: '_badge_color'
└── slug: 'typescript' └── meta_value: '#3178C6'Виклик register_taxonomy() на хуку init (пріоритет 9, до CPT). Інстанціювання WP_Taxonomy у $wp_taxonomies.
Зв'язування з типами записів та активація WP_REST_Terms_Controller (show_in_rest => true) для панелей Gutenberg.
Трансляція дерева умов tax_query (AND/OR) в SQL-клаузи JOIN та WHERE за таблицею wp_term_relationships.
Виконання перетину множин ID записів в InnoDB суворо за покриваючим індексом (object_id, term_taxonomy_id).
Пакетний праймінг об'єктів WP_Term та їхніх метаданих (wp_termmeta) у Persistent Object Cache за один мережевий раунд.
meta_query на таксономії знижує час SQL-вибірки з 4.2 секунд до 8 мілісекунд.public => false) для внутрішньої маршрутизації: Використання невидимих у публічному UI таксономій (як product_visibility у WooCommerce для маркерів featured, outofstock, exclude-from-catalog) замість повільних булевих прапорців у wp_postmeta.industry, region, compliance_standard), які одночасно об'єднують різні типи записів (case_study, whitepaper, news, video) у єдині тематичні хаби.register_term_meta: Прив'язка до термів таксономії SVG-іконок, колірних палітр та JSON-конфігурацій із валідацією схеми та віддачею напряму в Gutenberg Block Bindings та WPGraphQL.Головне архітектурне рішення під час проєктування бази даних у WordPress — проведення чіткої межі між таксономіями та довільними метаполями:
WP_Tax_Query: параметр include_children#За замовчуванням для будь-якої ієрархічної таксономії (hierarchical => true) клас WP_Tax_Query встановлює параметр 'include_children' => true. Це означає, що при запиті батьківської рубрики ядро попередньо витягує все дерево дочірніх підкатегорій та підставляє їх у величезний оператор WHERE term_taxonomy_id IN (12, 15, 19, ... 450). На глибоких деревах каталогів це роздуває SQL-запит. Встановлення 'include_children' => false при точній фільтрації економить до 40% часу компіляції запиту.
Нижче наведено production-ready модуль на PHP 8.4+, що реєструє наскрізну таксономію технологічного стека з типізованими метаполями терма (register_term_meta), експозицією в REST API / Gutenberg та високошвидкісним методом фасетної фільтрації.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Taxonomy & Facet Engine
* Description: Реєстрація наскрізних таксономій, типізованих метаполів термів та високошвидкісних фасетних вибірок.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\Taxonomies;
use WP_Query;
if (!defined('ABSPATH')) {
exit;
}
final readonly class TechStackTaxonomy
{
public const TAXONOMY = 'tech_stack';
public const OBJECT_TYPES = ['case_study', 'post'];
public static function init(): void
{
// Пріоритет 9: реєструємо таксономію гарантовано ДО реєстрації залежних CPT (пріоритет 10)
add_action('init', self::registerTaxonomy(...), 9);
add_action('init', self::registerTermMetadata(...), 10);
}
public static function registerTaxonomy(): void
{
$labels = [
'name' => 'Технологічний стек',
'singular_name' => 'Технологія',
'search_items' => 'Знайти технологію',
'all_items' => 'Весь стек технологій',
'parent_item' => 'Категорія стека',
'parent_item_colon' => 'Категорія стека:',
'edit_item' => 'Редагувати технологію',
'update_item' => 'Оновити технологію',
'add_new_item' => 'Додати технологію',
'new_item_name' => 'Назва нової технології',
'menu_name' => 'Стек технологій',
];
$args = [
'labels' => $labels,
'hierarchical' => true,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_admin_column' => true,
'show_in_nav_menus' => false,
'show_tagcloud' => false,
'show_in_rest' => true, // Обов'язково для Gutenberg та Headless REST API
'rest_base' => 'tech-stack',
'query_var' => true,
'rewrite' => [
'slug' => 'stack',
'with_front' => false,
'hierarchical' => false, // Плаский URL /stack/typescript/ замість довгих ланцюжків
],
];
register_taxonomy(self::TAXONOMY, self::OBJECT_TYPES, $args);
}
/**
* Сувора реєстрація метаполів терма з валідацією JSON Schema для REST API та Block Bindings.
*/
public static function registerTermMetadata(): void
{
register_term_meta(self::TAXONOMY, '_stack_badge_hex', [
'type' => 'string',
'description' => 'HEX-код акцентного кольору бейджа технології',
'single' => true,
'default' => '#06B6D4',
'sanitize_callback' => static fn(mixed $val): string => sanitize_hex_color((string) $val) ?: '#06B6D4',
'show_in_rest' => [
'schema' => [
'type' => 'string',
'pattern' => '^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$',
],
],
]);
}
/**
* Виконання високопродуктивної фасетної вибірки за перетином кількох термів
* з вимкненням надлишкового обходу дочірніх дерев (include_children => false).
*
* @param list<string> $requiredSlugs
* @return list<int>
*/
public static function queryPostIdsByFacetSlugs(array $requiredSlugs, int $limit = 24): array
{
if ($requiredSlugs === []) {
return [];
}
$query = new WP_Query([
'post_type' => 'case_study',
'post_status' => 'publish',
'posts_per_page' => min(max($limit, 1), 100),
'fields' => 'ids',
'no_found_rows' => true,
'update_post_meta_cache' => false,
'update_post_term_cache' => true,
'tax_query' => [
'relation' => 'AND',
[
'taxonomy' => self::TAXONOMY,
'field' => 'slug',
'terms' => array_map('sanitize_title', $requiredSlugs),
'operator' => 'AND',
'include_children' => false, // Пригнічуємо зайвий SQL-запит дерева нащадків
],
],
]);
/** @var list<int> */
return array_map('intval', $query->posts);
}
}
TechStackTaxonomy::init();У чому суть помилки: Розробник створює текстове або серіалізоване поле у wp_postmeta (наприклад, brand = 'Apple' або stack = '["React", "PHP"]') і будує каталог із фільтрацією через meta_query з оператором LIKE.
Чому розробники так роблять: Здається простішим додати ще одне поле до групи ACF, ніж реєструвати окрему таксономію через register_taxonomy().
Що відбувається в продакшені: Таблиця wp_postmeta не має індексу за значеннями LONGTEXT. При фільтрації за 3–4 параметрами MySQL генерує серію важких INNER JOIN wp_postmeta, створюючи тимчасові таблиці на диску (Using temporary; Using filesort). На базі з 50 000 записів запит починає виконуватися 3–7 секунд, блокуючи пул воркерів PHP-FPM.
Як правильно: Будь-який атрибут, за яким виконується фільтрація, групування або побудова списків, ЗОБОВ'ЯЗАНИЙ проєктуватися як таксономія (включаючи приховані системні таксономії з 'public' => false).
У чому суть помилки: Виконання десятків tax_query на сторінці каталогу без явного зазначення 'include_children' => false.
Чому розробники так роблять: Незнання того, що WP_Tax_Query за замовчуванням встановлює include_children в true для всіх ієрархічних таксономій.
Що відбувається в продакшені: Перед компіляцією основного запиту ядро викликає get_term_children() для кожного переданого терма, розгортаючи гігантські масиви дочірніх ID та роздуваючи умову IN (...) до сотень елементів.
Як правильно: Завжди явно передавати 'include_children' => false, якщо бізнес-логіка вимагає точного збігу з обраними вузлами фільтра.
У чому суть помилки: Виклик register_taxonomy() на хуку init із пріоритетом 20, тоді як пов'язаний із нею CPT реєструється з дефолтним пріоритетом 10 та використовує плейсхолдер таксономії у своєму rewrite['slug'].
Чому розробники так роблять: Довільне розставлення пріоритетів хуків у різних класах плагіна.
Що відбувається в продакшені: У момент компіляції правил ЧПУ для CPT таксономія ще не існує в глобальному реєстрі $wp_taxonomies. Правила маршрутизації генеруються некоректно, а зв'язок в об'єктній моделі вимагає додаткового виклику register_taxonomy_for_object_type().
Як правильно: Реєструвати таксономії з більш раннім пріоритетом (add_action('init', ..., 9)), ніж залежні від них типи записів (add_action('init', ..., 10)).
У чому суть помилки: Отримання масиву термів через get_terms(['update_term_meta_cache' => false]) або кастомний SQL із подальшим викликом get_term_meta($term->term_id, '_icon', true) всередині циклу foreach.
Чому розробники так роблять: Вимкнення кешу заради уявної економії пам'яті під час вибірки термів.
Що відбувається в продакшені: На меню з 80 категорій з іконками виконується 80 поодиноких SQL-запитів SELECT meta_key, meta_value FROM wp_termmeta WHERE term_id = X.
Як правильно: Залишати параметр 'update_term_meta_cache' => true у get_terms() (активний за замовчуванням) або викликати update_termmeta_cache($termIds) перед циклом рендерингу.
Оголошення конфігурації таксономії, ярликів та REST-схеми до ініціалізації залежних типів записів.
Прапорець show_in_rest => true обов'язковий для відображення панелі вибору термів у Gutenberg.
При створенні терма ядро перевіряє унікальність slug у wp_terms та створює запис контексту у wp_term_taxonomy.
Механізм Term Splitting гарантує ізоляцію термів різних таксономій.
Зв'язок поста з термом записується у wp_term_relationships як пара чисел (object_id, term_taxonomy_id).
Одночасно перераховується поле count у таблиці wp_term_taxonomy та скидається об'єктний кеш.
Формування оптимізованих INNER JOIN за складеним первинним ключем таблиці зв'язків.
Параметр include_children => false виключає рекурсивне читання дочірніх гілок.
Усі пов'язані поля з wp_termmeta завантажуються в пам'ять за один запит WHERE term_id IN (...).
Забезпечує миттєвий доступ до іконок та кольорів термів без N+1 запитів.