title: >- Программное управление товарами: Products CRUD, Lookup Tables и атомарный склад description: >- Инженерное руководство по объектной модели товаров в WooCommerce 9.x (PHP 8.4+): полиморфизм классов WC_Product, синхронизация служебных таблиц фильтрации wp_wc_product_meta_lookup, предотвращение Race Conditions при списании остатков и высокоскоростной пакетный импорт без утечек памяти. module: woocommerce order: 2 difficulty: advanced estimatedTime: 40 мин topics:
В экосистеме WordPress многие начинающие разработчики совершают фатальную ошибку: они обращаются с товарами WooCommerce так же, как с обычными постами блога — читают данные через get_post_meta($product_id, '_price') и обновляют их через update_post_meta($product_id, '_price', 99).
В реальном e-commerce такая практика приводит к немедленной рассинхронизации каталога. WooCommerce не хранит товар как плоский набор мета-полей. Архитектура товаров опирается на Полиморфную Объектную Модель (семейство WC_Product) и специализированные Денормализованные Таблицы Быстрого Поиска (Lookup Tables: wp_wc_product_meta_lookup). В таблице wp_wc_product_meta_lookup ядро держит предвычисленные колонки с первичными B-Tree индексами: min_price, max_price, stock_quantity, stock_status, rating_count, onsale и virtual.
Если вы обновите цену или остаток товара прямым вызовом update_post_meta(), запись в wp_postmeta изменится, но системная таблица wp_wc_product_meta_lookup останется нетронутой, а объектный кэш wp_cache не сбросится. В результате покупатель откроет карточку товара и увидит новую цену $99, но в фильтре каталога по цене или в поиске товар будет по-прежнему классифицироваться по старой цене $150, а фасетная навигация магазина сломается.
Только работа через официальный CRUD-слой (WC_Product::save()) гарантирует синхронное обновление всех реляционных связей, пересчёт диапазонов цен у вариативных товаров и корректное списание складских остатков без состояния гонки (Race Conditions).
Представьте архитектуру товаров WooCommerce как высокоавтоматизированный аптечный робот-склад:
WC_Product — это электронная сервисная карта лекарства. Когда провизор меняет цену или дозировку в системе ($product->set_regular_price('45.00')), изменения сначала фиксируются в локальном буфере карты.$product->save() — это нажатие кнопки «Применить изменения». В этот момент робот одновременно:
wp_posts / wp_postmeta);wp_wc_product_meta_lookup);clean_post_cache + сброс транзиентов цен);WC_Product_Variable).┌────────────────────────────────────────────────────────────────────────┐
│ ПРИКЛАДНОЙ СЕТТЕР: $product->set_regular_price('45') │
└───────────────────────────────────┬────────────────────────────────────┘
│ Вызов $product->save()
▼
┌────────────────────────────────────────────────────────────────────────┐
│ ОРКЕСТРАЦИЯ ТРАНЗАКЦИИ: WC_Product_Data_Store_CPT │
├───────────────────────────────────┬────────────────────────────────────┤
│ • wp_posts: статус, заголовок, слаг│ • wp_wc_product_meta_lookup: │
│ • wp_postmeta: _price, _sku, вес │ min_price, max_price, stock, │
│ • wc_delete_product_transients() │ onsale, rating (1 быстрый SQL) │
│ • clean_post_cache($product_id) │ • Синхронизация дочерних вариаций │
└───────────────────────────────────┴────────────────────────────────────┘Фабрика wc_get_product($id) определяет тип товара (product_type таксономия) и инстанциирует правильный полиморфный подкласс (Simple, Variable, Variation, External).
Сеттеры (set_regular_price, set_manage_stock, set_sku) валидируют типы данных и помечают изменённые поля во внутреннем массиве $changes.
Метод $product->save() выполняет SQL-записи только для изменившихся полей, исключая холостые обращения к дисковой подсистеме.
Data Store атомарно обновляет строку в wp_wc_product_meta_lookup, синхронизируя фильтры цен, признак распродажи (onsale) и статус наличия.
Ядро вызывает clean_post_cache() и очищает транзиенты ценовых диапазонов каталога (wc_delete_product_transients), делая изменения видимыми мгновенно.
Создайте экземпляр WC_Product_Variable, задайте имя, описание, артикул родителя и вызовите save(), чтобы получить первичный parentId.
Создайте объект WC_Product_Attribute, добавьте массив значений (опций), выставите флаг set_variation(true) и передайте в $product->set_attributes().
Для каждой комбинации атрибутов создайте WC_Product_Variation, привяжите set_parent_id($parentId), укажите уникальный SKU, цену и остаток и вызовите save().
Вызовите метод $product->variable_product_sync() у родителя, чтобы обновить минимальную и максимальную цену в wp_wc_product_meta_lookup.
Почему в коде плагинов и интеграций запрещено напрямую обновлять цену товара через update_post_meta($id, '_price', $price)?
В инженерном стеке Fullstack Agentic Lab паттерны Products CRUD применяются для:
memory_limit = 256M).WC_Product_Variable со связыванием десятков дочерних WC_Product_Variation с уникальными SKU и ценами.wc_update_product_stock() при пиковых нагрузках до сотен покупок в секунду.wp_wc_product_meta_lookup при любых программных изменениях цен.Сравним последствия использования низкоуровневых функций update_post_meta() и официального объектного CRUD WC_Product:
Ниже представлен эталонный производственный артефакт для WooCommerce 9.x (PHP 8.4+). Он реализует высокопроизводительный Enterprise Product Ingestion Engine:
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Product CRUD & Stock Engine
* Description: Объектный CRUD товаров, атомарный склад и пакетный импорт без утечек памяти.
* Version: 3.1.0
* Requires at least: 6.8
* Requires PHP: 8.4
* Author: Artur Yusupov (Fullstack Agentic Lab)
*/
declare(strict_types=1);
namespace AgenticLab\Commerce\Catalog;
use WC_Product;
use WC_Product_Simple;
use WC_Product_Variable;
use WC_Product_Variation;
use WC_Product_Attribute;
use Generator;
use RuntimeException;
defined('ABSPATH') || exit;
final readonly class EnterpriseProductService
{
/**
* Создает или обновляет сложный вариативный товар со строгой синхронизацией Lookup Tables.
*
* @param array<string, mixed> $payload
*/
public function upsertVariableProduct(array $payload): int
{
$sku = sanitize_text_field((string) ($payload['sku'] ?? ''));
if ($sku === '') {
throw new RuntimeException('Артикул (SKU) родительского товара обязателен.');
}
// 1. Ищем существующий товар по SKU или создаем новый объект
$productId = wc_get_product_id_by_sku($sku);
$product = $productId > 0 ? wc_get_product($productId) : new WC_Product_Variable();
if (!$product instanceof WC_Product_Variable) {
throw new RuntimeException("Товар с артикулом {$sku} не является вариативным.");
}
// 2. Заполняем базовые метаданные родительского товара
$product->set_name(sanitize_text_field((string) $payload['name']));
$product->set_status('publish');
$product->set_sku($sku);
$product->set_description(wp_kses_post((string) ($payload['description'] ?? '')));
$product->set_short_description(sanitize_textarea_field((string) ($payload['shortDescription'] ?? '')));
// 3. Конфигурируем глобальный атрибут вариации (например, Архитектурная редакция)
$attribute = new WC_Product_Attribute();
$attribute->set_id(0); // Кастомный атрибут товара (не глобальная таксономия)
$attribute->set_name('Edition');
$attribute->set_options(['Standard Architecture', 'High-Load Cluster', 'AI Enterprise']);
$attribute->set_position(0);
$attribute->set_visible(true);
$attribute->set_variation(true); // КРИТИЧНО: флаг разрешает вариации по этому атрибуту!
$product->set_attributes([$attribute]);
// Сохраняем родителя — получаем $parentId для привязки вариаций
$parentId = $product->save();
// 4. Синхронизируем дочерние вариации
$this->syncVariations($parentId, $payload['variations'] ?? []);
// 5. КРИТИЧНО: Принудительно пересчитываем диапазоны цен родительского товара
$product = wc_get_product($parentId);
if ($product instanceof WC_Product_Variable) {
$product->variable_product_sync();
}
return $parentId;
}
/**
* Создает или обновляет дочерние вариации с привязкой к родителю.
*
* @param array<int, array<string, mixed>> $variationsData
*/
private function syncVariations(int $parentId, array $variationsData): void
{
foreach ($variationsData as $varData) {
$varSku = sanitize_text_field((string) $varData['sku']);
$varId = wc_get_product_id_by_sku($varSku);
$variation = $varId > 0 ? new WC_Product_Variation($varId) : new WC_Product_Variation();
$variation->set_parent_id($parentId);
$variation->set_sku($varSku);
$variation->set_regular_price((string) $varData['regularPrice']);
if (!empty($varData['salePrice'])) {
$variation->set_sale_price((string) $varData['salePrice']);
}
// Управление остатками на уровне конкретной вариации
$variation->set_manage_stock(true);
$variation->set_stock_quantity((int) ($varData['stockQuantity'] ?? 0));
$variation->set_stock_status(((int) $varData['stockQuantity'] > 0) ? 'instock' : 'outofstock');
// Привязка атрибута вариации
$variation->set_attributes([
'edition' => sanitize_text_field((string) $varData['edition']),
]);
// save() вариации автоматически обновляет wp_wc_product_meta_lookup!
$variation->save();
}
}
/**
* Атомарное изменение складского остатка без Race Conditions.
*
* @param 'increase'|'decrease'|'set' $operation
*/
public function updateStockAtomically(int $productId, int $quantity, string $operation = 'decrease'): int
{
$product = wc_get_product($productId);
if (!$product instanceof WC_Product) {
throw new RuntimeException("Товар с ID {$productId} не найден.");
}
// КРИТИЧНО: Функция wc_update_product_stock() выполняет атомарный SQL-декремент
// в базе данных, защищая от оверселлинга при параллельных заказах!
$newStock = wc_update_product_stock($product, $quantity, $operation);
if (!is_numeric($newStock)) {
throw new RuntimeException("Ошибка списания остатков для товара #{$productId}");
}
return (int) $newStock;
}
/**
* Потоковая пакетная обработка каталога на генераторах с очисткой RAM.
*
* @param Generator<array<string, mixed>> $productStream
*/
public function processBatchStream(Generator $productStream, int $batchSize = 50): int
{
$processedCount = 0;
foreach ($productStream as $item) {
$this->upsertVariableProduct($item);
$processedCount++;
// Каждые 50 товаров сбрасываем кэш и собираем циклический мусор PHP
if ($processedCount % $batchSize === 0) {
// Очищаем объектный кэш постов и товаров для освобождения RAM
wp_cache_flush_group('products');
wp_cache_flush_group('posts');
// Принудительный запуск сборщика мусора PHP 8.4
gc_collect_cycles();
}
}
// Финальный сброс кэша диапазонов каталога
wc_delete_product_transients();
return $processedCount;
}
}
Как предотвратить оверселлинг (продажу в минус) при одновременной покупке последнего экземпляра товара двумя пользователями?