title: >- Gestión Programática de Productos: Products CRUD, Lookup Tables y Stock Atómico description: >- Guía de ingeniería sobre el modelo de objetos de productos en WooCommerce 9.x (PHP 8.4+): polimorfismo de clases WC_Product, sincronización de tablas de consulta wp_wc_product_meta_lookup, prevención de Race Conditions en el descuento de existencias e importación masiva sin fugas de memoria. module: woocommerce order: 2 difficulty: advanced estimatedTime: 40 min topics:
En el ecosistema de WordPress, muchos desarrolladores cometen un error crítico: tratan los productos de WooCommerce como entradas convencionales del blog, leyendo atributos mediante get_post_meta($product_id, '_price') y mutándolos con update_post_meta($product_id, '_price', 99).
En el comercio electrónico real, esta práctica induce una desincronización inmediata del catálogo. WooCommerce no persiste un producto como un simple conjunto plano de metacampos. La arquitectura de productos se fundamenta en un Modelo de Objetos Polimórfico (la jerarquía WC_Product) y en Tablas de Consulta Desnormalizadas de Alta Velocidad (Lookup Tables: wp_wc_product_meta_lookup). En wp_wc_product_meta_lookup, el núcleo mantiene columnas precalculadas respaldadas por índices primarios B-Tree: min_price, max_price, stock_quantity, stock_status, rating_count, onsale y virtual.
Si actualiza el precio o el inventario mediante una llamada directa a update_post_meta(), el registro en wp_postmeta se modificará, pero la tabla del sistema wp_wc_product_meta_lookup permanecerá inalterada, y la caché de objetos wp_cache no se invalidará. Como consecuencia, el comprador observará el nuevo precio de 99 $ en la ficha del producto, pero los filtros facetados del catálogo y las búsquedas continuarán clasificando el artículo con el precio anterior de 150 $, quebrando la navegación de la tienda.
Únicamente la interacción a través de la capa CRUD oficial (WC_Product::save()) garantiza la propagación síncrona de relaciones, el recálculo de rangos de precio en productos variables y el descuento atómico de inventario blindado contra condiciones de carrera (Race Conditions).
Visualice la arquitectura de productos de WooCommerce como un almacén robotizado de alta precisión para productos farmacéuticos:
WC_Product representa el expediente digital del fármaco. Cuando el farmacéutico ajusta una dosis o precio en la terminal ($product->set_regular_price('45.00')), los cambios se retienen provisionalmente en el buffer local del objeto.$product->save() equivale a pulsar el comando «Consolidar Lote». En ese instante, el sistema automatizado ejecuta simultáneamente:
wp_posts / wp_postmeta);wp_wc_product_meta_lookup);clean_post_cache + invalidación de transitorios de precio);WC_Product_Variable).┌────────────────────────────────────────────────────────────────────────┐
│ SETTER DE APLICACIÓN: $product->set_regular_price(45)│
└───────────────────────────────────┬────────────────────────────────────┘
│ Invocación $product->save()
▼
┌────────────────────────────────────────────────────────────────────────┐
│ ORQUESTACIÓN TRANSACCIONAL: WC_Product_Data_Store_CPT │
├───────────────────────────────────┬────────────────────────────────────┤
│ • wp_posts: estado, título, slug │ • wp_wc_product_meta_lookup: │
│ • wp_postmeta: _price, _sku, peso │ min_price, max_price, stock, │
│ • wc_delete_product_transients() │ onsale, rating (1 SQL directo) │
│ • clean_post_cache($product_id) │ • Sincronización variaciones hijas │
└───────────────────────────────────┴────────────────────────────────────┘La factoría wc_get_product($id) evalúa la taxonomía product_type e instancia la subclase polimórfica adecuada (Simple, Variable, Variation, External).
Los setters fuertemente tipados (set_regular_price, set_manage_stock, set_sku) validan tipos y registran los atributos modificados en el buffer $changes.
El método $product->save() ejecuta escrituras SQL exclusivamente para los campos alterados, suprimiendo accesos redundantes a disco.
El Data Store actualiza atómicamente la fila en wp_wc_product_meta_lookup, sincronizando filtros de rango de precio, rebaja (onsale) y stock.
El núcleo ejecuta clean_post_cache() y purga transitorios de rangos de precio (wc_delete_product_transients), reflejando cambios en la tienda al instante.
En el ecosistema de ingeniería de Fullstack Agentic Lab, los patrones de Products CRUD son obligatorios para:
memory_limit = 256M).WC_Product_Variable enlazadas a decenas de variaciones WC_Product_Variation con SKUs, precios y stocks diferenciados.wc_update_product_stock() ante picos de cientos de compras por segundo.wp_wc_product_meta_lookup ante cualquier alteración de precios por código.Compare el impacto de emplear funciones de bajo nivel update_post_meta() frente al CRUD orientado a objetos de WC_Product:
A continuación se expone un artefacto de producción empresarial para WooCommerce 9.x (PHP 8.4+). Implementa un Enterprise Product Ingestion Engine:
<?php
/**
* Plugin Name: Fullstack Agentic Lab — Enterprise Product CRUD & Stock Engine
* Description: CRUD orientado a objetos de productos, inventario atómico e ingesta sin fugas de memoria.
* 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
{
/**
* Crea o actualiza un producto variable complejo con sincronización estricta de 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('El SKU del producto padre es obligatorio.');
}
// 1. Localizar producto existente por SKU o instanciar nuevo producto variable
$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("El producto con SKU {$sku} no es de tipo variable.");
}
// 2. Establecer metadatos base del producto padre
$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. Configurar atributo para variaciones (p. ej. Edición de Arquitectura)
$attribute = new WC_Product_Attribute();
$attribute->set_id(0); // Atributo personalizado de producto (no taxonomía global)
$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); // CRÍTICO: Habilita variaciones para este atributo
$product->set_attributes([$attribute]);
// Guardar el padre para obtener o ratificar parentId para las variaciones
$parentId = $product->save();
// 4. Sincronizar variaciones hijas
$this->syncVariations($parentId, $payload['variations'] ?? []);
// 5. CRÍTICO: Forzar el recálculo de límites de precio del producto padre
$product = wc_get_product($parentId);
if ($product instanceof WC_Product_Variable) {
$product->variable_product_sync();
}
return $parentId;
}
/**
* Crea o actualiza variaciones hijas vinculadas al padre.
*
* @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']);
}
// Gestión de existencias a nivel de variación individual
$variation->set_manage_stock(true);
$variation->set_stock_quantity((int) ($varData['stockQuantity'] ?? 0));
$variation->set_stock_status(((int) $varData['stockQuantity'] > 0) ? 'instock' : 'outofstock');
// Asignación de atributo a la variación
$variation->set_attributes([
'edition' => sanitize_text_field((string) $varData['edition']),
]);
// save() en la variación actualiza automáticamente wp_wc_product_meta_lookup
$variation->save();
}
}
/**
* Modificación atómica de inventario sin condiciones de carrera.
*
* @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("Producto con ID {$productId} no encontrado.");
}
// CRÍTICO: wc_update_product_stock() ejecuta un decremento SQL atómico
// en MySQL, protegiendo contra la sobreventa en compras simultáneas
$newStock = wc_update_product_stock($product, $quantity, $operation);
if (!is_numeric($newStock)) {
throw new RuntimeException("Fallo al actualizar el inventario del producto #{$productId}");
}
return (int) $newStock;
}
/**
* Procesamiento masivo de catálogo en streaming con generadores y purga de 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++;
// Cada 50 productos, vaciamos cachés y activamos el recolector de basura
if ($processedCount % $batchSize === 0) {
// Purgar caché de objetos de productos y posts para liberar memoria
wp_cache_flush_group('products');
wp_cache_flush_group('posts');
// Recolección cíclica forzada de PHP 8.4
gc_collect_cycles();
}
}
// Purga final de transitorios de precios del catálogo
wc_delete_product_transients();
return $processedCount;
}
}
Instancie WC_Product_Variable, defina título, descripción y SKU del producto padre, y llame a save() para obtener un parentId persistente.
Instancie WC_Product_Attribute, configure el listado de opciones, active el flag set_variation(true) y asigne mediante $product->set_attributes().
Para cada combinación de atributos, instancie WC_Product_Variation, asigne set_parent_id($parentId), especifique SKU, precio y stock, y llame a save().
Invoque $product->variable_product_sync() en la instancia padre para calcular y persistir los precios mínimo y máximo en wp_wc_product_meta_lookup.