Files
WooNooW/includes/Api/ProductsController.php
dwindown c397639176 debug: Log all variation meta to find correct attribute storage key
Added logging to see ALL meta keys and values for variations.
This will show us exactly how WooCommerce stores the attribute values.

Check debug.log for:
Variation #362 ALL META: Array(...)

This will reveal the actual meta key format.
2025-11-20 01:02:14 +07:00

712 lines
21 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() {
error_log('WooNooW ProductsController::register_routes() START');
// List products
$callback = [__CLASS__, 'get_products'];
$is_callable = is_callable($callback);
error_log('WooNooW ProductsController: Callback is_callable: ' . ($is_callable ? 'YES' : 'NO'));
$result = register_rest_route('woonoow/v1', '/products', [
'methods' => 'GET',
'callback' => $callback,
'permission_callback' => [Permissions::class, 'check_admin_permission'],
]);
error_log('WooNooW ProductsController: GET /products registered: ' . ($result ? 'SUCCESS' : 'FAILED'));
// 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) {
error_log('WooNooW ProductsController::get_products() CALLED - START');
try {
$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,
'_debug' => 'ProductsController-v2-' . time(), // Verify this code is running
], 200);
// Prevent caching
$response->header('Cache-Control', 'no-cache, no-store, must-revalidate');
$response->header('Pragma', 'no-cache');
$response->header('Expires', '0');
$response->header('X-WooNooW-Version', '2.0'); // Debug header
error_log('WooNooW ProductsController::get_products() CALLED - END SUCCESS');
return $response;
} catch (\Exception $e) {
error_log('WooNooW ProductsController::get_products() ERROR: ' . $e->getMessage());
error_log('WooNooW ProductsController::get_products() TRACE: ' . $e->getTraceAsString());
return new WP_Error('products_error', $e->getMessage(), ['status' => 500]);
}
}
/**
* 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 = [];
// Debug: Log all meta for this variation
$all_meta = get_post_meta($variation_id);
error_log("Variation #{$variation_id} ALL META: " . print_r($all_meta, true));
// Get parent product attributes to know what to look for
$parent_attributes = $product->get_attributes();
foreach ($parent_attributes as $parent_attr) {
if (!$parent_attr->get_variation()) {
continue; // Skip non-variation attributes
}
$attr_name = $parent_attr->get_name();
$clean_name = $attr_name;
// Get the variation's value for this attribute
if (strpos($attr_name, 'pa_') === 0) {
// Global/taxonomy attribute
$clean_name = wc_attribute_label($attr_name);
$value = $variation->get_attribute($attr_name);
// Convert slug to term name
if (!empty($value)) {
$term = get_term_by('slug', $value, $attr_name);
$value = $term ? $term->name : $value;
}
} else {
// Custom attribute - get the value from variation meta
// WooCommerce stores it as 'attribute_' + lowercase attribute name
$meta_key = 'attribute_' . strtolower($attr_name);
$value = get_post_meta($variation_id, $meta_key, true);
// If not found, try with sanitized title
if (empty($value)) {
$meta_key = 'attribute_' . sanitize_title($attr_name);
$value = get_post_meta($variation_id, $meta_key, true);
}
// Capitalize the attribute name for display
$clean_name = ucfirst($attr_name);
}
$formatted_attributes[$clean_name] = $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();
}
}
}
}