Commit f38159076e for woocommerce

commit f38159076e4558e5bc523118e12c9462c255ff0a
Author: Ninos Ego <me@ninosego.de>
Date:   Fri Jan 30 09:15:23 2026 +0100

    Filter for modifying product attributes/downloads before reading & additional `extra_data` option (#62572)

diff --git a/plugins/woocommerce/changelog/PR-62572 b/plugins/woocommerce/changelog/PR-62572
new file mode 100644
index 0000000000..fc308d8106
--- /dev/null
+++ b/plugins/woocommerce/changelog/PR-62572
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Add filter for modifying product attributes/downloads before reading & additional `extra_data` option
diff --git a/plugins/woocommerce/includes/class-wc-product-attribute.php b/plugins/woocommerce/includes/class-wc-product-attribute.php
index 3af608af8a..53e44f43a2 100644
--- a/plugins/woocommerce/includes/class-wc-product-attribute.php
+++ b/plugins/woocommerce/includes/class-wc-product-attribute.php
@@ -32,6 +32,14 @@ class WC_Product_Attribute implements ArrayAccess {
 		'variation' => false,
 	);

+	/**
+	 * Extra data array.
+	 *
+	 * @since 10.6.0
+	 * @var array
+	 */
+	protected $extra_data = array();
+
 	/**
 	 * Return if this attribute is a taxonomy.
 	 *
@@ -124,6 +132,7 @@ class WC_Product_Attribute implements ArrayAccess {
 	 */
 	public function get_data() {
 		return array_merge(
+			$this->extra_data,
 			$this->data,
 			array(
 				'is_visible'   => $this->get_visible() ? 1 : 0,
@@ -140,6 +149,17 @@ class WC_Product_Attribute implements ArrayAccess {
 	|--------------------------------------------------------------------------
 	*/

+	/**
+	 * Set extra data by key.
+	 *
+	 * @since 10.6.0
+	 * @param string $key   Extra data key.
+	 * @param mixed  $value Extra data value.
+	 */
+	public function set_extra_data( string $key, $value ): void {
+		$this->extra_data[ $key ] = $value;
+	}
+
 	/**
 	 * Set ID (this is the attribute ID).
 	 *
@@ -200,6 +220,27 @@ class WC_Product_Attribute implements ArrayAccess {
 	|--------------------------------------------------------------------------
 	*/

+	/**
+	 * Get all extra data.
+	 *
+	 * @since 10.6.0
+	 * @return array
+	 */
+	public function get_all_extra_data() {
+		return $this->extra_data;
+	}
+
+	/**
+	 * Get extra data by key.
+	 *
+	 * @since 10.6.0
+	 * @param string $key Extra data key.
+	 * @return mixed
+	 */
+	public function get_extra_data( string $key ) {
+		return $this->extra_data[ $key ] ?? null;
+	}
+
 	/**
 	 * Get the ID.
 	 *
@@ -281,6 +322,9 @@ class WC_Product_Attribute implements ArrayAccess {
 				if ( is_callable( array( $this, "get_$offset" ) ) ) {
 					return $this->{"get_$offset"}();
 				}
+				if ( isset( $this->extra_data[ $offset ] ) ) {
+					return $this->extra_data[ $offset ];
+				}
 				break;
 		}
 		return '';
@@ -307,7 +351,9 @@ class WC_Product_Attribute implements ArrayAccess {
 			default:
 				if ( is_callable( array( $this, "set_$offset" ) ) ) {
 					$this->{"set_$offset"}( $value );
+					break;
 				}
+				$this->extra_data[ $offset ] = $value;
 				break;
 		}
 	}
@@ -328,6 +374,6 @@ class WC_Product_Attribute implements ArrayAccess {
 	 */
 	#[\ReturnTypeWillChange]
 	public function offsetExists( $offset ) {
-		return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ) ), true );
+		return in_array( $offset, array_merge( array( 'is_variation', 'is_visible', 'is_taxonomy', 'value' ), array_keys( $this->data ), array_keys( $this->extra_data ) ), true );
 	}
 }
diff --git a/plugins/woocommerce/includes/class-wc-product-download.php b/plugins/woocommerce/includes/class-wc-product-download.php
index 261c727015..6f52ff7aff 100644
--- a/plugins/woocommerce/includes/class-wc-product-download.php
+++ b/plugins/woocommerce/includes/class-wc-product-download.php
@@ -31,13 +31,21 @@ class WC_Product_Download implements ArrayAccess {
 		'enabled' => true,
 	);

+	/**
+	 * Extra data array.
+	 *
+	 * @since 10.6.0
+	 * @var array
+	 */
+	protected $extra_data = array();
+
 	/**
 	 * Returns all data for this object.
 	 *
 	 * @return array
 	 */
 	public function get_data() {
-		return $this->data;
+		return array_merge( $this->extra_data, $this->data );
 	}

 	/**
@@ -267,6 +275,17 @@ class WC_Product_Download implements ArrayAccess {
 	|--------------------------------------------------------------------------
 	*/

+	/**
+	 * Set extra data by key.
+	 *
+	 * @since 10.6.0
+	 * @param string $key   Extra data key.
+	 * @param mixed  $value Extra data value.
+	 */
+	public function set_extra_data( string $key, $value ): void {
+		$this->extra_data[ $key ] = $value;
+	}
+
 	/**
 	 * Set ID.
 	 *
@@ -332,6 +351,27 @@ class WC_Product_Download implements ArrayAccess {
 	|--------------------------------------------------------------------------
 	*/

+	/**
+	 * Get all extra data.
+	 *
+	 * @since 10.6.0
+	 * @return array
+	 */
+	public function get_all_extra_data() {
+		return $this->extra_data;
+	}
+
+	/**
+	 * Get extra data by key.
+	 *
+	 * @since 10.6.0
+	 * @param string $key Extra data key.
+	 * @return mixed
+	 */
+	public function get_extra_data( string $key ) {
+		return $this->extra_data[ $key ] ?? null;
+	}
+
 	/**
 	 * Get id.
 	 *
@@ -398,6 +438,9 @@ class WC_Product_Download implements ArrayAccess {
 				if ( is_callable( array( $this, "get_$offset" ) ) ) {
 					return $this->{"get_$offset"}();
 				}
+				if ( isset( $this->extra_data[ $offset ] ) ) {
+					return $this->extra_data[ $offset ];
+				}
 				break;
 		}
 		return '';
@@ -415,7 +458,9 @@ class WC_Product_Download implements ArrayAccess {
 			default:
 				if ( is_callable( array( $this, "set_$offset" ) ) ) {
 					$this->{"set_$offset"}( $value );
+					break;
 				}
+				$this->extra_data[ $offset ] = $value;
 				break;
 		}
 	}
@@ -436,6 +481,6 @@ class WC_Product_Download implements ArrayAccess {
 	 */
 	#[\ReturnTypeWillChange]
 	public function offsetExists( $offset ) {
-		return in_array( $offset, array_keys( $this->data ), true );
+		return in_array( $offset, array_merge( array_keys( $this->data ), array_keys( $this->extra_data ) ), true );
 	}
 }
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
index bc15fb8cb0..ce700d632d 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-data-store-cpt.php
@@ -622,7 +622,17 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 				$attribute->set_position( $meta_value['position'] );
 				$attribute->set_visible( $meta_value['is_visible'] );
 				$attribute->set_variation( $meta_value['is_variation'] );
