Lokasi ngalangkungan proxy:   [ UP ]  
[Ngawartoskeun bug]   [Panyetelan cookie]                
Skip to content

Cache Store API products last modified timestamp#63228

Merged
kraftbj merged 2 commits intotrunkfrom
WOOPLUG-6264/cache-products-last-modified
Feb 19, 2026
Merged

Cache Store API products last modified timestamp#63228
kraftbj merged 2 commits intotrunkfrom
WOOPLUG-6264/cache-products-last-modified

Conversation

@prettyboymp
Copy link
Copy Markdown
Contributor

@prettyboymp prettyboymp commented Feb 10, 2026

Changes proposed in this Pull Request:

ProductQuery::get_last_modified() runs a SELECT MAX(post_modified_gmt) query against the posts table on every Store API /wc/store/v1/products request to populate the Last-Modified response header. This adds unnecessary database overhead on every request since products change infrequently relative to how often they are read.

This PR caches the result in the object cache (last_modified key in wc_products group) and invalidates it via the clean_post_cache hook in WC_Post_Data. On cache hit, the DB query is skipped entirely. On cache miss (first request or after a product change), the query runs and the result is cached.

The implementation uses wp_cache_delete() / wp_cache_get() / wp_cache_set() rather than WordPress core's wp_cache_*_last_changed() pattern. Core's last_changed pattern auto-seeds with the current time on cache miss, which is fine for opaque cache-key salts but would cause incorrect behavior here — the value is exposed to clients via the Last-Modified header, and auto-seeding with "now" would force all clients to unnecessarily invalidate their local caches. Instead, on a cache miss we fall back to the database for the real last modification time.

Also normalizes the Last-Modified header value to RFC 7232 HTTP-date format (previously a raw unix timestamp).

Closes WOOPLUG-6264 / #63164

How to test the changes in this Pull Request:

  1. Open a WooCommerce store with products and navigate to a page that uses the Store API products endpoint (e.g., a page with a Products block, or call /wp-json/wc/store/v1/products directly).
  2. Check the response headers — the Last-Modified header should now be in HTTP-date format, e.g., Wed, 09 Oct 2024 08:28:00 GMT.
  3. Enable query logging (e.g., Query Monitor plugin) and reload the page twice. On the second load, confirm the SELECT MAX(post_modified_gmt) query is no longer present.
  4. Edit any product in WP Admin (e.g., change the title) and save.
  5. Reload the products endpoint and confirm the Last-Modified header value has updated to reflect the change. The DB query should run once on this first request after the change, then be cached again.
  6. Create a new product, then check the endpoint — Last-Modified should update.
  7. Trash a product, then check the endpoint — Last-Modified should update.
  8. Edit a non-product post (e.g., a page) and confirm the Last-Modified header on the products endpoint does NOT change.

Testing that has already taken place:

  • 8 unit tests added and passing (pnpm test:php:env -- --filter ProductQueryTest)
  • Covers: HTTP-date format, caching behavior, cache invalidation on product/variation/non-product changes, DB fallback on cache miss

Milestone

Changelog entry

  • This Pull Request does not require a changelog entry. (Comment required below)
Changelog Entry Comment

Comment

Changelog entry is included in this PR at plugins/woocommerce/changelog/wooplug-6264-cache-products-last-modified.

…r request

The ProductQuery::get_last_modified() method ran a MAX() query against
the posts table on every Store API /products request. This caches the
result in the object cache and invalidates it via the clean_post_cache
hook in WC_Post_Data, eliminating the query on subsequent requests.

The cached value uses a dedicated key/group rather than WordPress core's
wp_cache_*_last_changed() pattern because the value is exposed to clients
via the Last-Modified header. Core's pattern auto-seeds with the current
time on cache miss, which would force unnecessary client-side cache
invalidation. Instead, on a miss we fall back to the DB for the real
last modification time.

Also normalizes the Last-Modified header to RFC 7232 HTTP-date format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@prettyboymp prettyboymp requested a review from a team as a code owner February 10, 2026 14:50
@prettyboymp prettyboymp requested review from opr and removed request for a team February 10, 2026 14:50
@woocommercebot woocommercebot requested review from a team and ObliviousHarmony and removed request for a team February 10, 2026 14:50
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Feb 10, 2026

