<? phpukraine СТАТТІ
⌕Пошук по платформі
WORDPRESS 29 вересня 2026 · 8 хв читання

WooCommerce HPOS: що змінилось для плагінів і як перевести магазин

HPOS переносить замовлення з `wp_posts` і `wp_postmeta` у чотири власні таблиці, і після перемикання будь-який `get_post_meta($order_id, ...)` у вашому коді починає тихо повертати порожнечу. Розбираємо, як виглядає нова схема, які CRUD-методи й хуки замінюють роботу з постами, як оголосити сумісність через `FeaturesUtil` і в якому порядку безпечно вмикати синхронізацію, перемикати авторитетне сховище й перевіряти результат.

РP
Редакція phpukraine
Редакція платформи

Симптом виглядає так: плагін працював роками, власник магазину поставив галочку в налаштуваннях WooCommerce, і трек-номери в замовленнях зникли. Не видалились, а просто перестали читатися: запис лежить у wp_postmeta, а код тепер читає замовлення з іншої таблиці. Це найтиповіша зустріч із HPOS.

До HPOS замовлення було постом. Рядок у wp_posts із post_type = 'shop_order', а всі поля жили в wp_postmeta: _billing_email, _order_total, _customer_user, _payment_method і ще кілька десятків ключів. Одне замовлення - один пост плюс 30-40 рядків мети. Запит «замовлення цього клієнта за жовтень зі статусом processing» перетворюється на кілька self-join'ів wp_postmeta із собою, причому в таблиці, яку замовлення ділять з товарами, сторінками й усім кешем від інших плагінів. Індекси там рівно два: по post_id і по meta_key (перші 191 символ). Ні типів, ні складених індексів під конкретні поля.

HPOS (High-Performance Order Storage) розкладає це на чотири таблиці:

  • wc_orders - ядро: id, status, currency, total_amount, customer_id, billing_email, дати створення й оновлення, payment_method;
  • wc_order_addresses - адреси, по рядку на тип (billing, shipping);
  • wc_order_operational_data - технічне: cart hash, чи включений податок у ціни, дата оплати й завершення, версія, якою створено замовлення;
  • wc_orders_meta - довільна мета, яку пишуть плагіни.

Колонки типізовані, індекси нормальні, і фільтр за статусом і датою стає читанням індексу замість трьох join'ів. Плюс побічні ефекти: записи в замовлення більше не смикають wp_posts, а створення замовлення не тягне за собою весь життєвий цикл поста з ревізіями й хуками wp_insert_post, на які підписана половина плагінів на сайті.

Аналітичні таблиці на кшталт wc_order_stats існували й до HPOS і залишаються тим, чим були: похідними даними для звітів, а не джерелом істини.

Починаючи з WooCommerce 8.2, HPOS увімкнений за замовчуванням для нових магазинів. Наявні працюють на постах, доки власник не перемкне вручну.

Сумісність плагіна: CRUD-методи замість post meta

Правило одне: id замовлення не є id поста, і поводитися з ним як з постом не можна. Жодних get_post(), get_post_meta(), update_post_meta(), wp_update_post(), get_post_status() на замовленнях. Небезпека тут не в помилці, а в тиші: update_post_meta() запише рядок у wp_postmeta, функція повернеться успішно, і дані просто нікому не потрібні.

Заміни прямі:

// було
$tracking = get_post_meta($order_id, '_myplugin_tracking', true);
update_post_meta($order_id, '_myplugin_tracking', $new_tracking);

// стало
$order = wc_get_order($order_id);

if (! $order instanceof WC_Order) {
    return;
}

$tracking = $order->get_meta('_myplugin_tracking', true);

$order->update_meta_data('_myplugin_tracking', $new_tracking);
$order->save();

$order->save() тут обов'язковий: update_meta_data() змінює об'єкт у пам'яті. Якщо правите кілька полів, зберігайте один раз у кінці, а не після кожного. Для видалення є delete_meta_data(), для всього набору - get_meta_data() і save_meta_data().