-				$attributes[] = $attribute;
+
+				/**
+				 * Filter product attribute after initialization.
+				 *
+				 * @since 10.6.0
+				 *
+				 * @param WC_Product_Attribute $attribute  The attribute object.
+				 * @param array                $meta_value The meta value.
+				 * @param WC_Product           $product    The product object.
+				 */
+				$attributes[] = apply_filters( 'woocommerce_product_read_attribute', $attribute, $meta_value, $product );
 			}
 			$product->set_attributes( $attributes );
 		}
@@ -639,15 +649,35 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da

 		if ( $meta_values ) {
 			$downloads = array();
-			foreach ( $meta_values as $key => $value ) {
-				if ( ! isset( $value['name'], $value['file'] ) ) {
+			foreach ( $meta_values as $key => $meta_value ) {
+				if ( ! isset( $meta_value['name'], $meta_value['file'] ) ) {
 					continue;
 				}
 				$download = new WC_Product_Download();
 				$download->set_id( $key );
-				$download->set_name( $value['name'] ? $value['name'] : wc_get_filename_from_url( $value['file'] ) );
-				$download->set_file( apply_filters( 'woocommerce_file_download_path', $value['file'], $product, $key ) );
-				$downloads[] = $download;
+				$download->set_name( $meta_value['name'] ? $meta_value['name'] : wc_get_filename_from_url( $meta_value['file'] ) );
+
+				/**
+				 * Filter for the path of the downloadable file.
+				 *
+				 * @since 2.1.0
+				 *
+				 * @param string     $file    The file path.
+				 * @param WC_Product $product The product object.
+				 * @param string     $key     The download key.
+				 */
+				$download->set_file( apply_filters( 'woocommerce_file_download_path', $meta_value['file'], $product, $key ) );
+
+				/**
+				 * Filter product download after initialization.
+				 *
+				 * @since 10.6.0
+				 *
+				 * @param WC_Product_Download $download   The attribute object.
+				 * @param array               $meta_value The meta value.
+				 * @param WC_Product          $product    The product object.
+				 */
+				$downloads[] = apply_filters( 'woocommerce_product_read_download', $download, $meta_value, $product );
 			}
 			$product->set_downloads( $downloads );
 		}
@@ -1011,13 +1041,16 @@ class WC_Product_Data_Store_CPT extends WC_Data_Store_WP implements WC_Object_Da
 					}

 					// Store in format WC uses in meta.
-					$meta_values[ $attribute_key ] = array(
-						'name'         => $attribute->get_name(),
-						'value'        => $value,
-						'position'     => $attribute->get_position(),
-						'is_visible'   => $attribute->get_visible() ? 1 : 0,
-						'is_variation' => $attribute->get_variation() ? 1 : 0,
-						'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
+					$meta_values[ $attribute_key ] = array_merge(
+						$attribute->get_all_extra_data(),
+						array(
+							'name'         => $attribute->get_name(),
+							'value'        => $value,
+							'position'     => $attribute->get_position(),
+							'is_visible'   => $attribute->get_visible() ? 1 : 0,
+							'is_variation' => $attribute->get_variation() ? 1 : 0,
+							'is_taxonomy'  => $attribute->is_taxonomy() ? 1 : 0,
+						),
 					);
 				}
 			}