Testing Guidelines

Hi @opr @ObliviousHarmony @Copilot @woocommerce/flux,

Apart from reviewing the code changes, please make sure to review the testing instructions (Guide) and verify that relevant tests (E2E, Unit, Integration, etc.) have been added or updated as needed.

Reminder: PR reviewers are required to document testing performed. This includes:

  • 🖼️ Screenshots or screen recordings.
  • 📝 List of functionality tested / steps followed.
  • 🌐 Site details (environment attributes such as hosting type, plugins, theme, store size, store age, and relevant settings).
  • 🔍 Any analysis performed, such as assessing potential impacts on environment attributes and other plugins, conducting performance profiling, or using LLM/AI-based analysis.

⚠️ Within the testing details you provide, please ensure that no sensitive information (such as API keys, passwords, user data, etc.) is included in this public issue.

@github-actions github-actions Bot added the plugin: woocommerce Issues related to the WooCommerce Core plugin. label Feb 10, 2026
@prettyboymp prettyboymp marked this pull request as draft February 10, 2026 14:52
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 10, 2026

📝 Walkthrough

Walkthrough

Adds caching for the products "last modified" value in the WordPress object cache and a cache invalidation hook tied to post cache cleanup; ProductQuery now returns an HTTP-date formatted last-modified string (or null) and seeds/reads that value from cache.

Changes

Cohort / File(s) Summary
Cache Invalidation Hook
plugins/woocommerce/includes/class-wc-post-data.php
Added public static function invalidate_products_last_modified( $post_id, $post ) and hooked it into clean_post_cache to delete the last_modified key from the wc_products cache group for product and product_variation posts.
Query Caching and Format
plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php
get_last_modified() now uses wp_cache_get/wp_cache_set with the wc_products group, returns an HTTP-date formatted string (RFC 7232) or null instead of an int timestamp, and only queries DB on cache miss. Docblock updated to describe caching/invalidation.
Tests
plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/ProductQueryTest.php
Added PHPUnit tests covering empty-product null result, HTTP-date format, cache population and reuse, invalidation on product/variation changes, non-product updates not invalidating, and reseeding after invalidation.
Changelog
plugins/woocommerce/changelog/wooplug-6264-cache-products-last-modified
Added changelog entry noting patch-level performance change that caches products last-modified in object cache.
Static Analysis Baseline
plugins/woocommerce/phpstan-baseline.neon
Removed two PHPStan baseline entries referencing prior type expectations for get_last_modified() and a header parameter message.

Sequence Diagrams

sequenceDiagram
    participant Client
    participant Cache as Object Cache (wc_products)
    participant DB as Database
    participant Invalidator as WC_Post_Data (clean_post_cache)

    Note over Client,Cache: Initial request (cache miss)
    Client->>Cache: wp_cache_get('last_modified')
    Cache-->>Client: null
    Client->>DB: SELECT MAX(post_modified_gmt) WHERE type IN (product, product_variation)
    DB-->>Client: post_modified_gmt
    Client->>Cache: wp_cache_set('last_modified', HTTP-date)
    Cache-->>Client: stored

    Note over Client,Cache: Subsequent request (cache hit)
    Client->>Cache: wp_cache_get('last_modified')
    Cache-->>Client: HTTP-date (no DB query)

    Note over Invalidator,Cache: Product updated -> cache invalidated
    Invalidator->>Cache: wp_cache_delete('last_modified')
    Cache-->>Invalidator: deleted

    Note over Client,DB: Next request after invalidation (reseeding)
    Client->>Cache: wp_cache_get('last_modified')
    Cache-->>Client: null
    Client->>DB: SELECT MAX(post_modified_gmt) ...
    DB-->>Client: updated post_modified_gmt
    Client->>Cache: wp_cache_set('last_modified', new HTTP-date)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The pull request title accurately describes the main objective: caching the Store API products last modified timestamp to improve performance.
