=> 'all' === $this->attributes['catOperator'] ? false : true, ); } } /** * Set visibility query args. * * @param array $query_args Query args. */ protected function set_visibility_query_args( &$query_args ) { $product_visibility_terms = wc_get_product_visibility_term_ids(); $product_visibility_not_in = array( $product_visibility_terms['exclude-from-catalog'] ); if ( 'yes' === get_option( 'woocommerce_hide_out_of_stock_items' ) ) { $product_visibility_not_in[] = $product_visibility_terms['outofstock']; } $query_args['tax_query'][] = array( 'taxonomy' => 'product_visibility', 'field' => 'term_taxonomy_id', 'terms' => $product_visibility_not_in, 'operator' => 'NOT IN', ); } /** * Set which stock status to use when displaying products. * * @param array $query_args Query args. * @return void */ protected function set_stock_status_query_args( &$query_args ) { $stock_statuses = array_keys( wc_get_product_stock_status_options() ); // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query if ( isset( $this->attributes['stockStatus'] ) && $stock_statuses !== $this->attributes['stockStatus'] ) { // Reset meta_query then update with our stock status. $query_args['meta_query'] = $this->meta_query; $query_args['meta_query'][] = array( 'key' => '_stock_status', 'value' => array_merge( [ '' ], $this->attributes['stockStatus'] ), 'compare' => 'IN', ); } else { $query_args['meta_query'] = $this->meta_query; } // phpcs:enable WordPress.DB.SlowDBQuery.slow_db_query_meta_query } /** * Works out the item limit based on rows and columns, or returns default. * * @return int */ protected function get_products_limit() { if ( isset( $this->attributes['rows'], $this->attributes['columns'] ) && ! empty( $this->attributes['rows'] ) ) { $this->attributes['limit'] = intval( $this->attributes['columns'] ) * intval( $this->attributes['rows'] ); } return intval( $this->attributes['limit'] ); } /** * Run the query and return an array of product IDs * * @return array List of product IDs */ protected function get_products() { /** * Filters whether or not the product grid is cacheable. * * @param boolean $is_cacheable The list of script dependencies. * @param array $query_args Query args for the products query passed to BlocksWpQuery. * @return array True to enable cache, false to disable cache. * * @since 2.5.0 */ $is_cacheable = (bool) apply_filters( 'woocommerce_blocks_product_grid_is_cacheable', true, $this->query_args ); $transient_version = \WC_Cache_Helper::get_transient_version( 'product_query' ); $query = new BlocksWpQuery( $this->query_args ); $results = wp_parse_id_list( $is_cacheable ? $query->get_cached_posts( $transient_version ) : $query->get_posts() ); // Remove ordering query arguments which may have been added by get_catalog_ordering_args. WC()->query->remove_ordering_args(); // Prime caches to reduce future queries. Note _prime_post_caches is private--we could replace this with our own // query if it becomes unavailable. if ( is_callable( '_prime_post_caches' ) ) { _prime_post_caches( $results ); } $this->prime_product_variations( $results ); return $results; } /** * Retrieve IDs that are not already present in the cache. * * Based on WordPress function: _get_non_cached_ids * * @param int[] $product_ids Array of IDs. * @param string $cache_key The cache bucket to check against. * @return int[] Array of IDs not present in the cache. */ protected function get_non_cached_ids( $product_ids, $cache_key ) { $non_cached_ids = array(); $cache_values = wp_cache_get_multiple( $product_ids, $cache_key ); foreach ( $cache_values as $id => $value ) { if ( ! $value ) { $non_cached_ids[] = (int) $id; } } return $non_cached_ids; } /** * Prime query cache of product variation meta data. * * Prepares values in the product_ID_variation_meta_data cache for later use in the ProductSchema::get_variations() * method. Doing so here reduces the total number of queries needed. * * @param int[] $product_ids Product ids to prime variation cache for. */ protected function prime_product_variations( $product_ids ) { $cache_group = 'product_variation_meta_data'; $prime_product_ids = $this->get_non_cached_ids( wp_parse_id_list( $product_ids ), $cache_group ); if ( ! $prime_product_ids ) { return; } global $wpdb; // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared $product_variations = $wpdb->get_results( "SELECT ID as variation_id, post_parent as product_id from {$wpdb->posts} WHERE post_parent IN ( " . implode( ',', $prime_product_ids ) . ' )', ARRAY_A ); $prime_variation_ids = array_column( $product_variations, 'variation_id' ); $variation_ids_by_parent = array_column( $product_variations, 'product_id', 'variation_id' ); if ( empty( $prime_variation_ids ) ) { return; } $all_variation_meta_data = $wpdb->get_results( $wpdb->prepare( "SELECT post_id as variation_id, meta_key as attribute_key, meta_value as attribute_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ',', array_map( 'esc_sql', $prime_variation_ids ) ) . ') AND meta_key LIKE %s', $wpdb->esc_like( 'attribute_' ) . '%' ) ); // phpcs:enable // Prepare the data to cache by indexing by the parent product. $primed_data = array_reduce( $all_variation_meta_data, function( $values, $data ) use ( $variation_ids_by_parent ) { $values[ $variation_ids_by_parent[ $data->variation_id ] ?? 0 ][] = $data; return $values; }, array_fill_keys( $prime_product_ids, [] ) ); // Cache everything. foreach ( $primed_data as $product_id => $variation_meta_data ) { wp_cache_set( $product_id, [ 'last_modified' => get_the_modified_date( 'U', $product_id ), 'data' => $variation_meta_data, ], $cache_group ); } } /** * Get the list of classes to apply to this block. * * @return string space-separated list of classes. */ protected function get_container_classes() { $classes = array( 'wc-block-grid', "wp-block-{$this->block_name}", "wc-block-{$this->block_name}", "has-{$this->attributes['columns']}-columns", ); if ( $this->attributes['rows'] > 1 ) { $classes[] = 'has-multiple-rows'; } if ( isset( $this->attributes['align'] ) ) { $classes[] = "align{$this->attributes['align']}"; } if ( ! empty( $this->attributes['alignButtons'] ) ) { $classes[] = 'has-aligned-buttons'; } if ( ! empty( $this->attributes['className'] ) ) { $classes[] = $this->attributes['className']; } return implode( ' ', $classes ); } /** * Render a single products. * * @param \WC_Product $product Product object. * @return string Rendered product output. */ protected function render_product( $product ) { $data = (object) array( 'permalink' => esc_url( $product->get_permalink() ), 'image' => $this->get_image_html( $product ), 'title' => $this->get_title_html( $product ), 'rating' => $this->get_rating_html( $product ), 'price' => $this->get_price_html( $product ), 'badge' => $this->get_sale_badge_html( $product ), 'button' => $this->get_button_html( $product ), ); /** * Filters the HTML for products in the grid. * * @param string $html Product grid item HTML. * @param array $data Product data passed to the template. * @param \WC_Product $product Product object. * @return string Updated product grid item HTML. * * @since 2.2.0 */ return apply_filters( 'woocommerce_blocks_product_grid_item_html', "