WooCommerce Code Reference

ProductFilterPrice.php

Source code

<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;

use Automattic\WooCommerce\Blocks\Utils\ProductCollectionUtils;
use Automattic\WooCommerce\Blocks\QueryFilters;
use Automattic\WooCommerce\Blocks\Package;

/**
 * Product Filter: Price Block.
 */
final class ProductFilterPrice extends AbstractBlock {

	/**
	 * Block name.
	 *
	 * @var string
	 */
	protected $block_name = 'product-filter-price';

	const MIN_PRICE_QUERY_VAR = 'min_price';
	const MAX_PRICE_QUERY_VAR = 'max_price';

	/**
	 * Initialize this block type.
	 *
	 * - Hook into WP lifecycle.
	 * - Register the block with WordPress.
	 */
	protected function initialize() {
		parent::initialize();

		add_filter( 'collection_filter_query_param_keys', array( $this, 'get_filter_query_param_keys' ), 10, 2 );
		add_filter( 'collection_active_filters_data', array( $this, 'register_active_filters_data' ), 10, 2 );
	}

	/**
	 * Register the query param keys.
	 *
	 * @param array $filter_param_keys The active filters data.
	 * @param array $url_param_keys    The query param parsed from the URL.
	 *
	 * @return array Active filters param keys.
	 */
	public function get_filter_query_param_keys( $filter_param_keys, $url_param_keys ) {
		$price_param_keys = array_filter(
			$url_param_keys,
			function( $param ) {
				return self::MIN_PRICE_QUERY_VAR === $param || self::MAX_PRICE_QUERY_VAR === $param;
			}
		);

		return array_merge(
			$filter_param_keys,
			$price_param_keys
		);
	}

	/**
	 * Register the active filters data.
	 *
	 * @param array $data   The active filters data.
	 * @param array $params The query param parsed from the URL.
	 * @return array Active filters data.
	 */
	public function register_active_filters_data( $data, $params ) {
		$min_price           = intval( $params[ self::MIN_PRICE_QUERY_VAR ] ?? 0 );
		$max_price           = intval( $params[ self::MAX_PRICE_QUERY_VAR ] ?? 0 );
		$formatted_min_price = $min_price ? wp_strip_all_tags( wc_price( $min_price, array( 'decimals' => 0 ) ) ) : null;
		$formatted_max_price = $max_price ? wp_strip_all_tags( wc_price( $max_price, array( 'decimals' => 0 ) ) ) : null;

		if ( ! $formatted_min_price && ! $formatted_max_price ) {
			return $data;
		}

		if ( $formatted_min_price && $formatted_max_price ) {
			$title = sprintf(
				/* translators: %1$s and %2$s are the formatted minimum and maximum prices respectively. */
				__( 'Between %1$s and %2$s', 'woocommerce' ),
				$formatted_min_price,
				$formatted_max_price
			);
		}

		if ( ! $formatted_min_price ) {
			/* translators: %s is the formatted maximum price. */
			$title = sprintf( __( 'Up to %s', 'woocommerce' ), $formatted_max_price );
		}

		if ( ! $formatted_max_price ) {
			/* translators: %s is the formatted minimum price. */
			$title = sprintf( __( 'From %s', 'woocommerce' ), $formatted_min_price );
		}

		$data['price'] = array(
			'type'  => __( 'Price', 'woocommerce' ),
			'items' => array(
				array(
					'title'      => $title,
					'attributes' => array(
						'data-wc-on--click' => "{$this->get_full_block_name()}::actions.reset",
					),
				),
			),
		);

		return $data;
	}

