Commit daf0412fbc8 for woocommerce

commit daf0412fbc8f1457731ca7ed05f06a55e717e762
Author: Raluca Stan <ralucastn@gmail.com>
Date:   Tue Sep 8 18:32:50 2026 +0200

    Fix the product and add to cart hook docblocks (#68154)

    * Document loop_shop_per_page in WC_Query and skip it in Blocks

    Both Blocks call sites carried a one-line comment pointing at
    WC_Query::product_query() plus a phpcs:ignore, so the regenerated
    reference delisted the filter and left a dead anchor in the migrated
    hooks list, which #67839 removed.

    loop_shop_per_page is core's, in use long before Blocks read it, and
    WC_Query::product_query() is where it belongs. Document it there. Core
    has fired it since 1.0, in classes/woocommerce_query.class.php at the
    time.

    The two Blocks sites only read the filtered value so the editor and the
    front end agree on the page size; they are not where the filter is
    defined and should not be a second copy of its contract. They keep the
    phpcs:ignore, now naming where the docblock lives, which is the pattern
    ShippingController.php already uses for the local pickup tax filters.

    The filter stays out of the Blocks reference and out of the migrated
    hooks list, so nothing links to an anchor that is not generated.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Document the woocommerce_loop_add_to_cart_args arguments

    The hook docblock in ProductButton.php carried no @param tags, so the
    generated signature read `apply_filters( 'woocommerce_loop_add_to_cart_args' )`
    with no arguments at all while the call passes $args and $product.
    Add both and regenerate.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Correct the woocommerce_quantity_input_placeholder docblock

    The docblock described the placeholder filter but documented
    `int $max_value Maximum quantity value` — copied from the max_value
    argument sitting two lines above the call. The filtered value is the
    quantity input's placeholder, so name and describe it as one, and
    qualify WC_Product the way the rest of the file does.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Fix the @since on the fallback add-to-cart action

    The docblock at this call site came in with 3ffff54646 (#54569), which
    first shipped in 9.8.0; the 9.7.0 it carries was copied from the
    sibling docblock in AddToCartForm.php. The other call site in this file
    says 9.9.0, which matches its own introducing commit 57134b78b2
    (#56536), so it stays as is.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Convert the AddToCartForm @since to a WooCommerce version

    The 9.7.0 here was written in April 2023 and meant WooCommerce Blocks
    9.7.0, back when Blocks shipped as its own plugin with its own version
    line. In the monorepo it reads as WooCommerce 9.7.0 — February 2025,
    almost two years late.

    The call site came in with 9e0c20d43e (woocommerce-blocks#8284). Blocks
    9.7.x was never bundled into core: the pin went 9.6.5 straight to 9.8.0
    in b88e07eda6, first tagged 7.6.0, so that is when this action first
    reached WooCommerce.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Fence the array example in the hook compatibility docblock

    docblock-to-markdown joins consecutive prose lines, so the array format
    in the woocommerce_blocks_hook_compatibility_additional_data docblock
    landed in filters.md as a single run-on line. Fenced blocks pass
    through untouched, so wrap it in a ```text fence — which also makes the
    backticks inside it unnecessary, since the fence already renders the
    angle-bracket placeholders literally.

    Drop the same backticks from set_hook_data() next door for consistency:
    that docblock is not a hook docblock, so the generator never reads it
    and its Where: bullets already spelled the placeholders bare.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Add changelog entry for the product and add to cart hook docs

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Fix the woocommerce_blocks_product_grid_is_cacheable docblock

    Two copy-paste errors: $is_cacheable was described as "The list of
    script dependencies", and @return said array while the filtered value
    is cast to (bool). The reference published both.

    Supersedes #67837, which fixed the same docblock but hand-edited
    filters.md because build:docs was broken when it was opened. The
    pipeline generates that section now, so the correction lands through a
    regen instead — and it renders the return type as `bool`, which the
    hand-edit had as `boolean`.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Correct the impossible @since on the quantity placeholder filter

    The docblock said 3.10.0, a WooCommerce version that never existed
    (3.9 went straight to 4.0). It is a WooCommerce Blocks number, and this
    docblock is post-merge, so it was wrong when it was written.

    woocommerce_quantity_input_placeholder came in with 917795d78f
    (2020-01-20), first tagged 4.0.0.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Drop the baseline entry the product type qualification resolved

    Writing the param as \WC_Product made PHPStan resolve it to the global
    class, so the parameter.phpDocType error baselined for
    GroupedProductItemSelector.php no longer fires and the baseline check
    failed on the stale entry.

    Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

    * Scope the loop_shop_per_page docblock to WC_Query::product_query()

    The "only applies when the query does not already set posts_per_page"
    condition belongs to this call site, not to the filter. Product
    Collection (ProductCollection/Controller.php) and ProductQuery both fire
    loop_shop_per_page unconditionally, so stating it as a property of the
    filter is wrong for two of its three callers.

    * Simplify the changelog entry for the hook docblock fixes

    Enumerating each hook made the entry harder to read than what it
    describes. "Fix the product and add to cart block hook docblocks" is
    what a reader needs from a dev-type patch entry.

    * Regenerate the hook docs after rebasing onto trunk

    None of this comes from the docblocks this branch touches. Two trunk
    changes landed without a regen: the CheckoutFields split moved the
    source of three hooks to CheckoutFieldsStorage.php, and the
    woocommerce_thankyou_order_failed_text filter added in Status.php had
    no entry yet. Running build:docs on the rebased branch picks both up.

    Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

    ---------

    Co-authored-by: Claude Opus 5 <noreply@anthropic.com>

diff --git a/plugins/woocommerce/changelog/67855-hook-docs-product-add-to-cart b/plugins/woocommerce/changelog/67855-hook-docs-product-add-to-cart
new file mode 100644
index 00000000000..ee004cce4b8
--- /dev/null
+++ b/plugins/woocommerce/changelog/67855-hook-docs-product-add-to-cart
@@ -0,0 +1,4 @@
+Significance: patch
+Type: dev
+
+Fix the product and add to cart block hook docblocks.
diff --git a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/actions.md b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/actions.md
index c4f1b9b166b..7b505716b8c 100644
--- a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/actions.md
+++ b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/actions.md
@@ -681,7 +681,7 @@ do_action( 'woocommerce_set_additional_field_value', string $key, mixed $value,

 ### Source

-- [Blocks/Domain/Services/CheckoutFields.php](../../../../../../src/Blocks/Domain/Services/CheckoutFields.php)
+- [Blocks/Domain/Services/CheckoutFieldsStorage.php](../../../../../../src/Blocks/Domain/Services/CheckoutFieldsStorage.php)

 ---

diff --git a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
index ff29e44fbe8..a9e2833cdd7 100644
--- a/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
+++ b/plugins/woocommerce/client/blocks/docs/third-party-developers/extensibility/hooks/filters.md
@@ -74,6 +74,7 @@
 - [`woocommerce_store_api_product_quantity_{$value_type}`](#woocommerce_store_api_product_quantity_value_type)
 - [woocommerce_store_api_rate_limit_id](#woocommerce_store_api_rate_limit_id)
 - [woocommerce_store_api_rate_limit_options](#woocommerce_store_api_rate_limit_options)
+- [woocommerce_thankyou_order_failed_text](#woocommerce_thankyou_order_failed_text)
 - [woocommerce_thankyou_order_received_title](#woocommerce_thankyou_order_received_title)
 - [woocommerce_use_block_notices_in_classic_theme](#woocommerce_use_block_notices_in_classic_theme)
 - [woocommerce_variation_option_name](#woocommerce_variation_option_name)
@@ -564,7 +565,20 @@ apply_filters( 'woocommerce_blocks_hook_compatibility_additional_data', array $d

 ### Description

-Accepts an array of hooked data. The array should be in the following format: [ [ hook => `<hook-name>`, function => `<function-name>`, priority => `<priority>`, ], ... ] Where:
+Accepts an array of hooked data. The array should be in the following format:
+
+```text
+[
+  [
+    hook => <hook-name>,
+    function => <function-name>,
+    priority => <priority>,
+ ],
+ ...
+]
+```
+
+Where:

 - hook-name is the name of the hook that have the functions hooked to.
 - function-name is the hooked function name.
@@ -661,13 +675,13 @@ apply_filters( 'woocommerce_blocks_product_grid_is_cacheable', bool $is_cacheabl

 | Argument | Type | Description |
 | -------- | ---- | ----------- |
-| $is_cacheable | bool | The list of script dependencies. |
+| $is_cacheable | bool | Whether the product grid is cacheable. True to enable cache, false to disable. |
 | $query_args | array | Query args for the products query passed to BlocksWpQuery. |

 ### Returns


-`array` True to enable cache, false to disable cache.
+`bool` True to enable cache, false to disable cache.

 ### Source

@@ -1038,7 +1052,7 @@ apply_filters( 'woocommerce_get_default_value_for_{$key}', null $value, string $

 ### Source

-- [Blocks/Domain/Services/CheckoutFields.php](../../../../../../src/Blocks/Domain/Services/CheckoutFields.php)
+- [Blocks/Domain/Services/CheckoutFieldsStorage.php](../../../../../../src/Blocks/Domain/Services/CheckoutFieldsStorage.php)

 ---

@@ -1061,7 +1075,7 @@ apply_filters( 'woocommerce_get_default_value_for_{$missing_field}', null $value

 ### Source

-- [Blocks/Domain/Services/CheckoutFields.php](../../../../../../src/Blocks/Domain/Services/CheckoutFields.php)
+- [Blocks/Domain/Services/CheckoutFieldsStorage.php](../../../../../../src/Blocks/Domain/Services/CheckoutFieldsStorage.php)

 ---

@@ -1175,9 +1189,16 @@ Allows backward compatibility with the `rest_request_after_callbacks` filter by
 Allow filtering of the add to cart button arguments.

 ```php
-apply_filters( 'woocommerce_loop_add_to_cart_args' )
+apply_filters( 'woocommerce_loop_add_to_cart_args', array $args, \WC_Product $product )
 ```

+### Parameters
+
+| Argument | Type | Description |
+| -------- | ---- | ----------- |
+| $args | array | Button arguments, with a `class` string and an `attributes` array. |
+| $product | \WC_Product | Product the button is rendered for. |
+
 ### Source

 - [Blocks/BlockTypes/ProductButton.php](../../../../../../src/Blocks/BlockTypes/ProductButton.php)
@@ -1452,17 +1473,17 @@ apply_filters( 'woocommerce_product_tabs', array $tabs )
 ## woocommerce_quantity_input_placeholder


-Filter the placeholder value allowed for the product.
+Filter the placeholder shown in the quantity input.

 ```php
-apply_filters( 'woocommerce_quantity_input_placeholder', int $max_value, \WC_Product $product )
+apply_filters( 'woocommerce_quantity_input_placeholder', int $placeholder, \WC_Product $product )
 ```

 ### Parameters

 | Argument | Type | Description |
 | -------- | ---- | ----------- |
-| $max_value | int | Maximum quantity value. |
+| $placeholder | int | Placeholder for the quantity input. |
 | $product | \WC_Product | Product object. |

 ### Source
@@ -1821,6 +1842,32 @@ apply_filters( 'woocommerce_store_api_rate_limit_options', array $rate_limit_opt

 ---

+## woocommerce_thankyou_order_failed_text
+
+
+Filters the message shown when an order has failed.
+
+```php
+apply_filters( 'woocommerce_thankyou_order_failed_text', string $message, \WC_Order $order )
+```
+
+### Description
+
+Runs after the legacy order-received filter so callbacks can customize the final failed-order message.
+
+### Parameters
+
+| Argument | Type | Description |
+| -------- | ---- | ----------- |
+| $message | string | The failed order message. |
+| $order | \WC_Order | The failed order. |
+
+### Source
+
+- [Blocks/BlockTypes/OrderConfirmation/Status.php](../../../../../../src/Blocks/BlockTypes/OrderConfirmation/Status.php)
+
+---
+
 ## woocommerce_thankyou_order_received_title


diff --git a/plugins/woocommerce/includes/class-wc-query.php b/plugins/woocommerce/includes/class-wc-query.php
index caccc8d62f3..b35222a72c6 100644
--- a/plugins/woocommerce/includes/class-wc-query.php
+++ b/plugins/woocommerce/includes/class-wc-query.php
@@ -581,6 +581,17 @@ class WC_Query {
 		$q->set( 'post__in', array_unique( (array) apply_filters( 'loop_shop_post_in', array() ) ) );

 		// Work out how many products to query.
+		/**
+		 * Filters the number of products shown per page in a product loop.
+		 *
+		 * In WC_Query::product_query() this applies only when the query does not already
+		 * set posts_per_page. Other callers, such as the Product Collection block, apply it
+		 * unconditionally.
+		 *
+		 * @since 1.0.0
+		 *
+		 * @param int $per_page Products per page. Defaults to the store's columns times its rows setting.
+		 */
 		$q->set( 'posts_per_page', $q->get( 'posts_per_page' ) ? $q->get( 'posts_per_page' ) : apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) );

 		// Store reference to this query.
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index fff7b9ff431..f06154a3aa7 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -49329,12 +49329,6 @@ parameters:
 			count: 1
 			path: src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItem.php

-		-
-			message: '#^@param Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\WC_Product \$product does not accept actual type of parameter\: WC_Product\.$#'
-			identifier: parameter.phpDocType
-			count: 1
-			path: src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
-
 		-
 			message: '#^Method Automattic\\WooCommerce\\Blocks\\BlockTypes\\AddToCartWithOptions\\GroupedProductItemSelector\:\:get_button_markup\(\) should return string but returns string\|false\.$#'
 			identifier: return.type
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractProductGrid.php b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractProductGrid.php
index 7f2186d6b58..c192a5eb3e8 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractProductGrid.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractProductGrid.php
@@ -349,9 +349,9 @@ abstract class AbstractProductGrid extends AbstractDynamicBlock {
 		/**
 		 * Filters whether or not the product grid is cacheable.
 		 *
-		 * @param boolean $is_cacheable The list of script dependencies.
+		 * @param boolean $is_cacheable Whether the product grid is cacheable. True to enable cache, false to disable.
 		 * @param array $query_args Query args for the products query passed to BlocksWpQuery.
-		 * @return array True to enable cache, false to disable cache.
+		 * @return boolean True to enable cache, false to disable cache.
 		 *
 		 * @since 2.5.0
 		 */
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
index 9fb37e03e24..c52e84a773e 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartForm.php
@@ -222,7 +222,7 @@ class AddToCartForm extends AbstractBlock {
 		/**
 		 * Trigger the single product add to cart action for each product type.
 		 *
-		 * @since 9.7.0
+		 * @since 7.6.0
 		 */
 		do_action( 'woocommerce_' . $product->get_type() . '_add_to_cart' );

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
index 17c65282742..a4311cd8267 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/AddToCartWithOptions.php
@@ -659,7 +659,7 @@ class AddToCartWithOptions extends AbstractBlock {
 			/**
 			 * Trigger the single product add to cart action that prints the markup.
 			 *
-			 * @since 9.7.0
+			 * @since 9.8.0
 			 */
 			do_action( 'woocommerce_' . $product_type . '_add_to_cart' );

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
index 0f8a6e2e587..cf6fcd77e0f 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AddToCartWithOptions/GroupedProductItemSelector.php
@@ -57,11 +57,11 @@ class GroupedProductItemSelector extends AbstractBlock {
 				'min_value'   => 0,
 				'max_value'   => $max_value,
 				/**
-				 * Filter the placeholder value allowed for the product.
+				 * Filter the placeholder shown in the quantity input.
 				 *
-				 * @since 3.10.0
-				 * @param int        $max_value Maximum quantity value.
-				 * @param WC_Product $product   Product object.
+				 * @since 4.0.0
+				 * @param int         $placeholder Placeholder for the quantity input.
+				 * @param \WC_Product $product     Product object.
 				 */
 				'placeholder' => apply_filters( 'woocommerce_quantity_input_placeholder', 0, $product ),
 			)
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
index f2c532b9eed..0b6f83220f6 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductButton.php
@@ -211,6 +211,9 @@ class ProductButton extends AbstractBlock {
 		 * Allow filtering of the add to cart button arguments.
 		 *
 		 * @since 9.7.0
+		 *
+		 * @param array       $args    Button arguments, with a `class` string and an `attributes` array.
+		 * @param \WC_Product $product Product the button is rendered for.
 		 */
 		$args = apply_filters(
 			'woocommerce_loop_add_to_cart_args',
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php
index 3f87a11efe2..9b7ee4f01fe 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductCollection/Controller.php
@@ -220,8 +220,7 @@ class Controller extends AbstractBlock {
 	protected function enqueue_data( array $attributes = array() ) {
 		parent::enqueue_data( $attributes );

-		// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
-		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Documented in WC_Query::product_query().
 		$this->asset_data_registry->add( 'loopShopPerPage', apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) );
 	}

diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
index abf193797ff..cb87db61e06 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/ProductQuery.php
@@ -140,8 +140,7 @@ class ProductQuery extends AbstractBlock {
 			$post_template_has_support_for_grid_view
 		);

-		// The `loop_shop_per_page` filter can be found in WC_Query::product_query().
-		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+		// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Documented in WC_Query::product_query().
 		$this->asset_data_registry->add( 'loopShopPerPage', apply_filters( 'loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page() ) );
 	}

diff --git a/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php b/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php
index 179a073343e..19c1d9c76db 100644
--- a/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php
+++ b/plugins/woocommerce/src/Blocks/Templates/AbstractTemplateCompatibility.php
@@ -110,11 +110,11 @@ abstract class AbstractTemplateCompatibility {
 	 *
 	 * The array format:
 	 * [
-	 *   `<hook-name>` => [
-	 *     block_names => [ `<block-name>`, ... ],
+	 *   <hook-name> => [
+	 *     block_names => [ <block-name>, ... ],
 	 *     position => before|after,
 	 *     hooked => [
-	 *       `<function-name>` => `<priority>`,
+	 *       <function-name> => <priority>,
 	 *        ...
 	 *     ],
 	 *  ],
@@ -154,14 +154,18 @@ abstract class AbstractTemplateCompatibility {
 		 *
 		 * Accepts an array of hooked data. The array should be in the following
 		 * format:
+		 *
+		 * ```text
 		 * [
 		 *   [
-		 *     hook => `<hook-name>`,
-		 *     function => `<function-name>`,
-		 *     priority => `<priority>`,
+		 *     hook => <hook-name>,
+		 *     function => <function-name>,
+		 *     priority => <priority>,
 		 *  ],
 		 *  ...
 		 * ]
+		 * ```
+		 *
 		 * Where:
 		 * - hook-name is the name of the hook that have the functions hooked to.
 		 * - function-name is the hooked function name.