$settings widget settings * @param array $env { page, on_sale_ids, visibility_ids: {exclude-from-catalog, featured, outofstock}, hide_out_of_stock } */ public static function args(array $settings, array $env = []): array { $source = in_array($settings['source'] ?? '', self::SOURCES, true) ? (string) $settings['source'] : 'latest'; $columns = self::clamp($settings['columns'] ?? 4, 1, 8, 4); $rows = self::clamp($settings['rows'] ?? 1, 1, 20, 1); $per_page = $columns * $rows; $order = strtoupper((string) ($settings['order'] ?? 'DESC')) === 'ASC' ? 'ASC' : 'DESC'; $orderby = in_array($settings['orderby'] ?? '', self::ORDERBY, true) ? (string) $settings['orderby'] : 'date'; $page = max(1, (int) ($env['page'] ?? 1)); $vis = is_array($env['visibility_ids'] ?? null) ? $env['visibility_ids'] : []; $args = [ 'post_type' => 'product', 'post_status' => 'publish', 'ignore_sticky_posts' => true, 'posts_per_page' => $per_page, 'paged' => !empty($settings['pagination']) ? $page : 1, 'no_found_rows' => empty($settings['pagination']), // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_tax_query, WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- product visibility / category filters, as WooCommerce itself queries them. 'tax_query' => [], 'meta_query' => [], // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_tax_query, WordPress.DB.SlowDBQuery.slow_db_query_meta_query ]; $hidden = array_filter([(int) ($vis['exclude-from-catalog'] ?? 0)]); if (!empty($env['hide_out_of_stock']) && !empty($vis['outofstock'])) { $hidden[] = (int) $vis['outofstock']; } if ($hidden) { $args['tax_query'][] = ['taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => array_values($hidden), 'operator' => 'NOT IN']; } switch ($source) { case 'sale': $ids = array_map('intval', (array) ($env['on_sale_ids'] ?? [])); $args['post__in'] = $ids ?: [0]; break; case 'featured': $args['tax_query'][] = ['taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => [(int) ($vis['featured'] ?? 0)], 'operator' => 'IN']; break; case 'category': case 'tag': $terms = self::ids($settings[$source === 'category' ? 'categories' : 'tags'] ?? []); if ($terms) { $args['tax_query'][] = ['taxonomy' => $source === 'category' ? 'product_cat' : 'product_tag', 'field' => 'term_id', 'terms' => $terms, 'operator' => 'IN']; } break; case 'ids': $ids = self::ids($settings['ids'] ?? ''); $args['post__in'] = $ids ?: [0]; if ($orderby === 'menu_order' || empty($settings['orderby'])) { $args['orderby'] = 'post__in'; } break; case 'best_selling': $orderby = 'popularity'; $order = 'DESC'; break; case 'top_rated': $orderby = 'rating'; $order = 'DESC'; break; } $excluded = self::ids($settings['exclude'] ?? ''); if ($excluded) { // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_post__not_in -- user-chosen product exclusions, a short id list. $args['post__not_in'] = $excluded; } if (!isset($args['orderby'])) { $args = array_merge($args, self::ordering($orderby, $order)); } if (!$args['tax_query']) { unset($args['tax_query']); } elseif (count($args['tax_query']) > 1) { $args['tax_query']['relation'] = 'AND'; } if (!$args['meta_query']) { unset($args['meta_query']); } return (array) apply_filters('zadex/woocommerce/products_query', $args, $settings); } /** orderby / order (+ meta_key) for a WooCommerce ordering. */ public static function ordering(string $orderby, string $order): array { switch ($orderby) { case 'price': // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- WooCommerce price ordering needs its meta key. return ['orderby' => 'meta_value_num', 'meta_key' => '_price', 'order' => $order]; case 'popularity': // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- WooCommerce popularity ordering needs its meta key. return ['orderby' => ['meta_value_num' => 'DESC', 'ID' => 'DESC'], 'meta_key' => 'total_sales', 'order' => 'DESC']; case 'rating': // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- WooCommerce rating ordering needs its meta key. return ['orderby' => ['meta_value_num' => 'DESC', 'ID' => 'DESC'], 'meta_key' => '_wc_average_rating', 'order' => 'DESC']; case 'rand': return ['orderby' => 'rand', 'order' => $order]; case 'title': return ['orderby' => 'title', 'order' => $order]; case 'menu_order': return ['orderby' => 'menu_order title', 'order' => $order]; } return ['orderby' => 'date ID', 'order' => $order]; } /** Environment from WooCommerce (current page, sale ids, visibility term ids). */ public static function env(): array { // Public pagination link of the Products widget: read-only page number, no nonce. $page = isset($_GET[self::PAGE_VAR]) ? absint(wp_unslash($_GET[self::PAGE_VAR])) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return [ 'page' => max(1, $page), 'on_sale_ids' => function_exists('wc_get_product_ids_on_sale') ? wc_get_product_ids_on_sale() : [], 'visibility_ids' => function_exists('wc_get_product_visibility_term_ids') ? wc_get_product_visibility_term_ids() : [], 'hide_out_of_stock' => get_option('woocommerce_hide_out_of_stock_items') === 'yes', ]; } /** @return int[] from "1, 2,3", [1, "2"] or a term-id select value */ public static function ids($value): array { if (is_string($value) || is_int($value)) { $value = preg_split('/[\s,]+/', (string) $value) ?: []; } if (!is_array($value)) { return []; } return array_values(array_unique(array_filter(array_map('intval', $value), static fn(int $i): bool => $i > 0))); } private static function clamp($value, int $min, int $max, int $default): int { if (is_array($value)) { $value = $value['size'] ?? $default; } $n = is_numeric($value) ? (int) $value : $default; return max($min, min($max, $n)); } }