	/**
	 * Render the block.
	 *
	 * @param array    $attributes Block attributes.
	 * @param string   $content    Block content.
	 * @param WP_Block $block      Block instance.
	 * @return string Rendered block type output.
	 */
	protected function render( $attributes, $content, $block ) {
		// don't render if its admin, or ajax in progress.
		if ( is_admin() || wp_doing_ajax() ) {
			return '';
		}

		$price_range         = $this->get_filtered_price( $block );
		$min_range           = $price_range['min_price'] ?? 0;
		$max_range           = $price_range['max_price'] ?? 0;
		$min_price           = intval( get_query_var( self::MIN_PRICE_QUERY_VAR, $min_range ) );
		$max_price           = intval( get_query_var( self::MAX_PRICE_QUERY_VAR, $max_range ) );
		$formatted_min_price = wc_price( $min_price, array( 'decimals' => 0 ) );
		$formatted_max_price = wc_price( $max_price, array( 'decimals' => 0 ) );

		$data = array(
			'minPrice' => $min_price,
			'maxPrice' => $max_price,
			'minRange' => $min_range,
			'maxRange' => $max_range,
		);

		list (
			'showInputFields' => $show_input_fields,
			'inlineInput' => $inline_input
		) = $attributes;

		$wrapper_attributes = array(
			'data-wc-interactive' => wp_json_encode( array( 'namespace' => $this->get_full_block_name() ) ),
			'data-wc-context'     => wp_json_encode( $data ),
			'data-has-filter'     => 'no',
		);

		if ( $min_range === $max_range || ! $max_range ) {
			return sprintf(
				'<div %s></div>',
				get_block_wrapper_attributes( $wrapper_attributes )
			);
		}

		// CSS variables for the range bar style.
		$__low       = 100 * ( $min_price - $min_range ) / ( $max_range - $min_range );
		$__high      = 100 * ( $max_price - $min_range ) / ( $max_range - $min_range );
		$range_style = "--low: $__low%; --high: $__high%";

		$price_min = $show_input_fields ?
			sprintf(
				'<input
					class="min"
					name="min"
					type="text"
					value="%s"
					data-wc-bind--value="state.formattedMinPrice"
					data-wc-on--change="actions.updateProducts"
					pattern=""
				/>',
				wp_strip_all_tags( $formatted_min_price )
			) : sprintf(
				'<span data-wc-text="state.formattedMinPrice">%s</span>',
				// Not escaped, as this is HTML.
				$formatted_min_price
			);

		$price_max = $show_input_fields ?
			sprintf(
				'<input
					class="max"
					name="max"
					type="text"
					value="%s"
					data-wc-bind--value="state.formattedMaxPrice"
					data-wc-on--change="actions.updateProducts"
				/>',
				wp_strip_all_tags( $formatted_max_price )
			) : sprintf(
				'<span data-wc-text="state.formattedMaxPrice">%s</span>',
				// Not escaped, as this is HTML.
				$formatted_max_price
			);

		$wrapper_attributes['data-has-filter'] = 'yes';

		$filter_price_content_classes = array(
			'wp-block-woocommerce-product-filter-price-content',
			$show_input_fields && $inline_input ? 'wp-block-woocommerce-product-filter-price-content--inline' : '',
		);

		ob_start();
		?>
			<div <?php echo get_block_wrapper_attributes( $wrapper_attributes ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
				<?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
				<div
					class="<?php echo esc_attr( implode( ' ', $filter_price_content_classes ) ); ?>"
				>
					<div class="wp-block-woocommerce-product-filter-price-content-left-input text">
						<?php echo $price_min; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</div>
					<div
						class="wp-block-woocommerce-product-filter-price-content-price-range-slider range"
						style="<?php echo esc_attr( $range_style ); ?>"
						data-wc-bind--style="state.rangeStyle"
					>
						<div class="range-bar"></div>
						<input
							type="range"
							class="min"
							name="min"
							min="<?php echo esc_attr( $min_range ); ?>"
							max="<?php echo esc_attr( $max_range ); ?>"
							value="<?php echo esc_attr( $min_price ); ?>"
							data-wc-bind--min="context.minRange"
							data-wc-bind--max="context.maxRange"
							data-wc-bind--value="context.minPrice"
							data-wc-on--change="actions.updateProducts"
						>
						<input
							type="range"
							class="max"
							name="max"
							min="<?php echo esc_attr( $min_range ); ?>"
							max="<?php echo esc_attr( $max_range ); ?>"
							value="<?php echo esc_attr( $max_price ); ?>"
							data-wc-bind--min="context.minRange"
							data-wc-bind--max="context.maxRange"
							data-wc-bind--value="context.maxPrice"
							data-wc-on--change="actions.updateProducts"
						>
					</div>
					<div class="wp-block-woocommerce-product-filter-price-content-right-input text">
						<?php // $price_min and $price_max are escaped in the sprintf() calls above. ?>
						<?php echo $price_max; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</div>
				</div>
			</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * Retrieve the price filter data for current block.
	 *
	 * @param WP_Block $block Block instance.
	 */
	private function get_filtered_price( $block ) {
		$filters    = Package::container()->get( QueryFilters::class );
		$query_vars = ProductCollectionUtils::get_query_vars( $block, 1 );

		unset( $query_vars['min_price'], $query_vars['max_price'] );

		if ( ! empty( $query_vars['meta_query'] ) ) {
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
			$query_vars['meta_query'] = ProductCollectionUtils::remove_query_array( $query_vars['meta_query'], 'key', '_price' );
		}

		$price_results = $filters->get_filtered_price( $query_vars );

		return array(
			'min_price' => intval( floor( $price_results->min_price ?? 0 ) ),
			'max_price' => intval( ceil( $price_results->max_price ?? 0 ) ),
		);
	}
}