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), роблячи зміни видимими миттєво.
В інженерному стеку 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;
}
}
Створіть екземпляр 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.