Commit 58363d022dd for woocommerce

commit 58363d022dd7a4cc13dd1d3401f4a41ea7777d5e
Author: Bhavik Tank <53536925+bhavz-10@users.noreply.github.com>
Date:   Mon Apr 6 14:04:53 2026 +0530

    Product Collection block: empty offset ("") triggers fatal 500 (TypeError: int + string) — frontend and editor become inaccessible (#64012)

diff --git a/plugins/woocommerce/changelog/fix-61447-product-collection-offset-per-page b/plugins/woocommerce/changelog/fix-61447-product-collection-offset-per-page
new file mode 100644
index 00000000000..bb5b6bf3ef2
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-61447-product-collection-offset-per-page
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix fatal TypeError when Product Collection block offset or perPage is an empty string.
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index af0a7982f65..ed8e3b91f08 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -140,11 +140,13 @@ class QueryBuilder {
 	 * @param bool  $is_exclude_applied_filters Whether to exclude the applied filters or not.
 	 */
 	public function get_final_frontend_query( $collection_args, $query, $page = 1, $is_exclude_applied_filters = false ) {
-		$product_ids = $query['post__in'] ?? array();
-		$offset      = $query['offset'] ?? 0;
-		$per_page    = $query['perPage'] ?? 9;
-		$order       = $query['order'] ?? 'asc';
-		$search      = $query['search'] ?? '';
+		$product_ids  = $query['post__in'] ?? array();
+		$offset_raw   = $query['offset'] ?? 0;
+		$per_page_raw = $query['perPage'] ?? null;
+		$offset       = is_numeric( $offset_raw ) ? max( 0, (int) $offset_raw ) : 0;
+		$per_page     = is_numeric( $per_page_raw ) ? max( 1, (int) $per_page_raw ) : 9;
+		$order        = $query['order'] ?? 'asc';
+		$search       = $query['search'] ?? '';

 		$common_query_values = array(
 			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
index 5c370f5c5a0..1f22823b407 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/ProductCollection/QueryBuilder.php
@@ -1231,4 +1231,20 @@ class QueryBuilder extends \WP_UnitTestCase {

 		return $clauses;
 	}
+
+	/**
+	 * @testdox Empty string values for perPage and offset fall back to defaults.
+	 */
+	public function test_per_page_and_offset_empty_string_handling() {
+		$parsed_block = Utils::get_base_parsed_block();
+
+		// Set values as empty strings.
+		$parsed_block['attrs']['query']['perPage'] = '';
+		$parsed_block['attrs']['query']['offset']  = '';
+
+		$merged_query = Utils::initialize_merged_query( $this->block_instance, $parsed_block );
+
+		$this->assertSame( 0, $merged_query['offset'] );
+		$this->assertSame( 9, $merged_query['posts_per_page'] );
+	}
 }