Added Custom Collection Handlers

Developers can register collections along with handlers that implement
the custom behavior.
This commit is contained in:
Christopher Allford 2024-09-11 14:30:07 -07:00
parent 2f85a455cf
commit b47b0f638f
No known key found for this signature in database
GPG Key ID: 80E44C778F08A88E
3 changed files with 254 additions and 47 deletions

View File

@ -18,6 +18,13 @@ class ProductCollection extends AbstractBlock {
*/
protected $block_name = 'product-collection';
/**
* An array keyed by the name of the collection containing handlers for implementing custom collection behavior.
*
* @var array
*/
protected $collection_handler_store = array();
/**
* The Block with its attributes before it gets rendered
*
@ -617,22 +624,32 @@ class ProductCollection extends AbstractBlock {
/**
* Update the query for the product query block in Editor.
*
* @param array $args Query args.
* @param array $query Query args.
* @param WP_REST_Request $request Request.
*/
public function update_rest_query_in_editor( $args, $request ): array {
public function update_rest_query_in_editor( $query, $request ): array {
// Only update the query if this is a product collection block.
$is_product_collection_block = $request->get_param( 'isProductCollectionBlock' );
if ( ! $is_product_collection_block ) {
return $args;
return $query;
}
$product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
$collection_args = array(
'name' => $product_collection_query_context['collection'] ?? '',
);
// Allow collections to modify the collection arguments passed to the query builder.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['editor_args'] ) ) {
$collection_args = call_user_func( $handlers['editor_args'], $collection_args, $query );
}
// Is this a preview mode request?
// If yes, short-circuit the query and return the preview query args.
$product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
$is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
$is_preview = $product_collection_query_context['previewState']['isPreview'] ?? false;
if ( 'true' === $is_preview ) {
return $this->get_preview_query_args( $args, $request );
return $this->get_preview_query_args( $collection_args, $query, $request );
}
$orderby = $request->get_param( 'orderBy' );
@ -645,11 +662,11 @@ class ProductCollection extends AbstractBlock {
$price_range = $request->get_param( 'priceRange' );
// This argument is required for the tests to PHP Unit Tests to run correctly.
// Most likely this argument is being accessed in the test environment image.
$args['author'] = '';
$query['author'] = '';
$final_query = $this->get_final_query_args(
$product_collection_query_context['collection'] ?? '',
$args,
$collection_args,
$query,
array(
'orderby' => $orderby,
'on_sale' => $on_sale,
@ -757,8 +774,18 @@ class ProductCollection extends AbstractBlock {
$time_frame = $query['timeFrame'] ?? null;
$price_range = $query['priceRange'] ?? null;
$collection_args = array(
'name' => $this->parsed_block['attrs']['collection'] ?? '',
);
// Allow collections to modify the collection arguments passed to the query builder.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['frontend_args'] ) ) {
$collection_args = call_user_func( $handlers['frontend_args'], $collection_args, $query );
}
$final_query = $this->get_final_query_args(
$this->parsed_block['attrs']['collection'] ?? '',
$collection_args,
$common_query_values,
array(
'on_sale' => $is_on_sale,
@ -780,12 +807,17 @@ class ProductCollection extends AbstractBlock {
/**
* Get final query args based on provided values
*
* @param string $collection The name of the collection.
* @param array $common_query_values Common query values.
* @param array $query Query from block context.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
* @param array $collection_args Any special arguments that should change the behavior of the query.
* @param array $common_query_values Common query values.
* @param array $query Query from block context.
* @param bool $is_exclude_applied_filters Whether to exclude the applied filters or not.
*/
private function get_final_query_args( $collection, $common_query_values, $query, $is_exclude_applied_filters = false ) {
private function get_final_query_args(
$collection_args,
$common_query_values,
$query,
$is_exclude_applied_filters = false
) {
$orderby_query = $query['orderby'] ? $this->get_custom_orderby_query( $query['orderby'] ) : array();
$on_sale_query = $this->get_on_sale_products_query( $query['on_sale'] );
$stock_query = $this->get_stock_status_query( $query['stock_status'] );
@ -797,11 +829,24 @@ class ProductCollection extends AbstractBlock {
$date_query = $this->get_date_query( $query['timeFrame'] ?? array() );
$price_query_args = $this->get_price_range_query_args( $query['priceRange'] ?? array() );
$handpicked_query = $this->get_handpicked_query( $query['handpicked_products'] ?? false );
$collection_query = $this->get_core_collection_query( $collection, $query );
// We exclude applied filters to generate product ids for the filter blocks.
$applied_filters_query = $is_exclude_applied_filters ? array() : $this->get_queries_by_applied_filters();
// Allow collections to provide their own query parameters.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['build_query'] ) ) {
$collection_query = call_user_func(
$handlers['build_query'],
$collection_args,
$common_query_values,
$query,
$is_exclude_applied_filters
);
} else {
$collection_query = array();
}
return $this->merge_queries(
$common_query_values,
$orderby_query,
@ -816,41 +861,21 @@ class ProductCollection extends AbstractBlock {
);
}
/**
* Get any collection-specific query args to merge.
*
* @param string $collection The name of the collection.
* @param array $query Query from block context.
*
* @return array The collection-specific query to merge.
*/
private function get_core_collection_query( $collection, $query ) {
$collection = preg_match( '/^woocommerce\/product-collection\/(.*)/', $collection, $matches ) ? $matches[1] : '';
if ( '' === $collection ) {
return array();
}
return array();
}
/**
* Get query args for preview mode. These query args will be used with WP_Query to fetch the products.
*
* @param array $args Query args.
* @param WP_REST_Request $request Request.
* @param array $collection_args Any collection-specific arguments.
* @param array $args Query args.
* @param WP_REST_Request $request Request.
*/
private function get_preview_query_args( $args, $request ) {
private function get_preview_query_args( $collection_args, $args, $request ) {
$collection_query = array();
/**
* In future, Here we will modify the preview query based on the collection name. For example:
*
* $product_collection_query_context = $request->get_param( 'productCollectionQueryContext' );
* $collection_name = $product_collection_query_context['collection'] ?? '';
* if ( 'woocommerce/product-collection/on-sale' === $collection_name ) {
* $collection_query = $this->get_on_sale_products_query( true );
* }.
*/
// Allow collections to override the preview mode behavior.
$handlers = $this->collection_handler_store[ $collection_args['name'] ] ?? null;
if ( isset( $handlers['preview_query'] ) ) {
$collection_query = call_user_func( $handlers['preview_query'], $collection_args, $args, $request );
}
$args = $this->merge_queries( $args, $collection_query );
return $args;
@ -892,8 +917,8 @@ class ProductCollection extends AbstractBlock {
return $acc;
}
// If the $query doesn't contain any valid query keys, we unpack/spread it then merge.
if ( empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
// When the $query has keys but doesn't contain any valid query keys, we unpack/spread it then merge.
if ( ! empty( $query ) && empty( array_intersect( $this->get_valid_query_vars(), array_keys( $query ) ) ) ) {
return $this->merge_queries( $acc, ...array_values( $query ) );
}
@ -1750,4 +1775,28 @@ class ProductCollection extends AbstractBlock {
return $price_filter + array_sum( $taxes );
}
/**
* Registers handlers for a collection.
*
* @param string $collection_name The name of the custom collection.
* @param callable $build_query A hook returning any custom query arguments to merge with the collection's query.
* @param callable|null $frontend_args An optional hook that returns any frontend collection arguments to pass to the query builder.
* @param callable|null $editor_args An optional hook that returns any REST collection arguments to pass to the query builder.
* @param callable|null $preview_query An optional hook that returns a query to use in preview mode.
*
* @throws \InvalidArgumentException If collection handlers are already registered for the given collection name.
*/
protected function register_collection_handlers( $collection_name, $build_query, $frontend_args = null, $editor_args = null, $preview_query = null ) {
if ( isset( $this->collection_handler_store[ $collection_name ] ) ) {
throw new \InvalidArgumentException( 'Collection handlers already registered for ' . esc_html( $collection_name ) );
}
$this->collection_handler_store[ $collection_name ] = array(
'build_query' => $build_query,
'frontend_args' => $frontend_args,
'editor_args' => $editor_args,
'preview_query' => $preview_query,
);
}
}

View File

@ -981,4 +981,138 @@ class ProductCollection extends \WP_UnitTestCase {
$this->assertCount( 1, $merged_query['post__in'] );
}
/**
* Test for frontend collection handlers.
*/
public function test_frontend_collection_handlers() {
$build_query = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$frontend_args = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$this->block_instance->register_collection_handlers( 'test-collection', $build_query, $frontend_args );
$frontend_args->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$collection_args['test'] = 'test-arg';
return $collection_args;
}
);
$build_query->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$this->assertArrayHasKey( 'test', $collection_args );
$this->assertEquals( 'test-arg', $collection_args['test'] );
return array(
'post__in' => array( 111 ),
);
}
);
$parsed_block = $this->get_base_parsed_block();
$parsed_block['attrs']['collection'] = 'test-collection';
$merged_query = $this->initialize_merged_query( $parsed_block );
$this->block_instance->unregister_collection_handlers( 'test-collection' );
$this->assertContains( 111, $merged_query['post__in'] );
}
/**
* Test for editor collection handlers.
*/
public function test_editor_collection_handlers() {
$build_query = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$editor_args = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$this->block_instance->register_collection_handlers( 'test-collection', $build_query, null, $editor_args );
$editor_args->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$collection_args['test'] = 'test-arg';
return $collection_args;
}
);
$build_query->expects( $this->once() )
->method( '__invoke' )
->willReturnCallback(
function ( $collection_args ) {
$this->assertArrayHasKey( 'test', $collection_args );
$this->assertEquals( 'test-arg', $collection_args['test'] );
return array(
'post__in' => array( 111 ),
);
}
);
$args = array();
$request = $this->build_request();
$request->set_param(
'productCollectionQueryContext',
array(
'collection' => 'test-collection',
)
);
$updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
$this->block_instance->unregister_collection_handlers( 'test-collection' );
$this->assertContains( 111, $updated_query['post__in'] );
}
/**
* Test for the editor preview collection handler.
*/
public function test_editor_preview_collection_handler() {
$preview_query = $this->getMockBuilder( \stdClass::class )
->setMethods( [ '__invoke' ] )
->getMock();
$this->block_instance->register_collection_handlers(
'test-collection',
function () {
return array();
},
null,
null,
$preview_query
);
$preview_query->expects( $this->once() )
->method( '__invoke' )
->willReturn(
array(
'post__in' => array( 123 ),
)
);
$args = array();
$request = $this->build_request();
$request->set_param(
'productCollectionQueryContext',
array(
'collection' => 'test-collection',
'previewState' => array(
'isPreview' => 'true',
),
)
);
$updated_query = $this->block_instance->update_rest_query_in_editor( $args, $request );
$this->block_instance->unregister_collection_handlers( 'test-collection' );
$this->assertContains( 123, $updated_query['post__in'] );
}
}

View File

@ -9,6 +9,8 @@ use Automattic\WooCommerce\Blocks\Assets\Api;
use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
// phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
/**
* ProductCollectionMock used to test Product Query block functions.
*/
@ -49,4 +51,26 @@ class ProductCollectionMock extends ProductCollection {
public function set_attributes_filter_query_args( $data ) {
$this->attributes_filter_query_args = $data;
}
/**
* Makes a protected method public so that it can be used in tests.
*
* @param string $collection_name The name of the custom collection.
* @param callable $build_query A hook returning any custom query arguments to merge with the collection's query.
* @param callable|null $frontend_args An optional hook that returns any frontend collection arguments to pass to the query builder.
* @param callable|null $editor_args An optional hook that returns any REST collection arguments to pass to the query builder.
* @param callable|null $preview_query An optional hook that returns a query to use in preview mode.
*/
public function register_collection_handlers( $collection_name, $build_query, $frontend_args = null, $editor_args = null, $preview_query = null ) {
parent::register_collection_handlers( $collection_name, $build_query, $frontend_args, $editor_args, $preview_query );
}
/**
* Removes any custom collection handlers for the given collection.
*
* @param string $collection_name The name of the collection to unregister.
*/
public function unregister_collection_handlers( $collection_name ) {
unset( $this->collection_handlers[ $collection_name ] );
}
}