Поля, які в HPOS стали колонками, читаються геттерами, а не як мета. $order->get_meta('_billing_email') після міграції порожній, бо email живе в колонці: потрібен $order->get_billing_email(). Так само get_status() (без префікса wc-), get_total(), get_customer_id(), get_date_created() замість $post->post_date.

Вибірки замовлень через WP_Query або get_posts() з post_type => 'shop_order' повертають порожній результат, як тільки авторитетним сховищем стає HPOS і синхронізацію вимкнено. Заміна - wc_get_orders(), який під капотом ходить у правильне сховище:

$order_ids = wc_get_orders([
    'status'     => ['wc-processing', 'wc-on-hold'],
    'date_after' => '2026-09-01',
    'limit'      => 200,
    'return'     => 'ids',
    'meta_query' => [
        [
            'key'     => '_myplugin_tracking',
            'compare' => 'NOT EXISTS',
        ],
    ],
]);

Окремо варто сказати про хуки. save_post, save_post_shop_order, wp_insert_post, transition_post_status на замовленнях більше не спрацьовують, бо поста ніхто не створює. Замість них woocommerce_new_order, woocommerce_update_order і woocommerce_after_order_object_save. woocommerce_update_order може спрацювати кілька разів за один запит (кожен save() - це подія), тому обробник має бути ідемпотентним і не надсилати листи на кожен виклик.

В адмінці змінюється екран. Замовлення тепер не edit.php?post_type=shop_order, а admin.php?page=wc-orders, і всі хуки, що містять screen id, треба будувати динамічно:

add_action('add_meta_boxes', static function (): void {
    add_meta_box(
        'myplugin_delivery',
        'Доставка',
        'myplugin_render_delivery_box',
        wc_get_page_screen_id('shop-order'),
        'side'
    );
});

function myplugin_render_delivery_box(WP_Post|WC_Order $post_or_order): void
{
    $order = $post_or_order instanceof WP_Post
        ? wc_get_order($post_or_order->ID)
        : $post_or_order;

    // ...
}

wc_get_page_screen_id('shop-order') віддає woocommerce_page_wc-orders під HPOS і shop_order на легасі, тому один і той самий код працює в обох режимах. Другий аргумент колбека мета-боксу приходить різним типом залежно від режиму, звідси перевірка через instanceof. Хук збереження woocommerce_process_shop_order_meta живий в обох варіантах, посилання на редагування краще брати з $order->get_edit_order_url(), а не збирати рядок руками.

Список замовлень в адмінці під HPOS - звичайна WP_List_Table на сторінці плагіна, а не таблиця постів, тому фільтри manage_edit-shop_order_columns і manage_shop_order_posts_custom_column там не діють. Назви нових фільтрів виводяться з того ж screen id.

Оголошення сумісності

WooCommerce не вміє сам визначити, чи плагін готовий. Він питає: кожен плагін оголошує сумісність із фічею на хуку before_woocommerce_init, до того як ядро зібрало список.

add_action('before_woocommerce_init', static function (): void {
    if (! class_exists(\Automattic\WooCommerce\Utilities\FeaturesUtil::class)) {
        return;
    }

    \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
        'custom_order_tables',
        __FILE__,
        true
    );
});

__FILE__ має бути головним файлом плагіна, тим самим, який WordPress бачить у списку активних. Третій аргумент - власне відповідь: false оголошує несумісність явно, і це нормальний крок, поки код не переписаний.

Результат видно в WooCommerce → Налаштування → Розширені → Функції: там три групи - сумісні, несумісні й ті, що не сказали нічого. Плагін без оголошення потрапляє в невизначені й попадається власнику на очі як ризик. Якщо активний хоч один несумісний плагін, WooCommerce не дасть просто так увімкнути HPOS.

Оголошення - це обіцянка, а не перевірка. Ядро вірить на слово, тому declare_compatibility(..., true) ставлять після того, як з коду прибрані всі звернення до постмети замовлень, а не одночасно з планами це зробити.

Міграція і синхронізація даних

Перемикачі живуть у тому самому розділі налаштувань і зводяться до двох опцій у базі: woocommerce_custom_orders_table_enabled (яке сховище авторитетне) і woocommerce_custom_orders_table_data_sync_enabled (режим сумісності, тобто двостороння синхронізація постів і нових таблиць).

