get_per_page($request); $page = $this->get_page($request); $debug = $this->is_debug_enabled($request); $this->register_export_hooks(); $tax_query = $this->build_tax_query($request); if (is_wp_error($tax_query)) { return $tax_query; } $query_args = [ 'posts_per_page' => $per_page, 'paged' => $page, 'post_status' => $this->get_status($request), 'orderby' => 'ID', 'order' => 'DESC', 'post_type' => 'product', ]; $exclude = $this->get_id_list_param($request, 'exclude'); if (!empty($exclude)) { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- Optional client-driven product exclusion for catalog sync. $query_args['post__not_in'] = $exclude; } if (!empty($tax_query)) { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query -- Attribute filtering is a catalog sync feature. $query_args['tax_query'] = $tax_query; } $query = new WP_Query($query_args); $product_posts = $query->get_posts(); $data = [ 'count' => (int) $query->found_posts, 'max_pages' => (int) $query->max_num_pages, 'results' => [], ]; if (empty($product_posts)) { if ($debug) { $this->append_debug_data($data); } return new WP_REST_Response($data, 200); } if ($this->is_summery_enabled($request)) { foreach ($product_posts as $post) { $data['results'][] = $this->prepare_summery_item($post); } if ($debug) { $this->append_debug_data($data); } return new WP_REST_Response($data, 200); } $product_ids = wp_list_pluck($product_posts, 'ID'); update_post_caches($product_posts, 'product', true, false); $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, ]); $variation_ids = wp_list_pluck($variation_posts, 'ID'); $all_post_ids = array_merge($product_ids, $variation_ids); update_post_caches($variation_posts, 'product_variation', true, false); update_meta_cache('post', $all_post_ids); $products = []; foreach ($product_posts as $post) { $products[] = wc_get_product($post); } $variations = []; foreach ($variation_posts as $post) { $variations[] = wc_get_product($post); } $instances = array_merge($products, $variations); $this->warm_meta_cache($instances); $all_terms = $this->warm_taxonomy_caches($instances, $product_ids, $variation_ids, $products); $this->warm_attachment_cache($instances); $products_controller = new Twokcs_REST_Products_Controller(); $variations_controller = new Twokcs_REST_Variations_Controller($all_terms); foreach ($products as $product) { $product_data = $products_controller->prepare_object_for_response($product, $request)->get_data(); unset($product_data['brands']); $product_data['variations'] = []; foreach ($variations as $variation) { if ($variation->get_parent_id() === $product->get_id()) { $product_data['variations'][] = $variations_controller ->prepare_object_for_response($variation, $request) ->get_data(); } } $data['results'][] = $product_data; } if ($debug) { $this->append_debug_data($data); } return new WP_REST_Response($data, 200); } /** * 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); } /** * @param WP_Post $post Product post. * @return array{id: int, name: string, slug: string} */ private function prepare_summery_item($post) { return [ 'id' => (int) $post->ID, 'name' => $post->post_title, 'slug' => $post->post_name, ]; } /** * @param WP_REST_Request $request Request object. * @return bool */ private function is_summery_enabled(WP_REST_Request $request) { return (bool) $request->get_param('summery'); } /** * @param WP_REST_Request $request Request object. * @return string */ private function get_status(WP_REST_Request $request) { $status = $request->get_param('status'); if (empty($status)) { return 'publish'; } return sanitize_key($status); } /** * 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; })); } /** * Build tax_query clauses from attribute and attribute_term filters. * * @param WP_REST_Request $request Request object. * @return array[]|WP_Error */ private function build_tax_query(WP_REST_Request $request) { $attribute_slug = $request->get_param('attribute'); $term_ids = $this->get_id_list_param($request, 'attribute_term'); if (empty($attribute_slug) && empty($term_ids)) { return []; } if (!empty($attribute_slug) && !empty($term_ids)) { $taxonomy = $this->resolve_attribute_taxonomy($attribute_slug); if (is_wp_error($taxonomy)) { return $taxonomy; } return [ [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $term_ids, ], ]; } if (!empty($attribute_slug)) { $taxonomy = $this->resolve_attribute_taxonomy($attribute_slug); if (is_wp_error($taxonomy)) { return $taxonomy; } return [ [ 'taxonomy' => $taxonomy, 'operator' => 'EXISTS', ], ]; } $terms = get_terms([ 'include' => $term_ids, 'hide_empty' => false, ]); if (is_wp_error($terms)) { return $terms; } if (empty($terms)) { return new WP_Error( 'twokcs_invalid_attribute_term', __('Invalid attribute term ID.', '2khat-catalog-sync'), ['status' => 400] ); } $terms_by_taxonomy = []; foreach ($terms as $term) { $terms_by_taxonomy[$term->taxonomy][] = (int) $term->term_id; } $clauses = []; foreach ($terms_by_taxonomy as $taxonomy => $taxonomy_term_ids) { $clauses[] = [ 'taxonomy' => $taxonomy, 'field' => 'term_id', 'terms' => $taxonomy_term_ids, ]; } if (count($clauses) > 1) { $clauses['relation'] = 'OR'; } return $clauses; } /** * Resolve an attribute slug to its taxonomy name. * * @param string $slug Attribute slug (with or without pa_ prefix). * @return string|WP_Error */ private function resolve_attribute_taxonomy($slug) { $slug = sanitize_title($slug); if (strpos($slug, 'pa_') === 0) { $slug = substr($slug, 3); } $taxonomy = wc_attribute_taxonomy_name($slug); if (!taxonomy_exists($taxonomy)) { return new WP_Error( 'twokcs_invalid_attribute', __('Invalid attribute slug.', '2khat-catalog-sync'), ['status' => 400] ); } return $taxonomy; } /** * @param WP_REST_Request $request Request object. * @return int */ private function get_per_page(WP_REST_Request $request) { $per_page = (int) $request->get_param('per_page'); if ($per_page < 1) { $per_page = Twokcs_Constants::DEFAULT_PER_PAGE; } return min($per_page, Twokcs_Constants::MAX_PER_PAGE); } /** * @param WP_REST_Request $request Request object. * @return int */ private function get_page(WP_REST_Request $request) { $page = (int) $request->get_param('page'); return max($page, 1); } /** * @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; } } /** * Batch-load and cache product meta. * * @param WC_Product[] $instances Product and variation objects. */ private function warm_meta_cache(array $instances) { $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'); } } /** * Batch-load taxonomy terms and warm WooCommerce term caches. * * @param WC_Product[] $instances All product and variation objects. * @param int[] $product_ids Parent product IDs. * @param int[] $variation_ids Variation IDs. * @param WC_Product[] $products Parent products only. * @return WP_Term[] */ private function warm_taxonomy_caches(array $instances, array $product_ids, array $variation_ids, array $products) { $taxonomies = []; foreach ($instances as $instance) { foreach ($instance->get_attributes() as $attribute) { if (!empty($attribute['is_taxonomy'])) { $taxonomies[$attribute['name']] = 1; } } } $taxonomy_names = array_keys($taxonomies); $all_post_ids = array_merge($product_ids, $variation_ids); if (!empty($taxonomy_names)) { update_object_term_cache($all_post_ids, 'product'); } $all_terms = !empty($taxonomy_names) ? wp_get_object_terms($all_post_ids, $taxonomy_names) : []; if (is_wp_error($all_terms)) { $all_terms = []; } foreach ($products as $instance) { foreach ($instance->get_attributes() as $attribute) { if (empty($attribute['is_taxonomy'])) { continue; } $cache_key = 'wc_' . $attribute['name'] . md5(wp_json_encode(['fields' => 'names'])); $cache_group = WC_Cache_Helper::get_cache_prefix('product_' . $instance->get_id()) . $instance->get_id(); $terms = []; foreach ($attribute['options'] as $option) { foreach ($all_terms as $term) { if ($term->taxonomy === $attribute['name'] && (int) $term->term_id === (int) $option) { $terms[] = $term->name; } } } wp_cache_add($cache_key, $terms, $cache_group); } } return $all_terms; } /** * Preload attachment posts for product images. * * @param WC_Product[] $instances Product and variation objects. */ private function warm_attachment_cache(array $instances) { $attachment_ids = []; foreach ($instances as $instance) { if ($instance->get_image_id()) { $attachment_ids[] = $instance->get_image_id(); } $attachment_ids = array_merge($attachment_ids, $instance->get_gallery_image_ids()); } $attachment_ids = array_values(array_unique(array_filter($attachment_ids))); if (empty($attachment_ids)) { return; } $attachment_posts = get_posts([ 'posts_per_page' => -1, 'post_type' => 'attachment', 'post__in' => $attachment_ids, ]); if (!empty($attachment_posts)) { update_post_caches($attachment_posts, 'attachment', true, true); } } }