Files
WooNooW/includes/Api/ProductsController.php
dwindown 304a58d8a1 fix: Force fresh data fetch and improve variation attribute handling
Fixed 2 issues:

1. Frontend Showing Stale Data - FIXED
   Problem: Table shows "Simple" even though API returns "variable"
   Root Cause: React Query caching old data

   Solution (index.tsx):
   - Added staleTime: 0 (always fetch fresh)
   - Added gcTime: 0 (don't cache)
   - Forces React Query to fetch from API every time

   Result: Table will show correct product type

2. Variation Attribute Values - IMPROVED
   Problem: attributes show { "Color": "" } instead of { "Color": "Red" }

   Improvements:
   - Use wc_attribute_label() for proper attribute names
   - Better handling of global vs custom attributes
   - Added debug logging to see raw WooCommerce data

   Debug Added:
   - Logs raw variation attributes to debug.log
   - Check: wp-content/debug.log
   - Shows what WooCommerce actually returns

   Note: If attribute values still empty, it means:
   - Variations not properly saved in WooCommerce
   - Need to re-save product or regenerate variations

Test:
1. Refresh products page
2. Should show correct type (variable)
3. Check debug.log for variation attribute data
4. If still empty, re-save the variable product
2025-11-20 00:32:42 +07:00

672 lines
20 KiB
PHP

<?php
/**
* Products REST API Controller
*
* Handles CRUD operations for WooCommerce products
* Supports simple and variable products with comprehensive data
*
* @package WooNooW\Api
*/
namespace WooNooW\Api;
use WP_REST_Request;
use WP_REST_Response;
use WP_Error;
use WC_Product;
use WC_Product_Simple;
use WC_Product_Variable;
use WC_Product_Variation;
class ProductsController {
/**
* Register REST API routes
*/
public static function register_routes() {
// List products
register_rest_route('woonoow/v1', '/products', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_products'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Get single product
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_product'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Create product
register_rest_route('woonoow/v1', '/products', [
'methods' => 'POST',
'callback' => [__CLASS__, 'create_product'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Update product
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
'methods' => 'PUT',
'callback' => [__CLASS__, 'update_product'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Delete product
register_rest_route('woonoow/v1', '/products/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => [__CLASS__, 'delete_product'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Get product categories
register_rest_route('woonoow/v1', '/products/categories', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_categories'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Get product tags
register_rest_route('woonoow/v1', '/products/tags', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_tags'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
// Get product attributes
register_rest_route('woonoow/v1', '/products/attributes', [
'methods' => 'GET',
'callback' => [__CLASS__, 'get_attributes'],
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
}
/**
* Get products list with filters
*/
public static function get_products(WP_REST_Request $request) {
$page = max(1, (int) $request->get_param('page'));
$per_page = min(100, max(1, (int) ($request->get_param('per_page') ?: 20)));
$search = $request->get_param('search');
$status = $request->get_param('status');
$category = $request->get_param('category');
$type = $request->get_param('type');
$stock_status = $request->get_param('stock_status');
$orderby = $request->get_param('orderby') ?: 'date';
$order = $request->get_param('order') ?: 'DESC';
$args = [
'post_type' => 'product',
'posts_per_page' => $per_page,
'paged' => $page,
'orderby' => $orderby,
'order' => $order,
];
// Search
if ($search) {
$args['s'] = sanitize_text_field($search);
}
// Status filter
if ($status) {
$args['post_status'] = $status;
} else {
$args['post_status'] = ['publish', 'draft', 'pending', 'private'];
}
// Category filter
if ($category) {
$args['tax_query'] = [
[
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => (int) $category,
],
];
}
// Type filter
if ($type) {
$args['tax_query'] = $args['tax_query'] ?? [];
$args['tax_query'][] = [
'taxonomy' => 'product_type',
'field' => 'slug',
'terms' => $type,
];
}
// Stock status filter
if ($stock_status) {
$args['meta_query'] = [
[
'key' => '_stock_status',
'value' => $stock_status,
],
];
}
$query = new \WP_Query($args);
$products = [];
foreach ($query->posts as $post) {
$product = wc_get_product($post->ID);
if ($product) {
$formatted = self::format_product_list_item($product);
// Debug: Log first product to verify structure
if (empty($products)) {
error_log('WooNooW Debug - First product data: ' . print_r($formatted, true));
}
$products[] = $formatted;
}
}
$response = new WP_REST_Response([
'rows' => $products,
'total' => $query->found_posts,
'page' => $page,
'per_page' => $per_page,
'pages' => $query->max_num_pages,
], 200);
// Prevent caching
$response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
$response->header('Pragma', 'no-cache');
$response->header('Expires', '0');
return $response;
}
/**
* Get single product
*/
public static function get_product(WP_REST_Request $request) {
$id = (int) $request->get_param('id');
$product = wc_get_product($id);
if (!$product) {
return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]);
}
return new WP_REST_Response(self::format_product_full($product), 200);
}
/**
* Create new product
*/
public static function create_product(WP_REST_Request $request) {
try {
$data = $request->get_json_params();
// Validate required fields
if (empty($data['name'])) {
return new WP_Error('missing_name', __('Product name is required', 'woonoow'), ['status' => 400]);
}
// Determine product type
$type = $data['type'] ?? 'simple';
if ($type === 'variable') {
$product = new WC_Product_Variable();
} else {
$product = new WC_Product_Simple();
}
// Set basic data
$product->set_name($data['name']);
if (!empty($data['slug'])) {
$product->set_slug($data['slug']);
}
$product->set_status($data['status'] ?? 'publish');
$product->set_description($data['description'] ?? '');
$product->set_short_description($data['short_description'] ?? '');
if (!empty($data['sku'])) {
$product->set_sku($data['sku']);
}
if (!empty($data['regular_price'])) {
$product->set_regular_price($data['regular_price']);
}
if (!empty($data['sale_price'])) {
$product->set_sale_price($data['sale_price']);
}
$product->set_manage_stock($data['manage_stock'] ?? false);
if (!empty($data['manage_stock'])) {
$product->set_stock_quantity($data['stock_quantity'] ?? 0);
}
$product->set_stock_status($data['stock_status'] ?? 'instock');
// Optional fields
if (!empty($data['weight'])) {
$product->set_weight($data['weight']);
}
if (!empty($data['length'])) {
$product->set_length($data['length']);
}
if (!empty($data['width'])) {
$product->set_width($data['width']);
}
if (!empty($data['height'])) {
$product->set_height($data['height']);
}
// Virtual and downloadable
if (!empty($data['virtual'])) {
$product->set_virtual(true);
}
if (!empty($data['downloadable'])) {
$product->set_downloadable(true);
}
if (!empty($data['featured'])) {
$product->set_featured(true);
}
// Categories
if (!empty($data['categories']) && is_array($data['categories'])) {
$product->set_category_ids($data['categories']);
}
// Tags
if (!empty($data['tags']) && is_array($data['tags'])) {
$product->set_tag_ids($data['tags']);
}
// Images
if (!empty($data['image_id'])) {
$product->set_image_id($data['image_id']);
}
if (!empty($data['gallery_image_ids']) && is_array($data['gallery_image_ids'])) {
$product->set_gallery_image_ids($data['gallery_image_ids']);
}
$product->save();
// Handle variations for variable products
if ($type === 'variable' && !empty($data['attributes']) && is_array($data['attributes'])) {
self::save_product_attributes($product, $data['attributes']);
if (!empty($data['variations']) && is_array($data['variations'])) {
self::save_product_variations($product, $data['variations']);
}
}
return new WP_REST_Response(self::format_product_full($product), 201);
} catch (Exception $e) {
return new WP_Error('create_failed', $e->getMessage(), ['status' => 500]);
}
}
/**
* Update product
*/
public static function update_product(WP_REST_Request $request) {
$id = (int) $request->get_param('id');
$data = $request->get_json_params();
$product = wc_get_product($id);
if (!$product) {
return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]);
}
// Update basic data
if (isset($data['name'])) $product->set_name($data['name']);
if (isset($data['slug'])) $product->set_slug($data['slug']);
if (isset($data['status'])) $product->set_status($data['status']);
if (isset($data['description'])) $product->set_description($data['description']);
if (isset($data['short_description'])) $product->set_short_description($data['short_description']);
if (isset($data['sku'])) $product->set_sku($data['sku']);
if (isset($data['regular_price'])) $product->set_regular_price($data['regular_price']);
if (isset($data['sale_price'])) $product->set_sale_price($data['sale_price']);
if (isset($data['manage_stock'])) {
$product->set_manage_stock($data['manage_stock']);
if ($data['manage_stock']) {
if (isset($data['stock_quantity'])) $product->set_stock_quantity($data['stock_quantity']);
}
}
if (isset($data['stock_status'])) $product->set_stock_status($data['stock_status']);
if (isset($data['weight'])) $product->set_weight($data['weight']);
if (isset($data['length'])) $product->set_length($data['length']);
if (isset($data['width'])) $product->set_width($data['width']);
if (isset($data['height'])) $product->set_height($data['height']);
// Categories
if (isset($data['categories'])) {
$product->set_category_ids($data['categories']);
}
// Tags
if (isset($data['tags'])) {
$product->set_tag_ids($data['tags']);
}
// Images
if (isset($data['image_id'])) {
$product->set_image_id($data['image_id']);
}
if (isset($data['gallery_image_ids'])) {
$product->set_gallery_image_ids($data['gallery_image_ids']);
}
$product->save();
// Handle variations for variable products
if ($product->is_type('variable')) {
if (isset($data['attributes'])) {
self::save_product_attributes($product, $data['attributes']);
}
if (isset($data['variations'])) {
self::save_product_variations($product, $data['variations']);
}
}
return new WP_REST_Response(self::format_product_full($product), 200);
}
/**
* Delete product
*/
public static function delete_product(WP_REST_Request $request) {
$id = (int) $request->get_param('id');
$force = $request->get_param('force') === 'true';
$product = wc_get_product($id);
if (!$product) {
return new WP_Error('product_not_found', __('Product not found', 'woonoow'), ['status' => 404]);
}
$result = $product->delete($force);
if (!$result) {
return new WP_Error('delete_failed', __('Failed to delete product', 'woonoow'), ['status' => 500]);
}
return new WP_REST_Response(['success' => true, 'id' => $id], 200);
}
/**
* Get product categories
*/
public static function get_categories(WP_REST_Request $request) {
$terms = get_terms([
'taxonomy' => 'product_cat',
'hide_empty' => false,
]);
if (is_wp_error($terms)) {
return new WP_REST_Response([], 200); // Return empty array on error
}
$categories = [];
foreach ($terms as $term) {
$categories[] = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'parent' => $term->parent,
'count' => $term->count,
];
}
return new WP_REST_Response($categories, 200);
}
/**
* Get product tags
*/
public static function get_tags(WP_REST_Request $request) {
$terms = get_terms([
'taxonomy' => 'product_tag',
'hide_empty' => false,
]);
if (is_wp_error($terms)) {
return new WP_REST_Response([], 200); // Return empty array on error
}
$tags = [];
foreach ($terms as $term) {
$tags[] = [
'id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'count' => $term->count,
];
}
return new WP_REST_Response($tags, 200);
}
/**
* Get product attributes
*/
public static function get_attributes(WP_REST_Request $request) {
$attributes = wc_get_attribute_taxonomies();
$result = [];
foreach ($attributes as $attribute) {
$result[] = [
'id' => $attribute->attribute_id,
'name' => $attribute->attribute_name,
'label' => $attribute->attribute_label,
'type' => $attribute->attribute_type,
'orderby' => $attribute->attribute_orderby,
];
}
return new WP_REST_Response($result, 200);
}
/**
* Format product for list view
* Returns all essential product fields including type, status, prices, stock, etc.
*/
private static function format_product_list_item($product) {
$image = wp_get_attachment_image_src($product->get_image_id(), 'thumbnail');
// Get price HTML - for variable products, show price range
$price_html = $product->get_price_html();
if (empty($price_html) && $product->is_type('variable')) {
$prices = $product->get_variation_prices(true);
if (!empty($prices['price'])) {
$min_price = min($prices['price']);
$max_price = max($prices['price']);
if ($min_price !== $max_price) {
$price_html = wc_format_price_range($min_price, $max_price);
} else {
$price_html = wc_price($min_price);
}
}
}
return [
'id' => $product->get_id(),
'name' => $product->get_name(),
'sku' => $product->get_sku(),
'type' => $product->get_type(),
'status' => $product->get_status(),
'price' => $product->get_price(),
'regular_price' => $product->get_regular_price(),
'sale_price' => $product->get_sale_price(),
'price_html' => $price_html,
'stock_status' => $product->get_stock_status(),
'stock_quantity' => $product->get_stock_quantity(),
'manage_stock' => $product->get_manage_stock(),
'image_url' => $image ? $image[0] : '',
'permalink' => get_permalink($product->get_id()),
'date_created' => $product->get_date_created() ? $product->get_date_created()->date('Y-m-d H:i:s') : '',
'date_modified' => $product->get_date_modified() ? $product->get_date_modified()->date('Y-m-d H:i:s') : '',
];
}
/**
* Format product with full details
*/
private static function format_product_full($product) {
$data = self::format_product_list_item($product);
// Add full details
$data['description'] = $product->get_description();
$data['short_description'] = $product->get_short_description();
$data['slug'] = $product->get_slug();
$data['weight'] = $product->get_weight();
$data['length'] = $product->get_length();
$data['width'] = $product->get_width();
$data['height'] = $product->get_height();
$data['categories'] = $product->get_category_ids();
$data['tags'] = $product->get_tag_ids();
$data['image_id'] = $product->get_image_id();
$data['gallery_image_ids'] = $product->get_gallery_image_ids();
$data['virtual'] = $product->is_virtual();
$data['downloadable'] = $product->is_downloadable();
$data['featured'] = $product->is_featured();
// Gallery images
$gallery = [];
foreach ($product->get_gallery_image_ids() as $image_id) {
$image = wp_get_attachment_image_src($image_id, 'full');
if ($image) {
$gallery[] = [
'id' => $image_id,
'url' => $image[0],
'width' => $image[1],
'height' => $image[2],
];
}
}
$data['gallery'] = $gallery;
// Variable product specifics
if ($product->is_type('variable')) {
$data['attributes'] = self::get_product_attributes($product);
$data['variations'] = self::get_product_variations($product);
}
return $data;
}
/**
* Get product attributes
*/
private static function get_product_attributes($product) {
$attributes = [];
foreach ($product->get_attributes() as $attribute) {
$attributes[] = [
'id' => $attribute->get_id(),
'name' => $attribute->get_name(),
'options' => $attribute->get_options(),
'position' => $attribute->get_position(),
'visible' => $attribute->get_visible(),
'variation' => $attribute->get_variation(),
];
}
return $attributes;
}
/**
* Get product variations
*/
private static function get_product_variations($product) {
$variations = [];
foreach ($product->get_children() as $variation_id) {
$variation = wc_get_product($variation_id);
if ($variation) {
$image = wp_get_attachment_image_src($variation->get_image_id(), 'thumbnail');
// Format attributes with human-readable names and values
$formatted_attributes = [];
$variation_attributes = $variation->get_attributes();
// Debug: Log raw variation attributes
error_log('WooNooW Debug - Variation #' . $variation_id . ' raw attributes: ' . print_r($variation_attributes, true));
foreach ($variation_attributes as $attr_name => $attr_value) {
// Handle taxonomy attributes (pa_*)
if (strpos($attr_name, 'pa_') === 0) {
// Global attribute
$taxonomy = $attr_name;
$clean_name = wc_attribute_label($taxonomy);
if (!empty($attr_value)) {
$term = get_term_by('slug', $attr_value, $taxonomy);
$attr_value = $term ? $term->name : $attr_value;
}
} else {
// Custom attribute
$clean_name = ucfirst(str_replace('_', ' ', $attr_name));
// For custom attributes, the value is already the display value
}
$formatted_attributes[$clean_name] = $attr_value;
}
$variations[] = [
'id' => $variation->get_id(),
'sku' => $variation->get_sku(),
'price' => $variation->get_price(),
'regular_price' => $variation->get_regular_price(),
'sale_price' => $variation->get_sale_price(),
'stock_status' => $variation->get_stock_status(),
'stock_quantity' => $variation->get_stock_quantity(),
'manage_stock' => $variation->get_manage_stock(),
'attributes' => $formatted_attributes,
'image_id' => $variation->get_image_id(),
'image_url' => $image ? $image[0] : '',
];
}
}
return $variations;
}
/**
* Save product attributes
*/
private static function save_product_attributes($product, $attributes_data) {
$attributes = [];
foreach ($attributes_data as $attr_data) {
$attribute = new \WC_Product_Attribute();
$attribute->set_name($attr_data['name']);
$attribute->set_options($attr_data['options']);
$attribute->set_position($attr_data['position'] ?? 0);
$attribute->set_visible($attr_data['visible'] ?? true);
$attribute->set_variation($attr_data['variation'] ?? true);
$attributes[] = $attribute;
}
$product->set_attributes($attributes);
$product->save();
}
/**
* Save product variations
*/
private static function save_product_variations($product, $variations_data) {
foreach ($variations_data as $var_data) {
if (isset($var_data['id']) && $var_data['id']) {
// Update existing variation
$variation = wc_get_product($var_data['id']);
} else {
// Create new variation
$variation = new WC_Product_Variation();
$variation->set_parent_id($product->get_id());
}
if ($variation) {
if (isset($var_data['sku'])) $variation->set_sku($var_data['sku']);
if (isset($var_data['regular_price'])) $variation->set_regular_price($var_data['regular_price']);
if (isset($var_data['sale_price'])) $variation->set_sale_price($var_data['sale_price']);
if (isset($var_data['stock_status'])) $variation->set_stock_status($var_data['stock_status']);
if (isset($var_data['manage_stock'])) $variation->set_manage_stock($var_data['manage_stock']);
if (isset($var_data['stock_quantity'])) $variation->set_stock_quantity($var_data['stock_quantity']);
if (isset($var_data['attributes'])) $variation->set_attributes($var_data['attributes']);
if (isset($var_data['image_id'])) $variation->set_image_id($var_data['image_id']);
$variation->save();
}
}
}
}