Description check ✅ Passed The pull request description is detailed and directly related to the changeset, explaining the problem, solution, caching strategy, and testing approach.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch WOOPLUG-6264/cache-products-last-modified

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/ProductQueryTest.php (2)

44-52: Redundant restore query on lines 45-47 — the REPLACE produces empty strings.

REPLACE(post_type, '_tmp_hidden', '') sets post_type to an empty string, not the original type. The per-row loop on lines 50-52 is what actually restores the correct values, making the bulk UPDATE on lines 45-47 dead code that's also misleading.

Consider removing it:

♻️ Suggested cleanup
-		// Restore original posts.
-		$wpdb->query(
-			"UPDATE {$wpdb->posts} SET post_type = REPLACE(post_type, '_tmp_hidden', '') WHERE post_type = '_tmp_hidden'"
-		);
-
-		// Restore correct post types from the saved data.
+		// Restore original post types from the saved data.
 		foreach ( $original_posts as $post ) {
 			$wpdb->update( $wpdb->posts, array( 'post_type' => $post->post_type ), array( 'ID' => $post->ID ) );
 		}

196-216: $first_result is unused — assign inline or drop the variable.

The variable exists only for readability but triggers a static analysis warning. Since it's not asserted against, a bare call suffices.

♻️ Suggested fix
 		// Seed the cache.
-		$first_result = $this->product_query->get_last_modified();
+		$this->product_query->get_last_modified();

Tip

Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In
`@plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/ProductQueryTest.php`:
- Line 8: The test class ProductQueryTest currently extends
Yoast\PHPUnitPolyfills\TestCases\TestCase and declares a protected setUp();
change the class to extend WC_Unit_Test_Case (so WooCommerce test utilities and
cleanup run) and change the setUp() method signature to public function setUp():
void; also add or update the class docblock to reflect it's a WooCommerce unit
test (mention WC_Unit_Test_Case) to match project conventions.
🧹 Nitpick comments (4)
plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php (2)

337-353: Add a @since tag documenting the return-type change.

The method's return type changed from int (Unix timestamp) to string|null (HTTP-date). This is a public method on a non-internal class, so the signature change should be annotated with @since 10.6.0 to signal the breaking change to extension developers.

📝 Proposed docblock addition
  * `@return` string|null HTTP-date formatted string, or null if no products exist.
+ * `@since` 10.6.0 Return type changed from int (Unix timestamp) to string|null (HTTP-date).
  */

360-366: null return is not cached — repeated DB queries when no products exist.

When $last_modified_gmt is falsy (no products), the method returns null without caching, so every subsequent call will re-query the DB. For most stores this is fine since having zero products is an edge case. If this path ever becomes hot, consider caching a sentinel value.

plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/ProductQueryTest.php (2)

32-56: Fragile DB manipulation — consider wrapping in try/finally for safety.

The test temporarily renames all product post types in the DB. If an assertion or error occurs between lines 39 and 53, the DB is left in a corrupt state for subsequent tests. Wrapping the assert + restore in a try/finally would be safer.

Also, the REPLACE query on line 47 is redundant — it sets post_type to an empty string, and lines 51–53 overwrite that with the correct values anyway. Consider removing lines 46–48 entirely.

♻️ Proposed safer cleanup
 		$wpdb->query(
 			"UPDATE {$wpdb->posts} SET post_type = '_tmp_hidden' WHERE post_type IN ('product', 'product_variation')"
 		);

