get_id_list_param($request, 'product_ids'); $ids = $this->get_id_list_param($request, 'ids'); $has_product_ids = !empty($product_ids); $has_ids = !empty($ids); if ($has_product_ids && $has_ids) { return new WP_Error( 'twokcs_ambiguous_variants_request', __('Provide either product_ids or ids, not both.', '2khat-catalog-sync'), ['status' => 400] ); } if (!$has_product_ids && !$has_ids) { return new WP_Error( 'twokcs_missing_variants_filter', __('Provide product_ids or ids query parameter.', '2khat-catalog-sync'), ['status' => 400] ); } if ($has_product_ids) { return $this->export_by_product_ids($request, $product_ids); } return $this->export_by_ids($request, $ids); } /** * Export a single variant by ID. * * @param WP_REST_Request $request Request object. * @return WP_REST_Response|WP_Error */ public function export_by_id(WP_REST_Request $request) { $id = (int) $request->get_param('id'); if ($id < 1) { return new WP_Error( 'twokcs_missing_variation_id', __('Variation ID is required.', '2khat-catalog-sync'), ['status' => 400] ); } $this->register_export_hooks(); $variation = wc_get_product($id); if (!$variation || !$variation->is_type('variation') || $variation->get_status() !== 'publish') { return new WP_Error( 'twokcs_variation_not_found', __('Variation not found.', '2khat-catalog-sync'), ['status' => 404] ); } $this->warm_meta_cache([$variation]); $data = $this->prepare_compact_variation($variation, $request); return new WP_REST_Response($data, 200); } /** * Export variants grouped by parent product IDs. * * @param WP_REST_Request $request Request object. * @param int[] $product_ids Parent product IDs. * @return WP_REST_Response */ private function export_by_product_ids(WP_REST_Request $request, array $product_ids) { $this->register_export_hooks(); $products_by_id = $this->load_products_by_ids($product_ids); $variation_posts = get_posts([ 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'menu_order', 'order' => 'ASC', 'post_type' => 'product_variation', 'post_parent__in' => $product_ids, ]); $variations_by_parent = array_fill_keys($product_ids, []); if (!empty($variation_posts)) { $variations = $this->load_variations_from_posts($variation_posts); foreach ($variations as $variation) { $parent_id = (int) $variation->get_parent_id(); if (isset($variations_by_parent[$parent_id])) { $variations_by_parent[$parent_id][] = $this->prepare_compact_variation($variation, $request); } } } $results = []; foreach ($product_ids as $product_id) { if (isset($products_by_id[$product_id])) { $item = $this->prepare_compact_product($products_by_id[$product_id], $request); } else { $item = $this->empty_compact_product($product_id); } $item['variants'] = $variations_by_parent[$product_id]; $results[] = $item; } $data = [ 'count' => count($results), 'results' => $results, ]; if ($this->is_debug_enabled($request)) { $this->append_debug_data($data); } return new WP_REST_Response($data, 200); } /** * Export variants by variation IDs. * * @param WP_REST_Request $request Request object. * @param int[] $ids Variation IDs. * @return WP_REST_Response */ private function export_by_ids(WP_REST_Request $request, array $ids) { $this->register_export_hooks(); $variation_posts = get_posts([ 'posts_per_page' => -1, 'post_status' => 'publish', 'post_type' => 'product_variation', 'post__in' => $ids, 'orderby' => 'post__in', ]); $variations_by_id = []; if (!empty($variation_posts)) { $variations = $this->load_variations_from_posts($variation_posts); foreach ($variations as $variation) { $variations_by_id[$variation->get_id()] = $variation; } } $results = []; foreach ($ids as $id) { if (!isset($variations_by_id[$id])) { continue; } $results[] = $this->prepare_compact_variation($variations_by_id[$id], $request); } $data = [ 'count' => count($results), 'results' => $results, ]; if ($this->is_debug_enabled($request)) { $this->append_debug_data($data); } return new WP_REST_Response($data, 200); } /** * Load parent products keyed by ID and warm caches. * * @param int[] $product_ids Product IDs. * @return array */ private function load_products_by_ids(array $product_ids) { $product_posts = get_posts([ 'posts_per_page' => -1, 'post_status' => 'any', 'post_type' => 'product', 'post__in' => $product_ids, 'orderby' => 'post__in', ]); if (empty($product_posts)) { return []; } $loaded_ids = wp_list_pluck($product_posts, 'ID'); update_post_caches($product_posts, 'product', true, false); update_meta_cache('post', $loaded_ids); $products_by_id = []; foreach ($product_posts as $post) { $product = wc_get_product($post); if ($product && !$product->is_type('variation')) { $products_by_id[$product->get_id()] = $product; } } $this->warm_meta_cache(array_values($products_by_id)); return $products_by_id; } /** * Load variation objects from posts and warm caches. * * @param WP_Post[] $variation_posts Variation posts. * @return WC_Product_Variation[] */ private function load_variations_from_posts(array $variation_posts) { $variation_ids = wp_list_pluck($variation_posts, 'ID'); update_post_caches($variation_posts, 'product_variation', true, false); update_meta_cache('post', $variation_ids); $variations = []; foreach ($variation_posts as $post) { $variation = wc_get_product($post); if ($variation && $variation->is_type('variation')) { $variations[] = $variation; } } $this->warm_meta_cache($variations); return $variations; } /** * Build compact parent-product payload with discount-rule pricing applied. * * @param WC_Product $product Product object. * @param WP_REST_Request $request Request object. * @return array */ private function prepare_compact_product($product, WP_REST_Request $request) { $data = [ 'product_id' => $product->get_id(), 'type' => $product->get_type(), 'manage_stock' => $product->managing_stock(), 'price' => $product->get_price(), 'regular_price' => $product->get_regular_price(), 'sale_price' => $product->get_sale_price(), 'stock_quantity' => $product->get_stock_quantity(), 'stock_status' => $product->get_stock_status(), ]; $response = rest_ensure_response($data); $response = apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce core filter. 'woocommerce_rest_prepare_product_object', $response, $product, $request ); return $response->get_data(); } /** * Placeholder group when a requested product ID is missing. * * @param int $product_id Product ID. * @return array */ private function empty_compact_product($product_id) { return [ 'product_id' => $product_id, 'type' => null, 'manage_stock' => null, 'price' => '', 'regular_price' => '', 'sale_price' => '', 'stock_quantity' => null, 'stock_status' => '', ]; } /** * Build compact variation payload with discount-rule pricing applied. * * @param WC_Product_Variation $variation Variation object. * @param WP_REST_Request $request Request object. * @return array */ private function prepare_compact_variation($variation, WP_REST_Request $request) { $data = [ 'id' => $variation->get_id(), 'on_sale' => $variation->is_on_sale(), 'status' => $variation->get_status(), 'price' => $variation->get_price(), 'regular_price' => $variation->get_regular_price(), 'sale_price' => $variation->get_sale_price(), 'stock_quantity' => $variation->get_stock_quantity(), 'stock_status' => $variation->get_stock_status(), ]; $response = rest_ensure_response($data); $response = apply_filters( // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WooCommerce core filter. 'woocommerce_rest_prepare_product_variation_object', $response, $variation, $request ); return $response->get_data(); } /** * Register temporary hooks for export performance and compatibility. */ private function register_export_hooks() { add_filter('woocommerce_data_stores', function ($stores) { $stores['product'] = 'Twokcs_Product_Data_Store'; $stores['product-variation'] = 'Twokcs_Variation_Data_Store'; return $stores; }); add_action('pre_get_posts', function ($query) { if (!empty($query->query_vars['woodmart_single_variations_filter'])) { unset($query->query_vars['woodmart_single_variations_filter']); $query->set('post_type', 'product'); } }, PHP_INT_MAX); } /** * Parse a comma-separated or array request param into a list of positive integers. * * @param WP_REST_Request $request Request object. * @param string $name Parameter name. * @return int[] */ private function get_id_list_param(WP_REST_Request $request, $name) { $value = $request->get_param($name); if (empty($value)) { return []; } if (!is_array($value)) { $value = explode(',', (string) $value); } $ids = array_map('intval', $value); return array_values(array_filter($ids, function ($id) { return $id > 0; })); } /** * Batch-load and cache variation meta. * * @param WC_Product[] $instances Product or variation objects. */ private function warm_meta_cache(array $instances) { if (empty($instances)) { return; } $data_store = new Twokcs_Data_Store(); $metas = $data_store->read_metas($instances); foreach ($instances as $i => $instance) { wp_cache_set($instance->get_meta_cache_key(), $metas[$i], 'products'); } } /** * @param WP_REST_Request $request Request object. * @return bool */ private function is_debug_enabled(WP_REST_Request $request) { if (!(bool) $request->get_param('debug')) { return false; } if (apply_filters('twokcs_allow_debug', false)) { return true; } return defined('WP_DEBUG') && WP_DEBUG && defined('TWOKCS_DEBUG') && TWOKCS_DEBUG; } /** * Append read-only SQL debug stats when debug mode is enabled. * * @param array $data Response data array. */ private function append_debug_data(array &$data) { global $wpdb; $data['debug'] = ['num_queries' => $wpdb->num_queries]; if (defined('SAVEQUERIES') && SAVEQUERIES && !empty($wpdb->queries)) { $data['debug']['queries'] = $wpdb->queries; } } }