diff --git a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
index 3f88f99949..23b5bebcef 100644
--- a/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
+++ b/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
@@ -88,7 +88,17 @@ class WC_Product_Variable_Data_Store_CPT extends WC_Product_Data_Store_CPT imple
 				$attribute->set_position( $meta_value['position'] );
 				$attribute->set_visible( $meta_value['is_visible'] );
 				$attribute->set_variation( $meta_value['is_variation'] );
-				$attributes[] = $attribute;
+
+				/**
+				 * Filter product attribute after initialization.
+				 *
+				 * @since 10.6.0
+				 *
+				 * @param WC_Product_Attribute $attribute  The attribute object.
+				 * @param array                $meta_value The meta value.
+				 * @param WC_Product           $product    The product object.
+				 */
+				$attributes[] = apply_filters( 'woocommerce_product_read_attribute', $attribute, $meta_value, $product );
 			}
 			$product->set_attributes( $attributes );

diff --git a/plugins/woocommerce/includes/import/abstract-wc-product-importer.php b/plugins/woocommerce/includes/import/abstract-wc-product-importer.php
index c0192a040d..aa1d3cefac 100644
--- a/plugins/woocommerce/includes/import/abstract-wc-product-importer.php
+++ b/plugins/woocommerce/includes/import/abstract-wc-product-importer.php
@@ -414,7 +414,17 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
 						$attribute_object->set_position( $position );
 						$attribute_object->set_visible( $is_visible );
 						$attribute_object->set_variation( $is_variation );
-						$attributes[] = $attribute_object;
+
+						/**
+						 * Filter product attribute after initialization.
+						 *
+						 * @since 10.6.0
+						 *
+						 * @param WC_Product_Attribute $attribute_object  The attribute object.
+						 * @param array                $attribute         The attribute data.
+						 * @param WC_Product           $product           The product object.
+						 */
+						$attributes[] = apply_filters( 'woocommerce_product_importer_read_attribute', $attribute_object, $attribute, $product );
 					}
 				} elseif ( isset( $attribute['value'] ) ) {
 					// Check for default attributes and set "is_variation".
@@ -429,7 +439,9 @@ abstract class WC_Product_Importer implements WC_Importer_Interface {
 					$attribute_object->set_position( $position );
 					$attribute_object->set_visible( $is_visible );
 					$attribute_object->set_variation( $is_variation );
-					$attributes[] = $attribute_object;
+
+					// phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- see same filter in the 'if' block.
+					$attributes[] = apply_filters( 'woocommerce_product_importer_read_attribute', $attribute_object, $attribute, $product );
 				}
 			}