Підсистема метаданих WordPress (wp_postmeta, wp_usermeta, wp_termmeta, wp_commentmeta) реалізує гнучку схему зберігання «Сутність — Атрибут — Значення» (EAV, Entity-Attribute-Value), що дозволяє розширювати структуру будь-яких об'єктів без виконання дорогих операцій ALTER TABLE у базі даних.
У сучасному стеку WordPress 6.7–7.0+ («Armstrong») архітектура роботи з довільними полями розділилася на три взаємодоповнюючі шари:
register_post_meta()): Сувора декларація типу поля (string, integer, number, boolean, array, object), колбека санітизації (sanitize_callback), політики доступу (auth_callback) та JSON-схеми (show_in_rest['schema']).wp_register_block_bindings_source): Революційний механізм ядра (введений у WP 6.5 та розширений візуальним UI редагування у WP 6.7–7.0), що дозволяє безпосередньо зв'язувати атрибути стандартних блоків (core/paragraph, core/heading, core/image, core/button) з метаполями запису (core/post-meta) або зовнішніми API без написання кастомних React-блоків.acf-json/*.json) для контролю версій у Git.Потік метаданих у сучасній архітектурі пов'язує сховище wp_postmeta одночасно із серверним рендерингом блоків Gutenberg, REST API / WPGraphQL та захищеними формами адмін-панелі.
[Декларація контракту: register_post_meta('_case_roi', $schema)]
│
┌───────────────────────────────┼───────────────────────────────┐
▼ ▼ ▼
[1. Класичний Metabox / ACF] [2. WP REST API (/wp/v2/cases)] [3. Block Bindings API]
├── Nonce (CSRF-захист) ├── Валідація за JSON Schema ├── Блок core/paragraph
├── current_user_can() ├── Перевірка auth_callback ├── metadata.bindings.content
└── sanitize_callback └── Читання/запис поля meta └── Джерело: core/post-meta
│ │ │
└───────────────────────────────┼───────────────────────────────┘
▼
[Ядро: update_metadata() / get_metadata()]
│
▼
[Persistent Object Cache (Redis) ◄──► Таблиця wp_postmeta]Реєстрація ключа через register_post_meta() на хуку init із зазначенням типу, sanitize_callback, auth_callback та JSON Schema.
Редагування поля напряму всередині прив'язаного блоку Gutenberg (Block Bindings UI), у сайдбарі або через панель ACF Pro.
Перевірка CSRF-токена (wp_verify_nonce), авторизації (auth_callback / edit_post) та суворе очищення даних.
Запис значення у wp_postmeta та миттєве скидання ключа групи post_meta в Redis/Memcached.
Під час рендерингу сторінки Block Bindings API автоматично інжектує значення метаполя в HTML-розмітку блоку.
core/heading та core/button до метаполів без створення десятків кастомних React-блоків.wp-content/themes/enterprise/acf-json/, де деплой нових полів на staging та production відбувається автоматично через CI/CD без ручного перенесення дампів бази даних.register_post_meta: Зберігання складних вкладених об'єктів (наприклад, координат полігонів карти або матриць тарифів) з валідацією кожної властивості за стандартом JSON Schema Draft-04 на рівні ядра REST API.wp_postmeta тільки для відображуваних карткових даних (при увімкненому update_post_meta_cache => true), тоді як поля сортування та фільтрації виносяться в індексовані таксономії або кастомні таблиці.Вибір між нативними метаполями з Block Bindings, ACF Pro та класичними метабоксами визначається архітектурними вимогами проєкту:
Починаючи з WordPress 6.5–7.0, будь-який стандартний блок може зчитувати метадані напряму через атрибут metadata.bindings:
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"_case_client_industry"}}}}} -->
<p>Галузь клієнта завантажується автоматично з метаполя</p>
<!-- /wp:paragraph -->Під час рендерингу на сервері рушій Gutenberg перехоплює цей блок, перевіряє, що мета-ключ _case_client_industry зареєстрований із прапорцем 'show_in_rest' => true, витягує його з об'єктного кешу та безпечно підставляє всередину тегу <p>.
Нижче наведено production-ready модуль на PHP 8.4+, що об'єднує сувору реєстрацію захищених метаполів (register_post_meta з auth_callback), кастомне обчислюване джерело Block Bindings API (agentic-lab/case-metrics) та еталонний CSRF-захищений метабокс.
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Metadata & Block Bindings Engine
* Description: Типізовані метаполя, кастомні джерела Block Bindings API (WP 7.0+) та захищені метабокси.
* Version: 2.1.0
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Core\Metadata;
use WP_Block;
use WP_Post;
if (!defined('ABSPATH')) {
exit;
}
final readonly class EnterpriseMetaEngine
{
public const META_ROI_PERCENT = '_case_roi_percent';
public const META_ARCHITECTURE_URL = '_case_architecture_url';
private const NONCE_ACTION = 'agentic_lab_save_case_meta';
private const NONCE_FIELD = '_agentic_lab_meta_nonce';
public static function init(): void
{
add_action('init', self::registerTypedPostMeta(...), 10);
add_action('init', self::registerCustomBlockBindingsSource(...), 15);
add_action('add_meta_boxes_case_study', self::registerMetaBox(...), 10);
add_action('save_post_case_study', self::saveMetaBoxData(...), 10, 2);
}
/**
* 1. Сувора реєстрація захищених метаполів (_prefix) із підтримкою Block Bindings та REST API.
*/
public static function registerTypedPostMeta(): void
{
// Оскільки ключ починається з '_', він вважається protected: обов'язковий auth_callback!
register_post_meta('case_study', self::META_ROI_PERCENT, [
'type' => 'integer',
'description' => 'Підтверджене зростання ROI проєкту у відсотках',
'single' => true,
'default' => 100,
'sanitize_callback' => static fn(mixed $val): int => max(0, min(10000, (int) $val)),
'auth_callback' => static fn(bool $allowed, string $metaKey, int $postId): bool => current_user_can('edit_post', $postId),
'show_in_rest' => true,
]);
register_post_meta('case_study', self::META_ARCHITECTURE_URL, [
'type' => 'string',
'description' => 'Посилання на архітектурну діаграму або репозиторій',
'single' => true,
'default' => '',
'sanitize_callback' => 'esc_url_raw',
'auth_callback' => static fn(bool $allowed, string $metaKey, int $postId): bool => current_user_can('edit_post', $postId),
'show_in_rest' => [
'schema' => [
'type' => 'string',
'format' => 'uri',
],
],
]);
}
/**
* 2. Реєстрація кастомного обчислюваного джерела даних для Block Bindings API (WP 6.5 - 7.0+).
* Дозволяє виводити відформатовані метрики в будь-якому блоці core/paragraph або core/heading.
*/
public static function registerCustomBlockBindingsSource(): void
{
if (!function_exists('register_block_bindings_source')) {
return;
}
register_block_bindings_source('agentic-lab/case-metrics', [
'label' => 'Метрики інженерного кейсу (Agentic Lab)',
'uses_context' => ['postId', 'postType'],
'get_value_callback' => static function (array $sourceArgs, WP_Block $blockInstance): ?string {
$postId = (int) ($blockInstance->context['postId'] ?? get_the_ID());
if ($postId <= 0) {
return null;
}
$metricType = (string) ($sourceArgs['metric'] ?? 'roi_badge');
if ($metricType === 'roi_badge') {
$roi = (int) get_post_meta($postId, self::META_ROI_PERCENT, true);
return sprintf('+%d%% Verified ROI', $roi);
}
return null;
},
]);
}
/**
* 3. Реєстрація класичного адміністративного метабоксу.
*/
public static function registerMetaBox(WP_Post $post): void
{
add_meta_box(
'agentic_lab_case_metrics_box',
'Інженерні метрики та архітектура (Fullstack Agentic Lab)',
self::renderMetaBox(...),
'case_study',
'side',
'high'
);
}
public static function renderMetaBox(WP_Post $post): void
{
$roi = (int) get_post_meta($post->ID, self::META_ROI_PERCENT, true);
$archUrl = (string) get_post_meta($post->ID, self::META_ARCHITECTURE_URL, true);
wp_nonce_field(self::NONCE_ACTION, self::NONCE_FIELD);
?>
<p>
<label for="agentic_roi_input"><strong>Зростання ROI (%):</strong></label><br/>
<input type="number" id="agentic_roi_input" name="agentic_roi_percent" value="<?php echo esc_attr((string) $roi); ?>" min="0" max="10000" class="widefat" />
</p>
<p>
<label for="agentic_arch_url"><strong>URL архітектури:</strong></label><br/>
<input type="url" id="agentic_arch_url" name="agentic_arch_url" value="<?php echo esc_url($archUrl); ?>" class="widefat" />
</p>
<?php
}
/**
* 4. Багатоступенева верифікація та збереження даних метабоксу.
*/
public static function saveMetaBoxData(int $postId, WP_Post $post): void
{
// Ігноруємо автозбереження та ревізії
if ((defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) || wp_is_post_revision($postId)) {
return;
}
// Перевірка CSRF Nonce-токена
$nonce = isset($_POST[self::NONCE_FIELD]) ? sanitize_text_field(wp_unslash((string) $_POST[self::NONCE_FIELD])) : '';
if ($nonce === '' || !wp_verify_nonce($nonce, self::NONCE_ACTION)) {
return;
}
// Перевірка контекстних прав доступу (map_meta_cap)
if (!current_user_can('edit_post', $postId)) {
return;
}
if (isset($_POST['agentic_roi_percent'])) {
$cleanRoi = max(0, min(10000, (int) $_POST['agentic_roi_percent']));
update_post_meta($postId, self::META_ROI_PERCENT, $cleanRoi);
}
if (isset($_POST['agentic_arch_url'])) {
$cleanUrl = esc_url_raw(wp_unslash((string) $_POST['agentic_arch_url']));
update_post_meta($postId, self::META_ARCHITECTURE_URL, $cleanUrl);
}
}
}
EnterpriseMetaEngine::init();У чому суть помилки: Розробник реєструє метаполе з ключем, що починається на символ підкреслення (_case_roi), вказує 'show_in_rest' => true, але забуває передати параметр 'auth_callback'.
Чому розробники так роблять: Незнання того, що функція is_protected_meta() в ядрі автоматично блокує запис будь-яких полів із префіксом _ через REST API.
Що відбувається в продакшені: При спробі зберегти запис у редакторі Gutenberg або змінити значення через Block Bindings API сервер повертає помилку rest_cannot_update: Sorry, you are not allowed to edit the _case_roi custom field.
Як правильно: Для всіх захищених метаполів із префіксом _ обов'язково вказувати 'auth_callback' => fn($allowed, $metaKey, $postId) => current_user_can('edit_post', $postId).
У чому суть помилки: Зберігання конфігурацій груп полів ACF виключно в базі даних (wp_posts з типом acf-field-group) та виклик get_field() у циклі для 50 карток на сторінці.
Чому розробники так роблять: Використання дефолтних налаштувань ACF після встановлення плагіна.
Що відбувається в продакшені: На кожен виклик get_field() ACF не лише зчитує саме значення з wp_postmeta, але й запитує об'єкт визначення поля (field_65a...) з таблиці wp_posts для форматування даних. Без папки acf-json це генерує десятки зайвих SQL-запитів, а під час деплою між гілками Git конфігурації полів у різних розробників розсинхронізуються.
Як правильно: Завжди створювати директорію acf-json/ у темі або реєструвати шлях через фільтри acf/settings/save_json та acf/settings/load_json.
У чому суть помилки: Виконання delete_post_meta() за відсутності $_POST['my_field'] на загальному хуку save_post без перевірки типу операції.
Чому розробники так роблять: Логіка вигляду «якщо чекбокс не прийшов у $_POST, значить користувач його зняв — видаляємо метаполе».
Що відбувається в продакшені: Коли фоновий сервіс WP-Cron, REST API або Quick Edit (швидке редагування в таблиці записів) оновлює заголовок поста, масив $_POST не містить полів вашого метабоксу. Обробник спрацьовує і стирає всі збережені метадані запису!
Як правильно: Завжди перевіряти валідність вашого конкретного Nonce-токена (wp_verify_nonce) ДО виконання будь-яких операцій update_post_meta або delete_post_meta. Якщо Nonce метабоксу не передано — функція повинна негайно завершитися (return).
У чому суть помилки: Збереження масиву з 500 пов'язаних елементів (наприклад, історії замовлень або списку підписників події) в одне метаполе через update_post_meta($id, 'subscribers_list', $hugeArray).
Чому розробники так роблять: WordPress автоматично викликає maybe_serialize(), створюючи ілюзію зручного документарного сховища.
Що відбувається в продакшені: Оскільки ядро завантажує всі метаполя поста в пам'ять єдиним викликом update_postmeta_cache(), гігантський серіалізований рядок на 5–10 МБ вичитується з MySQL і десеріалізується в пам'ять PHP при кожному перегляді сторінки або виведенні картки в архіві, миттєво переповнюючи ліміт пам'яті PHP-FPM.
Як правильно: Для колекцій 1:N використовувати окремі рядки метаполів (single => false), таксономії або виділені кастомні таблиці.
Реєстрація ключа на хуку init із жорсткою типізацією, sanitize_callback, auth_callback та схемою show_in_rest.
Відкриває доступ до поля для редактора Gutenberg та Block Bindings API.
Зв'язування поля зі стандартними блоками Gutenberg через атрибут metadata.bindings або панель ACF Local JSON.
Виключає необхідність верстати кастомні блоки заради виведення простих полів.
Верифікація Nonce, прав current_user_can('edit_post', $id) та захист від спрацьовування при автозбереженні.
Гарантує захист від CSRF-атак та випадкового стирання полів при Quick Edit.
Завантаження всіх метаполів вибірки постів за один SQL-запит WHERE post_id IN (...) у Redis / Memcached.
Повністю запобігає проблемі N+1 при рендерингу карток архіву.
Суворе фільтрування при виведенні в шаблон через esc_html(), esc_attr(), esc_url() або wp_kses_post().
Захищає фронтенд від Stored XSS навіть при компрометації облікового запису редактора.