-		$result = $this->product_query->get_last_modified();
-
-		// Restore original posts.
-		$wpdb->query(
-			"UPDATE {$wpdb->posts} SET post_type = REPLACE(post_type, '_tmp_hidden', '') WHERE post_type = '_tmp_hidden'"
-		);
-
-		// Restore correct post types from the saved data.
-		foreach ( $original_posts as $post ) {
-			$wpdb->update( $wpdb->posts, array( 'post_type' => $post->post_type ), array( 'ID' => $post->ID ) );
+		try {
+			$result = $this->product_query->get_last_modified();
+		} finally {
+			foreach ( $original_posts as $post ) {
+				$wpdb->update( $wpdb->posts, array( 'post_type' => $post->post_type ), array( 'ID' => $post->ID ) );
+			}
 		}

 		$this->assertNull( $result );

166-181: Minor: unused $first_result variable.

The call on line 171 is needed to seed the cache, but the assignment to $first_result is unused. This can be cleaned up to avoid the PHPMD warning.

🧹 Quick fix
-		$first_result = $this->product_query->get_last_modified();
+		$this->product_query->get_last_modified();

Comment thread plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/ProductQueryTest.php Outdated
@prettyboymp prettyboymp force-pushed the WOOPLUG-6264/cache-products-last-modified branch 4 times, most recently from f8bd3ca to 632ed87 Compare February 10, 2026 16:10
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@prettyboymp prettyboymp force-pushed the WOOPLUG-6264/cache-products-last-modified branch from 632ed87 to 9d19429 Compare February 10, 2026 18:54
@prettyboymp prettyboymp marked this pull request as ready for review February 10, 2026 19:37
@woocommercebot woocommercebot requested a review from a team February 10, 2026 19:38
@prettyboymp prettyboymp added this to the 10.6.0 milestone Feb 10, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Test using WordPress Playground

The changes in this pull request can be previewed and tested using a WordPress Playground instance.
WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Test this pull request with WordPress Playground.

Note that this URL is valid for 30 days from when this comment was last updated. You can update it by closing/reopening the PR or pushing a new commit.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements caching for the Store API products' last modified timestamp to reduce database overhead. Previously, every /wc/store/v1/products request executed a SELECT MAX(post_modified_gmt) query to populate the Last-Modified response header. The new implementation caches this value in the object cache and invalidates it when products are modified, eliminating unnecessary database queries on cache hits.

Changes:

  • Added object cache support for ProductQuery::get_last_modified() with explicit invalidation on product changes
  • Normalized the Last-Modified header format from Unix timestamp to RFC 7232 HTTP-date format
  • Added comprehensive unit tests covering caching behavior, cache invalidation, and edge cases

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php Implements caching logic with database fallback on cache miss and converts return format to HTTP-date string
plugins/woocommerce/includes/class-wc-post-data.php Adds cache invalidation hook that clears the cached timestamp when product posts are modified
plugins/woocommerce/tests/php/src/Blocks/StoreApi/Utilities/ProductQueryTest.php Adds 8 unit tests covering HTTP-date format, caching, invalidation for products/variations, and non-product changes
plugins/woocommerce/phpstan-baseline.neon Removes two resolved PHPStan errors related to the return type and header value type changes
plugins/woocommerce/changelog/wooplug-6264-cache-products-last-modified Documents the performance improvement in the changelog

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread plugins/woocommerce/src/StoreApi/Utilities/ProductQuery.php
@kraftbj kraftbj enabled auto-merge (squash) February 19, 2026 23:08
@kraftbj kraftbj merged commit 599ca7d into trunk Feb 19, 2026
90 of 91 checks passed
@kraftbj kraftbj deleted the WOOPLUG-6264/cache-products-last-modified branch February 19, 2026 23:22
samnajian pushed a commit that referenced this pull request Mar 11, 2026
* Cache Store API products last modified timestamp to avoid DB query per request

The ProductQuery::get_last_modified() method ran a MAX() query against
the posts table on every Store API /products request. This caches the
result in the object cache and invalidates it via the clean_post_cache
hook in WC_Post_Data, eliminating the query on subsequent requests.

The cached value uses a dedicated key/group rather than WordPress core's
wp_cache_*_last_changed() pattern because the value is exposed to clients
via the Last-Modified header. Core's pattern auto-seeds with the current
time on cache miss, which would force unnecessary client-side cache
invalidation. Instead, on a miss we fall back to the DB for the real
last modification time.

Also normalizes the Last-Modified header to RFC 7232 HTTP-date format.

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

* Add changelog entry for products last modified caching

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin: woocommerce Issues related to the WooCommerce Core plugin.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants