HPOS — это High-Performance Order Storage, высокопроизводительное хранилище заказов в WooCommerce.
Если раньше заказы WooCommerce жили в стандартных таблицах WordPress wp_posts и wp_postmeta, то теперь для них используются отдельные таблицы WooCommerce:
wp_wc_orders
wp_wc_order_addresses
wp_wc_order_operational_data
wp_wc_orders_meta
Задача HPOS простая: меньше таскать заказы через общую помойку wp_posts, быстрее работать с заказами и не превращать базу данных магазина в кастрюлю с макаронами.
Что такое HPOS и зачем он нужен
WordPress изначально создавался не как e-commerce-платформа. Посты, страницы, вложения, товары, заказы — всё долгое время хранилось через одну и ту же модель данных.
Для блога это нормально.
Для магазина с большим количеством заказов — уже не очень.
Старый подход выглядел примерно так:
Заказ → wp_posts
Данные заказа → wp_postmeta
То есть заказ технически был обычным WordPress-постом с типом shop_order.
HPOS меняет эту схему. Заказы переезжают в отдельные таблицы, которые лучше подходят именно для заказов: статусы, адреса, суммы, даты, служебные данные.
Это не делает WooCommerce идеальным. Но это заметно лучше, чем пытаться вечно хранить коммерческие заказы как записи блога.
Что изменилось для разработчиков плагинов
Главное правило:
нельзя работать с заказами как с обычными WordPress-постами.
Раньше в коде часто встречалось такое:
$post = get_post( $order_id );
$status = get_post_status( $order_id );
$value = get_post_meta( $order_id, '_some_meta_key', true );
update_post_meta( $order_id, '_some_meta_key', 'value' );
При включённом HPOS такой код становится опасным.
Почему?
Потому что актуальные данные заказа могут находиться уже не в wp_posts и wp_postmeta, а в новых таблицах WooCommerce. В результате код может читать старые данные, писать не туда или просто ломать логику.
Правильный путь — использовать WooCommerce CRUD API:
$order = wc_get_order( $order_id );
Дальше работаем уже не с WP_Post, а с объектом WC_Order.
Как адаптировать код под HPOS
Заменяем get_post() на wc_get_order()
Было:
$post = get_post( $order_id );
if ( ! $post || 'shop_order' !== $post->post_type ) {
return;
}
Стало:
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
wc_get_order() вернёт объект заказа независимо от того, где сейчас WooCommerce хранит данные: в старых таблицах WordPress или в HPOS.
Заменяем get_post_meta() на get_meta()
Было:
$external_id = get_post_meta( $order_id, '_external_order_id', true );
Стало:
$order = wc_get_order( $order_id );
if ( ! $order ) {
return null;
}
$external_id = $order->get_meta( '_external_order_id' );
Так код будет корректно работать и со старым хранилищем, и с HPOS.
Заменяем update_post_meta() на update_meta_data()
Было:
update_post_meta( $order_id, '_external_order_id', $external_id );
Стало:
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$order->update_meta_data( '_external_order_id', $external_id );
$order->save();
Важно: после изменения метаданных нужно вызвать:
$order->save();
Без save() изменения могут не сохраниться.
Но и дергать save() после каждой мелочи тоже не надо. Если обновляете несколько полей, лучше обновить всё и сохранить заказ один раз:
$order->update_meta_data( '_external_order_id', $external_id );
$order->update_meta_data( '_sync_status', 'success' );
$order->update_meta_data( '_sync_at', time() );
$order->save();
Заменяем delete_post_meta()
Было:
delete_post_meta( $order_id, '_sync_error' );
Стало:
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
$order->delete_meta_data( '_sync_error' );
$order->save();
Проверяем, является ли ID заказом
Старый вариант:
if ( 'shop_order' !== get_post_type( $order_id ) ) {
return;
}
Лучше использовать OrderUtil:
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( ! OrderUtil::is_order( $order_id, wc_get_order_types() ) ) {
return;
}
Или если нужно получить тип заказа:
use Automattic\WooCommerce\Utilities\OrderUtil;
$order_type = OrderUtil::get_order_type( $order_id );
Проверяем, включён ли HPOS
В большинстве случаев это не нужно. Если вы используете wc_get_order() и методы WC_Order, WooCommerce сам разберётся, из каких таблиц читать данные.
Но если у вас есть низкоуровневый код, кастомные SQL-запросы или сложная логика миграции, можно проверить активное хранилище:
use Automattic\WooCommerce\Utilities\OrderUtil;
if ( OrderUtil::custom_orders_table_usage_is_enabled() ) {
// HPOS включён и используется.
} else {
// Используется старое хранилище через wp_posts/wp_postmeta.
}
Обновляем запросы заказов
Если раньше вы искали заказы через get_posts():
$orders = get_posts( [
'post_type' => 'shop_order',
'post_status' => 'wc-completed',
'numberposts' => 10,
] );
лучше перейти на wc_get_orders():
$orders = wc_get_orders( [
'status' => 'completed',
'limit' => 10,
] );
Если нужно искать по метаполю:
$orders = wc_get_orders( [
'limit' => 10,
'meta_query' => [
[
'key' => '_external_order_id',
'value' => $external_id,
],
],
] );
Если нужно искать по полям заказа:
$orders = wc_get_orders( [
'field_query' => [
[
'field' => 'total',
'value' => 1000,
'compare' => '>=',
'type' => 'NUMERIC',
],
],
] );
Для дат тоже есть нормальный вариант:
$orders = wc_get_orders( [
'date_query' => [
[
'column' => 'date_created_gmt',
'after' => '1 month ago',
],
],
] );
То есть не нужно руками писать SQL в wp_posts, если задача решается через API WooCommerce.
Как объявить совместимость плагина с HPOS
После того как код проверен и адаптирован, нужно явно сообщить WooCommerce, что плагин совместим с HPOS.
Добавьте в основной файл плагина:
add_action( 'before_woocommerce_init', function () {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
true
);
}
} );
Третий аргумент true означает: плагин совместим с HPOS.
Если плагин точно не готов, можно явно объявить несовместимость:
add_action( 'before_woocommerce_init', function () {
if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
__FILE__,
false
);
}
} );
Но лучше, конечно, не гордо объявлять «я несовместим», а привести код в порядок.
Если декларация не в главном файле плагина
Если код находится не в основном файле плагина, вместо __FILE__ нужно передать путь к главному файлу:
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
'custom_order_tables',
'my-plugin/my-plugin.php',
true
);
Что проверить в своём коде
Ищите всё, что напрямую работает с заказами как с постами WordPress.
В первую очередь:
get_post()
get_posts()
get_post_meta()
update_post_meta()
delete_post_meta()
get_post_type()
get_post_status()
wp_insert_post()
wp_update_post()
wp_delete_post()
$wpdb
shop_order
wp_posts
wp_postmeta
Само наличие этих функций не всегда ошибка. Например, get_post_meta() может использоваться для обычной записи WordPress, а не для заказа.
Проблема начинается там, где эти функции используются именно для WooCommerce-заказов.
Правильная логика такая:
Работаем с обычными постами WordPress → можно использовать WP API.
Работаем с заказами WooCommerce → используем WC_Order и WooCommerce CRUD API.
Как тестировать HPOS
Минимальный план проверки:
- Сделать копию сайта на staging.
- Обновить WooCommerce.
- Включить HPOS в настройках WooCommerce.
- Включить compatibility mode на время проверки.
- Прогнать основные сценарии:
- создание заказа;
- оплата;
- смена статуса;
- возврат;
- сохранение метаданных;
- интеграции с доставкой, оплатой, CRM, iiko, 1С и другими внешними системами.
- Проверить логи WooCommerce и PHP.
- Проверить, нет ли прямых SQL-запросов к заказам через
wp_postsиwp_postmeta.
Для WP-CLI сейчас используется пространство команд:
wp wc hpos status
Проверить статус HPOS:
wp wc hpos status
Синхронизировать данные между старым и новым хранилищем:
wp wc hpos sync
Проверить расхождения между хранилищами:
wp wc hpos verify_data
Посмотреть различия по конкретному заказу:
wp wc hpos diff 123
Где 123 — ID заказа.
В старых статьях можно встретить команды вида:
wp wc cot sync
Но в актуальной документации WooCommerce команды HPOS находятся в namespace:
wp wc hpos
Короткий чек-лист адаптации
Если совсем кратко, адаптация под HPOS выглядит так:
1. Не работаем с заказами как с WP_Post.
2. get_post() меняем на wc_get_order().
3. get_post_meta() меняем на $order->get_meta().
4. update_post_meta() меняем на $order->update_meta_data().
5. delete_post_meta() меняем на $order->delete_meta_data().
6. После изменения данных вызываем $order->save().
7. get_posts() по заказам меняем на wc_get_orders().
8. Проверки shop_order переводим на OrderUtil.
9. Прямые SQL-запросы к wp_posts/wp_postmeta убираем или переписываем.
10. Добавляем declare_compatibility( 'custom_order_tables', ..., true ).
11. Проверяем плагин на staging с включённым HPOS.
Вывод
HPOS — это уже не экспериментальная штука «на будущее». Это нормальное хранилище заказов WooCommerce, с которым нужно считаться.
Если плагин работает с заказами через get_post(), get_post_meta() и прямые запросы к wp_posts, его нужно адаптировать.
Хорошая новость: в большинстве случаев всё сводится к простой замене старого WordPress-подхода на WooCommerce CRUD API:
$order = wc_get_order( $order_id );
А дальше работаем с заказом как с объектом WC_Order.
Да, WooCommerce всё ещё местами похож на старый сарай, к которому пристроили второй этаж, балкон и лифт. Но HPOS — как минимум полезная пристройка. И если ваш плагин работает с заказами, лучше привести код в порядок сейчас, чем потом ловить странные баги на живом магазине.