Commit 5fa549207c7 for woocommerce
commit 5fa549207c79e22c4015f7c342a79492d57c826e
Author: Deepak Lalwani <deepak.lalwani81@gmail.com>
Date: Mon Apr 27 05:13:59 2026 +0530
Add 'product.published' webhook topic (#63555)
* Add 'product.published' webhook topic
* Add changefile(s) from automation for the following project(s): woocommerce
* Enhance product published webhook to include product variations and add tests for transition actions
* Fix @since tag version and array alignment for product.published webhook
Update @since from 10.7.0 to 10.8.0 to match current release version.
Fix double arrow alignment in webhook topic arrays to satisfy phpcs.
---------
Co-authored-by: woocommercebot <woocommercebot@users.noreply.github.com>
Co-authored-by: Michael Pretty <prettyboymp@users.noreply.github.com>
diff --git a/plugins/woocommerce/changelog/63555-enhancement-27650-product-published-webhook-topic b/plugins/woocommerce/changelog/63555-enhancement-27650-product-published-webhook-topic
new file mode 100644
index 00000000000..5e15455dff9
--- /dev/null
+++ b/plugins/woocommerce/changelog/63555-enhancement-27650-product-published-webhook-topic
@@ -0,0 +1,4 @@
+Significance: minor
+Type: enhancement
+
+Add 'product.published' webhook topic than runs when a product is published.
\ No newline at end of file
diff --git a/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php b/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php
index 66c3d772f3e..fb61dd1fe15 100644
--- a/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php
+++ b/plugins/woocommerce/includes/admin/settings/views/html-webhooks-edit.php
@@ -65,23 +65,24 @@ if ( ! defined( 'ABSPATH' ) ) {
$topics = apply_filters(
'woocommerce_webhook_topics',
array(
- '' => __( 'Select an option…', 'woocommerce' ),
- 'coupon.created' => __( 'Coupon created', 'woocommerce' ),
- 'coupon.updated' => __( 'Coupon updated', 'woocommerce' ),
- 'coupon.deleted' => __( 'Coupon deleted', 'woocommerce' ),
- 'coupon.restored' => __( 'Coupon restored', 'woocommerce' ),
- 'customer.created' => __( 'Customer created', 'woocommerce' ),
- 'customer.updated' => __( 'Customer updated', 'woocommerce' ),
- 'customer.deleted' => __( 'Customer deleted', 'woocommerce' ),
- 'order.created' => __( 'Order created', 'woocommerce' ),
- 'order.updated' => __( 'Order updated', 'woocommerce' ),
- 'order.deleted' => __( 'Order deleted', 'woocommerce' ),
- 'order.restored' => __( 'Order restored', 'woocommerce' ),
- 'product.created' => __( 'Product created', 'woocommerce' ),
- 'product.updated' => __( 'Product updated', 'woocommerce' ),
- 'product.deleted' => __( 'Product deleted', 'woocommerce' ),
- 'product.restored' => __( 'Product restored', 'woocommerce' ),
- 'action' => __( 'Action', 'woocommerce' ),
+ '' => __( 'Select an option…', 'woocommerce' ),
+ 'coupon.created' => __( 'Coupon created', 'woocommerce' ),
+ 'coupon.updated' => __( 'Coupon updated', 'woocommerce' ),
+ 'coupon.deleted' => __( 'Coupon deleted', 'woocommerce' ),
+ 'coupon.restored' => __( 'Coupon restored', 'woocommerce' ),
+ 'customer.created' => __( 'Customer created', 'woocommerce' ),
+ 'customer.updated' => __( 'Customer updated', 'woocommerce' ),
+ 'customer.deleted' => __( 'Customer deleted', 'woocommerce' ),
+ 'order.created' => __( 'Order created', 'woocommerce' ),
+ 'order.updated' => __( 'Order updated', 'woocommerce' ),
+ 'order.deleted' => __( 'Order deleted', 'woocommerce' ),
+ 'order.restored' => __( 'Order restored', 'woocommerce' ),
+ 'product.created' => __( 'Product created', 'woocommerce' ),
+ 'product.updated' => __( 'Product updated', 'woocommerce' ),
+ 'product.deleted' => __( 'Product deleted', 'woocommerce' ),
+ 'product.restored' => __( 'Product restored', 'woocommerce' ),
+ 'product.published' => __( 'Product published', 'woocommerce' ),
+ 'action' => __( 'Action', 'woocommerce' ),
)
);
diff --git a/plugins/woocommerce/includes/class-wc-post-data.php b/plugins/woocommerce/includes/class-wc-post-data.php
index 56fa862a165..3326045eed1 100644
--- a/plugins/woocommerce/includes/class-wc-post-data.php
+++ b/plugins/woocommerce/includes/class-wc-post-data.php
@@ -147,6 +147,16 @@ class WC_Post_Data {
if ( ( ProductStatus::PUBLISH === $new_status || ProductStatus::PUBLISH === $old_status ) && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
self::delete_product_query_transients();
}
+
+ if ( ProductStatus::PUBLISH === $new_status && ProductStatus::PUBLISH !== $old_status && in_array( $post->post_type, array( 'product', 'product_variation' ), true ) ) {
+ /**
+ * Fires when a product or product variation transitions to published status.
+ *
+ * @since 10.8.0
+ * @param int $product_id The product or variation ID.
+ */
+ do_action( 'woocommerce_product_published', $post->ID );
+ }
}
/**
diff --git a/plugins/woocommerce/includes/class-wc-webhook.php b/plugins/woocommerce/includes/class-wc-webhook.php
index a0234cd305e..cef7bf36d7f 100644
--- a/plugins/woocommerce/includes/class-wc-webhook.php
+++ b/plugins/woocommerce/includes/class-wc-webhook.php
@@ -941,63 +941,66 @@ class WC_Webhook extends WC_Legacy_Webhook {
*/
private function get_topic_hooks( $topic ) {
$topic_hooks = array(
- 'coupon.created' => array(
+ 'coupon.created' => array(
'woocommerce_process_shop_coupon_meta',
'woocommerce_new_coupon',
),
- 'coupon.updated' => array(
+ 'coupon.updated' => array(
'woocommerce_process_shop_coupon_meta',
'woocommerce_update_coupon',
),
- 'coupon.deleted' => array(
+ 'coupon.deleted' => array(
'wp_trash_post',
),
- 'coupon.restored' => array(
+ 'coupon.restored' => array(
'untrashed_post',
),
- 'customer.created' => array(
+ 'customer.created' => array(
'user_register',
'woocommerce_created_customer',
'woocommerce_new_customer',
),
- 'customer.updated' => array(
+ 'customer.updated' => array(
'profile_update',
'woocommerce_update_customer',
),
- 'customer.deleted' => array(
+ 'customer.deleted' => array(
'delete_user',
),
- 'order.created' => array(
+ 'order.created' => array(
'woocommerce_new_order',
),
- 'order.updated' => array(
+ 'order.updated' => array(
'woocommerce_update_order',
'woocommerce_order_refunded',
),
- 'order.deleted' => array(
+ 'order.deleted' => array(
'wp_trash_post',
'woocommerce_trash_order',
),
- 'order.restored' => array(
+ 'order.restored' => array(
'untrashed_post',
'woocommerce_untrash_order',
),
- 'product.created' => array(
+ 'product.created' => array(
'woocommerce_process_product_meta',
'woocommerce_new_product',
'woocommerce_new_product_variation',
),
- 'product.updated' => array(
+ 'product.updated' => array(
'woocommerce_process_product_meta',
'woocommerce_update_product',
'woocommerce_update_product_variation',
),
- 'product.deleted' => array(
+ 'product.deleted' => array(
'wp_trash_post',
),
- 'product.restored' => array(
+ 'product.restored' => array(
'untrashed_post',
),
+ 'product.published' => array(
+ 'woocommerce_product_published',
+ ),
);
$topic_hooks = apply_filters( 'woocommerce_webhook_topic_hooks', $topic_hooks, $this );
diff --git a/plugins/woocommerce/includes/wc-webhook-functions.php b/plugins/woocommerce/includes/wc-webhook-functions.php
index 62448b8b6c6..d16e16dd658 100644
--- a/plugins/woocommerce/includes/wc-webhook-functions.php
+++ b/plugins/woocommerce/includes/wc-webhook-functions.php
@@ -117,7 +117,13 @@ function wc_is_webhook_valid_topic( $topic ) {
}
$valid_resources = apply_filters( 'woocommerce_valid_webhook_resources', array( 'coupon', 'customer', 'order', 'product' ) );
- $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored' ) );
+ /**
+ * Filters the list of valid webhook events.
+ *
+ * @since 2.2.0
+ * @param array $valid_events Array of valid webhook events.
+ */
+ $valid_events = apply_filters( 'woocommerce_valid_webhook_events', array( 'created', 'updated', 'deleted', 'restored', 'published' ) );
if ( in_array( $data[0], $valid_resources, true ) && in_array( $data[1], $valid_events, true ) ) {
return true;
diff --git a/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php b/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php
index 46294873d65..f51e754d32b 100644
--- a/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php
+++ b/plugins/woocommerce/tests/legacy/unit-tests/webhooks/functions.php
@@ -30,6 +30,7 @@ class WC_Tests_Webhook_Functions extends WC_Unit_Test_Case {
array( true, wc_is_webhook_valid_topic( 'product.updated' ) ),
array( true, wc_is_webhook_valid_topic( 'product.deleted' ) ),
array( true, wc_is_webhook_valid_topic( 'product.restored' ) ),
+ array( true, wc_is_webhook_valid_topic( 'product.published' ) ),
array( true, wc_is_webhook_valid_topic( 'order.created' ) ),
array( true, wc_is_webhook_valid_topic( 'order.updated' ) ),
array( true, wc_is_webhook_valid_topic( 'order.deleted' ) ),
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php b/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php
index c170c643794..3384529bec7 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-post-data-test.php
@@ -45,4 +45,121 @@ class WC_Post_Data_Test extends \WC_Unit_Test_Case {
$order = wc_get_order( $order->get_id() );
$this->assertEmpty( $order->get_items() );
}
+
+ /**
+ * @testdox Should fire woocommerce_product_published when product transitions to publish status.
+ */
+ public function test_transition_post_status_fires_product_published_action(): void {
+ $product = \WC_Helper_Product::create_simple_product( false );
+ $product->set_status( 'draft' );
+ $product->save();
+
+ $published_ids = array();
+ $callback = function ( $product_id ) use ( &$published_ids ) {
+ $published_ids[] = $product_id;
+ };
+ add_action( 'woocommerce_product_published', $callback );
+
+ $post = get_post( $product->get_id() );
+ WC_Post_Data::transition_post_status( 'publish', 'draft', $post );
+
+ $this->assertContains( $product->get_id(), $published_ids, 'woocommerce_product_published should fire when product transitions to publish' );
+
+ remove_action( 'woocommerce_product_published', $callback );
+ $product->delete( true );
+ }
+
+ /**
+ * @testdox Should not fire woocommerce_product_published when product is already published and updated.
+ */
+ public function test_transition_post_status_does_not_fire_product_published_on_update(): void {
+ $product = \WC_Helper_Product::create_simple_product();
+
+ $published_ids = array();
+ $callback = function ( $product_id ) use ( &$published_ids ) {
+ $published_ids[] = $product_id;
+ };
+ add_action( 'woocommerce_product_published', $callback );
+
+ $post = get_post( $product->get_id() );
+ WC_Post_Data::transition_post_status( 'publish', 'publish', $post );
+
+ $this->assertEmpty( $published_ids, 'woocommerce_product_published should not fire when product is already published' );
+
+ remove_action( 'woocommerce_product_published', $callback );
+ $product->delete( true );
+ }
+
+ /**
+ * @testdox Should not fire woocommerce_product_published for non-product post types.
+ */
+ public function test_transition_post_status_does_not_fire_product_published_for_non_products(): void {
+ $post_id = wp_insert_post(
+ array(
+ 'post_title' => 'Test Post',
+ 'post_type' => 'post',
+ 'post_status' => 'draft',
+ )
+ );
+
+ $published_ids = array();
+ $callback = function ( $product_id ) use ( &$published_ids ) {
+ $published_ids[] = $product_id;
+ };
+ add_action( 'woocommerce_product_published', $callback );
+
+ $post = get_post( $post_id );
+ WC_Post_Data::transition_post_status( 'publish', 'draft', $post );
+
+ $this->assertEmpty( $published_ids, 'woocommerce_product_published should not fire for non-product post types' );
+
+ remove_action( 'woocommerce_product_published', $callback );
+ wp_delete_post( $post_id, true );
+ }
+
+ /**
+ * @testdox Should fire woocommerce_product_published when a product variation transitions to publish status.
+ */
+ public function test_transition_post_status_fires_product_published_action_for_variation(): void {
+ $variation = new WC_Product_Variation();
+ $variation->set_status( 'draft' );
+ $variation->save();
+
+ $published_ids = array();
+ $callback = function ( $product_id ) use ( &$published_ids ) {
+ $published_ids[] = $product_id;
+ };
+ add_action( 'woocommerce_product_published', $callback );
+
+ $post = get_post( $variation->get_id() );
+ WC_Post_Data::transition_post_status( 'publish', 'draft', $post );
+
+ $this->assertContains( $variation->get_id(), $published_ids, 'woocommerce_product_published should fire when a variation transitions to publish' );
+
+ remove_action( 'woocommerce_product_published', $callback );
+ $variation->delete( true );
+ }
+
+ /**
+ * @testdox Should fire woocommerce_product_published when a scheduled product transitions from future to publish.
+ */
+ public function test_transition_post_status_fires_product_published_action_on_scheduled_publish(): void {
+ $product = \WC_Helper_Product::create_simple_product( false );
+ $product->set_status( 'future' );
+ $product->save();
+
+ $published_ids = array();
+ $callback = function ( $product_id ) use ( &$published_ids ) {
+ $published_ids[] = $product_id;
+ };
+ add_action( 'woocommerce_product_published', $callback );
+
+ $post = get_post( $product->get_id() );
+ WC_Post_Data::transition_post_status( 'publish', 'future', $post );
+
+ $this->assertContains( $product->get_id(), $published_ids, 'woocommerce_product_published should fire when a scheduled product transitions from future to publish' );
+
+ remove_action( 'woocommerce_product_published', $callback );
+ $product->delete( true );
+ }
}