Commit 7a9e8d454d9 for woocommerce
commit 7a9e8d454d9cd37b0ccdedc4141eb220e13640c9
Author: Vladimir Reznichenko <kalessil@gmail.com>
Date: Wed Jun 24 12:22:27 2026 +0200
[Performance] Centralize wp_count_posts invocations towards products (#65894)
Groundwork for optimizing admin performance with big product catalogs: centralize wp_count_posts calls via a new utility class. Reduces the number of integration points in subsequent optimization steps.
diff --git a/plugins/woocommerce/changelog/performance-centralize-wp_count_posts-invocation b/plugins/woocommerce/changelog/performance-centralize-wp_count_posts-invocation
new file mode 100644
index 00000000000..f3d5d9813d5
--- /dev/null
+++ b/plugins/woocommerce/changelog/performance-centralize-wp_count_posts-invocation
@@ -0,0 +1,4 @@
+Significance: patch
+Type: performance
+
+Centralize wp_count_posts invocations towards products.
diff --git a/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php b/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php
index 4a0c93d6274..6939d4b8ce0 100644
--- a/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php
+++ b/plugins/woocommerce/includes/admin/list-tables/abstract-class-wc-admin-list-table.php
@@ -63,10 +63,9 @@ abstract class WC_Admin_List_Table {
if ( $post_type === $this->list_table_type && 'bottom' === $which ) {
$counts = (array) wp_count_posts( $post_type );
- unset( $counts['auto-draft'] );
- $count = array_sum( $counts );
+ $count = array_sum( $counts ) - ( $counts['auto-draft'] ?? 0 );
- if ( 0 < $count ) {
+ if ( $count > 0 ) {
return;
}
diff --git a/plugins/woocommerce/includes/class-wc-install.php b/plugins/woocommerce/includes/class-wc-install.php
index e2fba02952b..8ae2a7eb9ca 100644
--- a/plugins/woocommerce/includes/class-wc-install.php
+++ b/plugins/woocommerce/includes/class-wc-install.php
@@ -21,6 +21,7 @@ use Automattic\WooCommerce\Admin\API\Reports\Orders\Stats\DataStore as OrdersSta
use Automattic\WooCommerce\Utilities\FeaturesUtil;
use Automattic\WooCommerce\Internal\Utilities\DatabaseUtil;
use Automattic\WooCommerce\Internal\WCCom\ConnectionHelper as WCConnectionHelper;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Utilities\{ OrderUtil, PluginUtil };
defined( 'ABSPATH' ) || exit;
@@ -851,7 +852,7 @@ class WC_Install {
return is_null( get_option( 'woocommerce_version', null ) )
|| (
-1 === wc_get_page_id( 'shop' )
- && 0 === array_sum( (array) wp_count_posts( 'product' ) )
+ && 0 === array_sum( ProductUtil::get_counts_for_type( 'product' ) )
);
}
diff --git a/plugins/woocommerce/includes/class-wc-tracker.php b/plugins/woocommerce/includes/class-wc-tracker.php
index 432e1d3c966..9930b2eaf19 100644
--- a/plugins/woocommerce/includes/class-wc-tracker.php
+++ b/plugins/woocommerce/includes/class-wc-tracker.php
@@ -11,14 +11,16 @@
*/
use Automattic\Jetpack\Constants;
+use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
+use Automattic\WooCommerce\Blocks\Package;
+use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Internal\Admin\EmailImprovements\EmailImprovements;
use Automattic\WooCommerce\Internal\CLI\Migrator\Core\MigratorTracker;
use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;
-use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };
use Automattic\WooCommerce\Internal\Utilities\BlocksUtil;
use Automattic\WooCommerce\Proxies\LegacyProxy;
-use Automattic\WooCommerce\Blocks\Package;
-use Automattic\WooCommerce\Blocks\Domain\Services\CheckoutFields;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
+use Automattic\WooCommerce\Utilities\{ FeaturesUtil, OrderUtil, PluginUtil };
defined( 'ABSPATH' ) || exit;
@@ -488,9 +490,8 @@ class WC_Tracker {
* @return array
*/
public static function get_product_counts() {
- $product_count = array();
- $product_count_data = wp_count_posts( 'product' );
- $product_count['total'] = $product_count_data->publish;
+ $product_count_data = ProductUtil::get_counts_for_type( 'product' );
+ $product_count = array( 'total' => $product_count_data[ ProductStatus::PUBLISH ] ?? 0 );
$product_statuses = get_terms( 'product_type', array( 'hide_empty' => 0 ) );
foreach ( $product_statuses as $product_status ) {
diff --git a/plugins/woocommerce/src/Admin/API/OnboardingTasks.php b/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
index 8d37abb2a1c..3b5c75e31fb 100644
--- a/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
+++ b/plugins/woocommerce/src/Admin/API/OnboardingTasks.php
@@ -7,12 +7,13 @@
namespace Automattic\WooCommerce\Admin\API;
+use Automattic\WooCommerce\Admin\Features\Features;
+use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
+use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingIndustries;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
-use Automattic\WooCommerce\Admin\Features\Features;
-use Automattic\WooCommerce\Admin\Features\OnboardingTasks\TaskLists;
-use Automattic\WooCommerce\Admin\Features\OnboardingTasks\DeprecatedExtendedTask;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
defined( 'ABSPATH' ) || exit;
@@ -512,8 +513,8 @@ class OnboardingTasks extends \WC_REST_Data_Controller {
* @return string Template contents.
*/
private static function get_homepage_template( $post_id ) {
- $products = wp_count_posts( 'product' );
- if ( $products->publish >= 4 ) {
+ $products = ProductUtil::get_counts_for_type( 'product' );
+ if ( ( $products[ ProductStatus::PUBLISH ] ?? 0 ) >= 4 ) {
$images = self::sideload_homepage_images( $post_id, 1 );
$image_1 = ! empty( $images[0] ) ? $images[0] : '';
$template = self::get_homepage_cover_block( $image_1 ) . '
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
index edb3b05320b..ff7d22945da 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/Products.php
@@ -4,8 +4,9 @@ namespace Automattic\WooCommerce\Admin\Features\OnboardingTasks\Tasks;
use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
use Automattic\WooCommerce\Enums\ProductStatus;
-use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
use Automattic\WooCommerce\Internal\Admin\Onboarding\OnboardingProfile;
+use Automattic\WooCommerce\Internal\Admin\WCAdminAssets;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
/**
* Products Task
@@ -339,10 +340,8 @@ class Products extends Task {
public function maybe_redirect_to_add_product_tasklist() {
$screen = get_current_screen();
if ( $screen && 'edit' === $screen->base && 'product' === $screen->post_type ) {
- // wp_count_posts is cached.
- $counts = (array) wp_count_posts( $screen->post_type );
- unset( $counts['auto-draft'] );
- $count = array_sum( $counts );
+ $counts = ProductUtil::get_counts_for_type( 'product' );
+ $count = array_sum( $counts ) - ( $counts[ ProductStatus::AUTO_DRAFT ] ?? 0 );
if ( $count > 0 ) {
return;
}
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
index 336666503de..404da7ffe83 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/AbstractBlock.php
@@ -1,12 +1,13 @@
<?php
namespace Automattic\WooCommerce\Blocks\BlockTypes;
-use WP_Block;
-use Automattic\WooCommerce\Blocks\Package;
-use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
+use Automattic\WooCommerce\Admin\Features\Features;
use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
+use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
-use Automattic\WooCommerce\Admin\Features\Features;
+use Automattic\WooCommerce\Enums\ProductStatus;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
+use WP_Block;
/**
* AbstractBlock class.
@@ -443,8 +444,8 @@ abstract class AbstractBlock {
'wordCountType' => _x( 'words', 'Word count type. Do not translate!', 'woocommerce' ),
];
if ( is_admin() && ! WC()->is_rest_api_request() ) {
- $product_counts = wp_count_posts( 'product' );
- $published_products = isset( $product_counts->publish ) ? $product_counts->publish : 0;
+ $product_counts = ProductUtil::get_counts_for_type( 'product' );
+ $published_products = $product_counts[ ProductStatus::PUBLISH ] ?? 0;
$wc_blocks_config = array_merge(
$wc_blocks_config,
[
diff --git a/plugins/woocommerce/src/Internal/Admin/SiteHealth.php b/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
index afb182a24f6..ff8ee30b9e6 100644
--- a/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
+++ b/plugins/woocommerce/src/Internal/Admin/SiteHealth.php
@@ -8,7 +8,9 @@ declare( strict_types = 1 );
namespace Automattic\WooCommerce\Internal\Admin;
use Automattic\WooCommerce\Enums\DefaultCustomerAddress;
+use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Internal\DataStores\Orders\DataSynchronizer;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use Automattic\WooCommerce\Utilities\OrderUtil;
use WC_Admin_Notices;
use WC_Admin_Status;
@@ -680,9 +682,9 @@ class SiteHealth {
return false;
}
- $product_count = wp_count_posts( 'product' );
+ $product_count = ProductUtil::get_counts_for_type( 'product' );
- return $product_count->publish > 0 && 0 === wc_get_shipping_method_count();
+ return ( $product_count[ ProductStatus::PUBLISH ] ?? 0 ) > 0 && 0 === wc_get_shipping_method_count();
}
/**
diff --git a/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php b/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php
index 158ed088908..8463ffca414 100644
--- a/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php
+++ b/plugins/woocommerce/src/Internal/ProductAttributesLookup/CLIRunner.php
@@ -2,6 +2,8 @@
namespace Automattic\WooCommerce\Internal\ProductAttributesLookup;
+use Automattic\WooCommerce\Enums\ProductStatus;
+use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
use WP_CLI;
/**
@@ -382,10 +384,8 @@ class CLIRunner {
$was_enabled = 'yes' === get_option( 'woocommerce_attribute_lookup_enabled' );
- // phpcs:ignore Generic.Commenting.Todo.TaskFound
- // TODO: adjust for non-CPT datastores (this is only used for the progress bar, though).
- $products_count = wp_count_posts( 'product' );
- $products_count = intval( $products_count->publish ) + intval( $products_count->pending ) + intval( $products_count->draft );
+ $products_count = ProductUtil::get_counts_for_type( 'product' );
+ $products_count = ( $products_count[ ProductStatus::PUBLISH ] ?? 0 ) + ( $products_count[ ProductStatus::PENDING ] ?? 0 ) + ( $products_count[ ProductStatus::DRAFT ] ?? 0 );
if ( ! $this->lookup_data_store->regeneration_is_in_progress() || array_key_exists( 'from-scratch', $assoc_args ) ) {
$info = $this->get_lookup_table_info();
diff --git a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
index c925cf0eeb6..503504b151e 100644
--- a/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
+++ b/plugins/woocommerce/src/Internal/Utilities/ProductUtil.php
@@ -127,4 +127,17 @@ class ProductUtil {
_prime_post_caches( $image_ids );
}
}
+
+ /**
+ * Counts per-status number of products.
+ *
+ * @since 11.0.0
+ *
+ * @param string $post_type Post type (e.g. 'product', 'product_variation').
+ * @return array<string,int>
+ */
+ public static function get_counts_for_type( string $post_type ): array {
+ // Performance note: integration point for upcoming persistent counters solution.
+ return array_map( 'intval', (array) wp_count_posts( $post_type ) );
+ }
}
diff --git a/plugins/woocommerce/src/Utilities/OrderUtil.php b/plugins/woocommerce/src/Utilities/OrderUtil.php
index e2d5a6811ea..dbe798c24e8 100644
--- a/plugins/woocommerce/src/Utilities/OrderUtil.php
+++ b/plugins/woocommerce/src/Utilities/OrderUtil.php
@@ -231,18 +231,15 @@ final class OrderUtil {
if ( null === $count_per_status ) {
if ( self::custom_orders_table_usage_is_enabled() ) {
- // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared
- $results = $wpdb->get_results(
+ $results = $wpdb->get_results(
$wpdb->prepare(
- 'SELECT `status`, COUNT(*) AS `count` FROM ' . self::get_table_for_orders() . ' WHERE `type` = %s GROUP BY `status`',
+ 'SELECT status, COUNT(*) AS count FROM %i WHERE type = %s GROUP BY status',
+ self::get_table_for_orders(),
$order_type
),
ARRAY_A
);
- // phpcs:enable
-
$count_per_status = array_map( 'absint', array_column( $results, 'count', 'status' ) );
-
} else {
$count_per_status = (array) wp_count_posts( $order_type );
}
diff --git a/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php b/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php
index ab28f843873..7be7ceb010a 100644
--- a/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/Utilities/ProductUtilTest.php
@@ -4,13 +4,35 @@ declare(strict_types=1);
namespace Automattic\WooCommerce\Tests\Internal\Utilities;
-use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
+use Automattic\WooCommerce\Enums\ProductStatus;
use Automattic\WooCommerce\Internal\Utilities\ProductUtil;
+use Automattic\WooCommerce\RestApi\UnitTests\Helpers\ProductHelper;
/**
* Tests for the internal ProductUtil class.
*/
class ProductUtilTest extends \WC_Unit_Test_Case {
+ /**
+ * @testdox `get_counts_for_type` returns per-status counts for the given post type.
+ */
+ public function test_get_counts_for_type_returns_per_status_counts(): void {
+ $before = ProductUtil::get_counts_for_type( 'product' );
+
+ $published = \WC_Helper_Product::create_simple_product();
+ $draft = \WC_Helper_Product::create_simple_product( true, array( 'status' => ProductStatus::DRAFT ) );
+ $pending = \WC_Helper_Product::create_simple_product( true, array( 'status' => ProductStatus::PENDING ) );
+
+ $after = ProductUtil::get_counts_for_type( 'product' );
+
+ $this->assertSame( ( $before[ ProductStatus::PUBLISH ] ?? 0 ) + 1, $after[ ProductStatus::PUBLISH ] );
+ $this->assertSame( ( $before[ ProductStatus::DRAFT ] ?? 0 ) + 1, $after[ ProductStatus::DRAFT ] );
+ $this->assertSame( ( $before[ ProductStatus::PENDING ] ?? 0 ) + 1, $after[ ProductStatus::PENDING ] );
+
+ $published->delete( true );
+ $draft->delete( true );
+ $pending->delete( true );
+ }
+
/**
* @testdox delete_product_transients_for_products deletes fixed-name transients once and fires hooks once per product.
*/