Порядок, який дає шлях назад:

  1. Бекап бази. Не «швидкий дамп після», а справжній бекап до.
  2. Увімкнути режим сумісності, залишивши авторитетними пости. WooCommerce поставить фоновий процес через Action Scheduler і перенесе наявні замовлення в нові таблиці партіями. На магазині з сотнями тисяч замовлень це години, і воно залежить від того, чи взагалі жива черга Action Scheduler.
  3. Дочекатися, доки синхронізувати буде нічого, і порівняти дані.
  4. Перемкнути авторитетне сховище на HPOS, синхронізацію поки не вимикати. Тиждень-два в цьому стані - ваша страховка: повернення до постів залишається питанням однієї галочки.
  5. Вимкнути синхронізацію, коли переконалися, що все працює. Тримати її назавжди означає платити подвійною записною роботою за кожне замовлення.
  6. Прибрати легасі-дані з wp_posts і wp_postmeta окремою дією, вже після цього.

На великих магазинах через адмінку це робити незручно, є CLI:

wp wc hpos status
wp wc hpos sync --batch-size=500
wp wc hpos verify_data --batch-size=500

status показує, скільки замовлень лишилось несинхронізованими й який режим активний. sync крутить міграцію в лоб, без очікування на фонову чергу, і його спокійно ставити під flock у нічний cron. verify_data порівнює рядки в постах і в нових таблицях і скаржиться на розбіжності; у нього є опція повторної міграції знайдених проблемних замовлень. Повний перелік команд і прапорців звірте локально через wp help wc hpos, бо набір поповнювався з версіями.

Типове джерело розбіжностей у звіті - мета з нестандартною серіалізацією або дублікати одного ключа, які легасі-схема допускала. Їх видно поштучно, і полагодити їх краще до перемикання, а не після.

Що перевірити після перемикання

Автотести WooCommerce покривають ядро, ваш магазин - ні, тому прохід руками обов'язковий. На стейджингу з копією продакшн-бази:

  • оформити замовлення з фронтенду й переконатися, що воно з'явилось в адмінці з правильними сумами й адресами;
  • зробити часткове й повне повернення (повернення - теж рядок у wc_orders, свого типу);
  • створити замовлення вручну з адмінки, змінити статус, перевірити, що пішли листи;
  • пошук в адмінці за email, телефоном, номером замовлення й прізвищем;
  • свої мета-бокси: і читання, і збереження, і на новому замовленні, і на старому міграційному;
  • усі місця, де код ходить у базу напряму. grep по wp_posts, postmeta, shop_order, get_post_meta у плагінах і темі дає реальний список роботи швидше за будь-який аудит;
  • cron-задачі й фонові процеси, які перебирали замовлення через get_posts();
  • REST API і вебхуки для зовнішніх інтеграцій: складський облік, доставка, бухгалтерія;
  • експорт та імпорт CSV;
  • звіти в Analytics і збіг цифр із тим, що було до перемикання.

Окремо подивіться на slow query log перші кілька днів. Буває, що плагін формально працює, але замість індексованої колонки читає те саме поле через wc_orders_meta, і запит стає гіршим за легасі-варіант.

І найголовніше: план відкату існує лише поки увімкнена синхронізація. Після її вимкнення й прибирання постів шлях назад - це відновлення з бекапу.

ПИШЕТЕ ПРО PHP?Опублікуйте розбір або історію з проєкту на платформіРедактор із чеклістом, редактура, авторська сторінка. Републікація з блогу отримує canonical на оригінал. Відкрити редактор →
РP
Редакція phpukraine
Редакція платформи
Матеріали, які готує команда платформи на основі власних даних: каталогу вакансій, зарплатного звіту й банку питань. Кожна цифра в них рахується з бази, а не береться з голови.
оновлено 30 вересня 2026 · ліцензія CC-BY-SA-4.0
ДАЛІ ПО ТЕМІ
ЧИТАТИ ДАЛІ
← Усі статті