Skip to content

Conversation

@Konamiman
Copy link
Contributor

@Konamiman Konamiman commented Nov 3, 2025

Changes proposed in this Pull Request:

The problem

WC_Product_Variable_Data_Store_CPT uses transients to cache information about the prices of all the variations for variable products. Since there are filters that can influence these prices, the generation of the hash key for the transient for a given product (get_price_hash method) includes information about the current usages of these filters: if add_filters are added or removed for those, the next hash calculation will yield a different result, and this will effectively invalidate the cached prices.

However, while this approach is valid, the implementation had a bug: when retrieving the information for a given filter usage (priority and callback), the entire callback was being serialized. For callbacks that are class methods (which is the most common case), this meant that the entire object containing the method was being serialized, and any change in a public property of that object meant a different serialization, thus a different hash, thus a prices transient that was effectively noy used.

The solution

This pull request:

  1. Introduces a new CallbackUtil class with a get_callback_signature method that will generate a proper signature string for any callback, irrespective of any state in the involved objects; and a get_hook_callback_signatures method that given a hook name will return an array of priority => [callback signature1, callback signature 2...]
  2. Modifies WC_Product_Variable_Data_Store_CPT::get_price_hash so that it uses CallbackUtil::get_hook_callback_signatures for each of the relevant hooks, and then generates the hash based on that information.

Closes #37547.

How to test the changes in this Pull Request:

  1. Make sure that your store has at least one variable product (preferably with a large number of variations), take note of the product id.

  2. Add the following code snippet to your server:

class WooPriceHashTester
{
    public $random;

    public function __construct()
    {
        $this->random = microtime(true);
        add_filter('woocommerce_variation_prices_sale_price', [$this, 'product_sale_price'], 10, 3);
    }

    public function product_sale_price($price, $variation, $product)
    {
        return $price;
    }

	public static function test_price_hash($product_id, bool $delete_transients_first=false) {
		if($delete_transients_first) {
			delete_transient('wc_var_prices_' . $product_id);
		}

		$datastore = new class extends WC_Product_Variable_Data_Store_CPT {
			public function get_price_hash( &$product, $for_display = false ) {
				return parent::get_price_hash( $product, $for_display );
			}
		};

		$tester = new self();
		$product = wc_get_product( $product_id );
		$hash1 = $datastore->get_price_hash( $product );

		$tester->random++;
		$hash2 = $datastore->get_price_hash( $product );

		echo "Hash 1: $hash1\n";
		echo "Hash 2: $hash2\n";

		$time1 = microtime(true);
		echo "Reading price data... ";
		$datastore->read_price_data($product);
		$time2 = microtime(true);
		echo " done in " . round(($time2-$time1)*1000, 2) . " ms\n";
	}
}
  1. Run the following in your terminal:
wp eval 'WooPriceHashTester::test_price_hash(<product id>, true);'

The two hashes printed should be the same ones.

  1. Now run the following command but passing false to the function. You should get the exact same hashes and this time it should have finished much faster.

  2. Repeat 3 and 4 a few times, the hashes should always be the same ones.

Without these changes you would get a different hash every time, even within the same run of the command.

  1. Go to the shop and verify that you can still see the product with the correct prices, place an order including a couple of variations, and verify that everything is correct in the generated order.

Changelog entry

  • Automatically create a changelog entry from the details below.
  • This Pull Request does not require a changelog entry. (Comment required below)
Changelog Entry Details

Significance

  • Patch
  • Minor
  • Major

Type

  • Fix - Fixes an existing bug
  • Add - Adds functionality
  • Update - Update existing functionality
  • Dev - Development related task
  • Tweak - A minor adjustment to the codebase
  • Performance - Address performance issues
  • Enhancement - Improvement to existing functionality

Message

Changelog Entry Comment

Comment

@github-actions github-actions bot added the plugin: woocommerce Issues related to the WooCommerce Core plugin. label Nov 3, 2025
@Konamiman Konamiman marked this pull request as ready for review November 3, 2025 12:33
@woocommercebot woocommercebot requested review from a team and joshuatf and removed request for a team November 3, 2025 12:33
@github-actions
Copy link
Contributor

github-actions bot commented Nov 3, 2025

Testing Guidelines

Hi @joshuatf ,

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.

…cture

WordPress stores callbacks in WP_Hook with an intermediate associative key
between the priority level and callback data. This commit adds array_values()
to strip those keys and return clean numerically-indexed arrays.

Also adds safety check for corrupted filter data.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 3, 2025

📝 Walkthrough

Walkthrough

Adds a final utility class CallbackUtil to produce stable callback signatures and enumerate hook callbacks by priority, updates the variable product data store to use it when building variation price hashes, and adds PHPUnit tests plus helper classes for callback scenarios.

Changes

Cohort / File(s) Summary
New Utility Class
plugins/woocommerce/src/Utilities/CallbackUtil.php
Adds Automattic\WooCommerce\Utilities\CallbackUtil (final) with static methods get_callback_signature($callback): string and get_hook_callback_signatures(string $hook_name): array to produce stable, human-readable callback signatures and enumerate hook callbacks by priority.
Updated Data Store
plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php
Replaces direct inspection of global $wp_filter with CallbackUtil::get_hook_callback_signatures() for hooks woocommerce_variation_prices_price, woocommerce_variation_prices_regular_price, and woocommerce_variation_prices_sale_price when building variation price hashes; imports CallbackUtil.
Tests
plugins/woocommerce/tests/php/src/Utilities/CallbackUtilTest.php
Adds PHPUnit tests validating get_callback_signature() and get_hook_callback_signatures() for functions, instance/static methods, closures, invokable objects, hook registrations, and corrupted wp_filter cases; checks closure identity and signature consistency.
Test Helpers
plugins/woocommerce/tests/php/src/Utilities/CallbackUtilTestClasses/*
.../DummyCallbackClass.php, .../AnotherDummyCallbackClass.php, .../DummyCallbackClassWithDynamicProps.php, .../DummyInvokableClass.php
Adds test helper classes providing instance/static methods, dynamic properties, and __invoke to exercise callback-signature behaviors used by the test suite.

Sequence Diagram(s)

sequenceDiagram
    participant VarStore as Variable Product Data Store
    participant CallbackUtil
    participant WPFilter as global $wp_filter

    VarStore->>CallbackUtil: get_hook_callback_signatures(hook_name)
    CallbackUtil->>WPFilter: read $wp_filter[hook_name]
    WPFilter-->>CallbackUtil: callbacks grouped by priority

    loop per priority
        loop per callback
            CallbackUtil->>CallbackUtil: get_callback_signature(callback)
            alt function name
                Note right of CallbackUtil: "functionName"
            else class method
                Note right of CallbackUtil: "ClassName::method"
            else closure
                Note right of CallbackUtil: "Closure@hash"
            else invokable object
                Note right of CallbackUtil: "ClassName::__invoke"
            else
                Note right of CallbackUtil: "serialize(callback)"
            end
            CallbackUtil-->>VarStore: signatures grouped by priority
        end
    end

    VarStore->>VarStore: include signatures in variation price hash
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Inspect get_callback_signature() for edge-case handling (array targets, closures, invokable objects, serialization fallback).
  • Verify get_hook_callback_signatures() normalizes priority ordering and handles corrupted $wp_filter entries safely.
  • Confirm integration in class-wc-product-variable-data-store-cpt.php produces deterministic variation price hashes.
  • Run and review the new PHPUnit tests and helper classes for potential flakiness (dynamic properties, closure identity).

Pre-merge checks and finishing touches

✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main changes: introducing a new CallbackUtil class and fixing the get_price_hash method for variations.
Description check ✅ Passed The description is comprehensive and directly related to the changeset, explaining the problem, solution, and providing testing instructions for the fix.
Linked Issues check ✅ Passed The PR successfully implements the required solution: CallbackUtil provides stable callback signatures independent of object state, and get_price_hash uses these signatures, directly addressing the issue #37547.
Out of Scope Changes check ✅ Passed All changes are directly related to addressing the linked issue #37547: new CallbackUtil class, updated get_price_hash implementation, comprehensive test suite, and test helper classes are all in scope.
✨ 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 add-proper-callback-serialization-tool

📜 Recent review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1dc5bc and 8bb4524.

📒 Files selected for processing (2)
  • plugins/woocommerce/src/Utilities/CallbackUtil.php (1 hunks)
  • plugins/woocommerce/tests/php/src/Utilities/CallbackUtilTest.php (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • plugins/woocommerce/tests/php/src/Utilities/CallbackUtilTest.php
  • plugins/woocommerce/src/Utilities/CallbackUtil.php
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (31)
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests - Legacy MiniCart 2/3 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests - Legacy MiniCart 1/3 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 3/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: pre-release [WP 6.9-beta2] 2/2 - @woocommerce/plugin-woocommerce [unit:php] (optional)
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 8/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests - Legacy MiniCart - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: PHP: 8.4 WP: latest - Legacy MiniCart [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: pre-release [WP 6.9-beta2] 1/2 - @woocommerce/plugin-woocommerce [unit:php] (optional)
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.4] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.4] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest - Legacy MiniCart [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: build

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.

@Konamiman Konamiman force-pushed the add-proper-callback-serialization-tool branch from e1baf80 to dd3f7c6 Compare November 3, 2025 12:37
@github-actions
Copy link
Contributor

github-actions bot commented Nov 3, 2025

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
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

🧹 Nitpick comments (1)
plugins/woocommerce/src/Utilities/CallbackUtil.php (1)

41-44: Document that closure signatures are not stable across requests.

The use of spl_object_hash() produces a unique hash per closure instance, which changes between requests. This means closures attached as filter callbacks will cause the price hash to change on every request, preventing transient reuse.

While there's no reliable way to generate stable signatures for closures across requests (they're unique instances each time), this limitation should be documented in the method's docblock so developers understand that closures will bypass caching benefits.

Apply this diff to add a warning in the docblock:

 	/**
 	 * Get a stable signature for a callback that can be used for hashing.
 	 *
 	 * This method normalizes callbacks into consistent string representations,
 	 * regardless of changes in dynamic properties in callback instances.
+	 *
+	 * Note: Closure signatures use spl_object_hash() and are not stable across
+	 * requests. Closures will cause cache invalidation on every request.
 	 *
 	 * @param callable|mixed $callback A PHP callback.
 	 * @return string Normalized callback signature.
📜 Review details

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between dd3f7c6 and c1dc5bc.

📒 Files selected for processing (1)
  • plugins/woocommerce/src/Utilities/CallbackUtil.php (1 hunks)
🧰 Additional context used
📓 Path-based instructions (3)
**/*.{php,js,jsx,ts,tsx}

📄 CodeRabbit inference engine (.cursor/rules/code-quality.mdc)

**/*.{php,js,jsx,ts,tsx}: Guard against unexpected inputs
Sanitize and validate any potentially dangerous inputs
Ensure code is backwards compatible
Write code that is readable and intuitive
Ensure code has unit or E2E tests where applicable

Files:

  • plugins/woocommerce/src/Utilities/CallbackUtil.php
**/*.{php,js,jsx,tsx,ts}

📄 CodeRabbit inference engine (.cursor/rules/avoid-regex.mdc)

**/*.{php,js,jsx,tsx,ts}: Avoid using regular expressions unless absolutely necessary to favor readability and maintainability
Only consider regex when no built-in language alternative (string/array APIs) fits the need
Only use regex when performance is critical and faster than alternatives, with measurements to justify
Use regex for complex pattern matching only if the pattern is well-documented and thoroughly tested
Allow regex when maintaining legacy code where an existing, correct pattern is being modified
If regex is necessary, document the pattern extensively to explain what it matches
If regex is necessary, add comprehensive tests covering edge cases and potential security issues
Use named capture groups in regex to improve readability when supported
Validate input before applying regex to ensure it is safe
Assess and mitigate security risks when using regex, including ReDoS and injection vulnerabilities
Avoid regex patterns that can cause catastrophic backtracking (ReDoS)
Do not construct regex from untrusted input to prevent injection attacks
Ensure regex patterns do not overmatch and unintentionally capture unexpected inputs

Files:

  • plugins/woocommerce/src/Utilities/CallbackUtil.php
**/*.{php,js,ts,jsx,tsx}

⚙️ CodeRabbit configuration file

**/*.{php,js,ts,jsx,tsx}: Don't trust that extension developers will follow the best practices, make sure the code:

  • Guards against unexpected inputs.
  • Sanitizes and validates any potentially dangerous inputs.
  • Is backwards compatible.
  • Is readable and intuitive.
  • Has unit or E2E tests where applicable.
    When making any changes to code that deletes or modifies orders/products/customer data, make sure that there are
    sufficient checks in place to prevent accidental data loss. As an example, if deleting a draft order, check that
    the order status is indeed draft or checkout-draft. Also think about whether race conditions could occur and
    delete orders that don't belong to the current customer. When in doubt, ask for clarification in the PR comments.

Files:

  • plugins/woocommerce/src/Utilities/CallbackUtil.php
🧠 Learnings (1)
📓 Common learnings
Learnt from: Aljullu
Repo: woocommerce/woocommerce PR: 60641
File: plugins/woocommerce/client/blocks/tests/e2e/tests/add-to-cart-with-options/add-to-cart-with-options.block_theme.spec.ts:410-480
Timestamp: 2025-08-29T14:38:18.023Z
Learning: The "sold individually" variation functionality (where the Product Quantity block should hide when a variation is sold individually) mentioned in issue #59443 objectives is not implemented in PR #60641 and will be addressed in a future PR.
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (30)
  • GitHub Check: Blocks e2e tests 5/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 10/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 6/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests - Legacy MiniCart 1/3 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 7/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 9/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 4/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Blocks e2e tests 1/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core API tests - @woocommerce/plugin-woocommerce [api]
  • GitHub Check: Blocks e2e tests 2/10 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests - Legacy MiniCart - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Metrics - @woocommerce/plugin-woocommerce [performance]
  • GitHub Check: Core e2e tests 5/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 1/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 8.4 WP: latest (HPOS:off) [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Core e2e tests 2/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 4/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: Core e2e tests 3/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: pre-release [WP 6.9-beta2] 2/2 - @woocommerce/plugin-woocommerce [unit:php] (optional)
  • GitHub Check: Core e2e tests 6/6 - @woocommerce/plugin-woocommerce [e2e]
  • GitHub Check: PHP: 7.4 WP: pre-release [WP 6.9-beta2] 1/2 - @woocommerce/plugin-woocommerce [unit:php] (optional)
  • GitHub Check: PHP: 8.4 WP: latest - Legacy MiniCart [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.4] 2/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest - Legacy MiniCart [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 7.4 WP: latest - 1 [WP 6.7.4] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: PHP: 8.4 WP: latest [WP latest] 1/2 - @woocommerce/plugin-woocommerce [unit:php]
  • GitHub Check: Lint - @woocommerce/plugin-woocommerce
  • GitHub Check: build

@claude
Copy link

claude bot commented Nov 3, 2025

Claude encountered an error —— View job


Backwards Compatibility Review

Analyzing PR #61779 for breaking changes and ecosystem impacts...

Todo List

  • Read and analyze changed files
  • Assess public API changes
  • Check hook modifications
  • Evaluate price hash generation changes
  • Consider ecosystem/extension impacts
  • Review CodeRabbit feedback
  • Provide BC assessment summary

@Konamiman Konamiman force-pushed the add-proper-callback-serialization-tool branch from 1c94b1a to 8bb4524 Compare November 3, 2025 17:50
@claude
Copy link

claude bot commented Nov 3, 2025

Claude finished @Konamiman's task —— View job


Backwards Compatibility Review

Comprehensive analysis of PR #61779 for breaking changes and ecosystem impacts.


Summary

Risk Level: LOW

This PR introduces a new utility class and modifies internal price hash generation logic to fix a caching bug. The changes are backwards compatible with minimal ecosystem risk. The price hash format change is intentional and necessary to fix the bug—existing cached transients will naturally invalidate and rebuild with correct values.

Key Points:

  • New CallbackUtil class adds public API but doesn't modify existing APIs
  • get_price_hash is a protected method (internal implementation detail)
  • Price hash format change will cause one-time cache invalidation (expected behavior)
  • No hooks added, removed, or modified in their external contracts
  • Defensive coding added for corrupted $wp_filter structures

Detailed Analysis

1. Public API Changes ✅ Safe - New additions only

New Class: Automattic\WooCommerce\Utilities\CallbackUtil (final)

File: plugins/woocommerce/src/Utilities/CallbackUtil.php

Two new public static methods:

// Line 26-55
public static function get_callback_signature( $callback ): string
  • Purpose: Generates stable signature strings for PHP callbacks
  • Parameters: callable|mixed $callback
  • Returns: string - Normalized callback signature
  • BC Impact: None - new method, no conflicts
  • @SInCE: 10.4.0
// Line 72-99
public static function get_hook_callback_signatures( string $hook_name ): array
  • Purpose: Returns array of callback signatures for a hook, organized by priority
  • Parameters: string $hook_name
  • Returns: array<int, array<string>> - Priority => signatures mapping
  • BC Impact: None - new method, no conflicts
  • @SInCE: 10.4.0
  • Note: Includes defensive handling for corrupted $wp_filter structures (lines 81-93)

Assessment: These are new utility methods with no conflicts with existing APIs. Extensions can safely adopt them.


2. Modified Internal Implementation ✅ Safe - Protected method

File: plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php:488-509

Method: WC_Product_Variable_Data_Store_CPT::get_price_hash() (protected)

Changes:

- Direct access to global $wp_filter
- Manual iteration over priorities and callbacks
- Using wp_list_pluck() to extract 'function' keys
- Serializing entire callback objects (buggy behavior)

+ Using CallbackUtil::get_hook_callback_signatures()
+ Stable callback signatures independent of object state
+ Defensive handling of corrupted $wp_filter

BC Assessment:

  • ✅ Method visibility: protected - not part of public API contract
  • ✅ Method signature unchanged: protected function get_price_hash( &$product, $for_display = false )
  • ✅ Return type unchanged: string (MD5 hash)
  • ⚠️ Hash value will change (but this is the intended fix)

Price Hash Format Change:

Before (buggy):

$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
// Serialized entire callback objects including dynamic properties

After (fixed):

$price_hash[ $filter_name ] = CallbackUtil::get_hook_callback_signatures( $filter_name );
// Returns: array( priority => array( 'ClassName::method', 'function_name', ... ) )

Impact:

  • First request after deployment: All variation price transients (wc_var_prices_{product_id}) will invalidate
  • Subsequent requests: Stable cache keys enable proper transient reuse
  • This is the intended behavior - the bug was that hashes changed on every request

3. Hook Modifications ✅ No changes

Checked hooks:

  • woocommerce_variation_prices_price - Not modified
  • woocommerce_variation_prices_regular_price - Not modified
  • woocommerce_variation_prices_sale_price - Not modified
  • woocommerce_get_variation_prices_hash - Not modified

Assessment: No hooks added, removed, or changed. Filter contracts remain identical.


4. Ecosystem Impact Analysis ✅ Minimal risk

Extensions hooking into variation price filters:

The three price filters analyzed:

  • woocommerce_variation_prices_price
  • woocommerce_variation_prices_regular_price
  • woocommerce_variation_prices_sale_price

Common Extension Patterns:

  1. WooCommerce Subscriptions - Hooks these filters for subscription pricing

    • Impact: None - filter behavior unchanged, will benefit from cache fix
  2. WooCommerce Memberships - Modifies prices based on membership levels

    • Impact: None - dynamic pricing will now cache correctly
  3. Custom pricing plugins - Add discounts, markups, role-based pricing

    • Impact: None - existing callbacks continue working
    • Benefit: Object property changes no longer break caching
  4. Payment gateways - Typically don't hook variation price filters

    • Impact: None

Closure Usage Warning:

Per CallbackUtil.php:64-65:

Note: Closure signatures use spl_object_hash(), which will differ between requests.

  • Extensions using closures for these filters will still experience cache invalidation on each request
  • Recommendation: Extensions should use named functions or class methods, not closures
  • This is a limitation, not a regression (closures were already problematic)

5. Testing & Validation ✅ Comprehensive

Test Coverage: plugins/woocommerce/tests/php/src/Utilities/CallbackUtilTest.php (425 lines, 18 test methods)

Key test scenarios:

  • ✅ Function callbacks: test_get_callback_signature_with_function_name()
  • ✅ Instance methods: test_get_callback_signature_with_instance_method()
  • ✅ Static methods: test_get_callback_signature_with_static_method()
  • ✅ Closures: test_get_callback_signature_different_closures()
  • ✅ Invokable objects: test_get_callback_signature_with_invokable_object()
  • ✅ Consistency across instances: test_get_callback_signature_consistent_across_instances()
  • ✅ Multiple callbacks per hook: test_get_hook_callback_signatures_with_multiple_callbacks()
  • Corrupted $wp_filter: test_get_hook_callback_signatures_with_corrupted_wp_filter() (line 348)
  • Corrupted priority entries: test_get_hook_callback_signatures_with_corrupted_priority_entry() (line 371)
  • Corrupted callback entries: test_get_hook_callback_signatures_with_corrupted_callback_entry() (line 399)

Defensive Coding: Lines 81-93 and 108-117 in CallbackUtil.php handle corrupted $wp_filter structures gracefully.


6. CodeRabbit Review Feedback ✅ Addressed

Review Comment: Add defensive coding for corrupted $wp_filter structure

Resolution: Commit 8bb4524a62 - "Add robustness against corrupted $wp_filter"

  • Added validation in is_valid_wp_filter_priority_entry() (lines 108-117)
  • Fallback to serialization for invalid structures (lines 81-93)
  • Comprehensive test coverage for edge cases

Testing Focus

Critical Regression Testing Areas:

  1. Variable Product Price Display (High Priority)

    • Load product pages with variable products
    • Verify price ranges display correctly
    • Test with/without active filters on the three price hooks
    • Expected: Prices display correctly, faster on subsequent loads
  2. Transient Invalidation Behavior (High Priority)

    • Monitor wc_var_prices_{product_id} transients
    • Add/remove hooks to price filters dynamically
    • Expected: Hash changes when hooks change, stable when hooks unchanged
  3. Extension Compatibility (Medium Priority)

    • Test with WooCommerce Subscriptions
    • Test with WooCommerce Memberships
    • Test with dynamic pricing plugins
    • Expected: All continue functioning, benefit from stable caching
  4. Performance Validation (Medium Priority)

    • Measure price calculation time for variable products with many variations
    • First load vs. cached load
    • Expected: Significant performance improvement on cached loads

Test Steps (from PR description):

  1. Create/identify variable product with many variations
  2. Apply test snippet (adds filter that modifies object properties)
  3. Run wp eval 'WooPriceHashTester::test_price_hash(<id>, true);'
  4. Expected: Hash1 === Hash2 (stable across property changes)
  5. Run again with false parameter
  6. Expected: Same hashes, much faster (using cache)
  7. Verify shop display and checkout still work correctly

Cache Migration Considerations

One-Time Impact:

When this code deploys to production:

  1. All existing variation price transients will have old hash format in keys
  2. First request after deployment will not find matching hash in transient
  3. Prices will recalculate (correct behavior)
  4. New transient stored with stable hash
  5. Subsequent requests use cache (performance improvement)

No manual cache clearing needed - natural invalidation occurs automatically.


Confidence

95% - No breaking changes detected.

Justification:

  • All modified code is internal/protected (not public API)
  • New utility class adds functionality without conflicts
  • Comprehensive test coverage including edge cases
  • Price hash format change is intentional fix, not regression
  • Defensive coding handles corrupted data structures
  • No hook contract modifications
  • Extensions benefit from fix without code changes

Remaining 5% uncertainty:

  • Edge cases with exotic callback types not covered in tests
  • Potential unforeseen extension patterns that inspect $wp_filter directly for these specific hooks
  • Performance impact on sites with thousands of variable products (should be positive but not measured)

Recommendations

  1. Approve for merge - No blocking BC concerns
  2. 📊 Monitor post-deployment:
    • Transient regeneration patterns
    • Price display accuracy
    • Cache hit rates
    • Error logs for callback signature failures
  3. 📝 Documentation suggestion: Add developer note to Woo docs about avoiding closures in price filters for optimal caching

Conclusion

This PR is backwards compatible. The price hash format change is an intentional bug fix that will cause one-time cache invalidation—this is correct behavior. Extensions hooking the variation price filters require no code changes and will automatically benefit from the stable caching. The new CallbackUtil class provides useful utilities for the ecosystem without introducing conflicts.

@claude
Copy link

claude bot commented Nov 4, 2025

Claude finished @Konamiman's task —— View job


Backwards Compatibility Review

Comprehensive analysis of PR #61779 for breaking changes and ecosystem impacts.


Summary

Risk Level: LOW

This PR introduces a new utility class and modifies internal price hash generation logic to fix a caching bug. The changes are backwards compatible with minimal ecosystem risk. The price hash format change is intentional and necessary to fix the bug—existing cached transients will naturally invalidate and rebuild with correct values.

Key Points:

  • New CallbackUtil class adds public API but doesn't modify existing APIs
  • get_price_hash is a protected method (internal implementation detail)
  • Price hash format change will cause one-time cache invalidation (expected behavior)
  • No hooks added, removed, or modified in their external contracts
  • Defensive coding added for corrupted $wp_filter structures

Detailed Analysis

1. Public API Changes ✅ Safe - New additions only

New Class: Automattic\WooCommerce\Utilities\CallbackUtil (final)

File: plugins/woocommerce/src/Utilities/CallbackUtil.php

Two new public static methods:

// Line 26-55
public static function get_callback_signature( $callback ): string
  • Purpose: Generates stable signature strings for PHP callbacks
  • Parameters: callable|mixed $callback
  • Returns: string - Normalized callback signature
  • BC Impact: None - new method, no conflicts
  • @SInCE: 10.4.0
// Line 72-99
public static function get_hook_callback_signatures( string $hook_name ): array
  • Purpose: Returns array of callback signatures for a hook, organized by priority
  • Parameters: string $hook_name
  • Returns: array<int, array<string>> - Priority => signatures mapping
  • BC Impact: None - new method, no conflicts
  • @SInCE: 10.4.0
  • Note: Includes defensive handling for corrupted $wp_filter structures (lines 81-93, 108-117)

Assessment: These are new utility methods with no conflicts with existing APIs. Extensions can safely adopt them.


2. Modified Internal Implementation ✅ Safe - Protected method

File: plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php:488-509

Method: WC_Product_Variable_Data_Store_CPT::get_price_hash() (protected)

Changes:

- Direct access to global $wp_filter
- Manual iteration over priorities and callbacks
- Using wp_list_pluck() to extract 'function' keys
- Serializing entire callback objects (buggy behavior)

+ Using CallbackUtil::get_hook_callback_signatures()
+ Stable callback signatures independent of object state
+ Defensive handling of corrupted $wp_filter

BC Assessment:

  • ✅ Method visibility: protected - not part of public API contract
  • ✅ Method signature unchanged: protected function get_price_hash( &$product, $for_display = false )
  • ✅ Return type unchanged: string (MD5 hash)
  • ⚠️ Hash value will change (but this is the intended fix)

Price Hash Format Change:

Before (buggy):

$price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
// Serialized entire callback objects including dynamic properties

After (fixed):

$price_hash[ $filter_name ] = CallbackUtil::get_hook_callback_signatures( $filter_name );
// Returns: array( priority => array( 'ClassName::method', 'function_name', ... ) )

Impact:

  • First request after deployment: All variation price transients (wc_var_prices_{product_id}) will invalidate
  • Subsequent requests: Stable cache keys enable proper transient reuse
  • This is the intended behavior - the bug was that hashes changed on every request

3. Hook Modifications ✅ No changes

Checked hooks:

  • woocommerce_variation_prices_price (line 312) - Not modified
  • woocommerce_variation_prices_regular_price (line 313) - Not modified
  • woocommerce_variation_prices_sale_price (line 314) - Not modified
  • woocommerce_variation_prices_array (lines 379, 395, 402) - Not modified
  • woocommerce_variation_prices (lines 445, 448) - Not modified
  • woocommerce_get_variation_prices_hash (line 508) - Not modified

Assessment: No hooks added, removed, or changed. Filter contracts remain identical.


4. Ecosystem Impact Analysis ✅ Minimal risk

Extensions hooking into variation price filters:

The three price filters analyzed:

  • woocommerce_variation_prices_price
  • woocommerce_variation_prices_regular_price
  • woocommerce_variation_prices_sale_price

Common Extension Patterns:

  1. WooCommerce Subscriptions - Hooks these filters for subscription pricing

    • Impact: None - filter behavior unchanged, will benefit from cache fix
  2. WooCommerce Memberships - Modifies prices based on membership levels

    • Impact: None - dynamic pricing will now cache correctly
  3. Custom pricing plugins - Add discounts, markups, role-based pricing

    • Impact: None - existing callbacks continue working
    • Benefit: Object property changes no longer break caching
  4. Payment gateways - Typically don't hook variation price filters

    • Impact: None

Closure Usage Limitation:

Per CallbackUtil.php:64-65:

Note: Closure signatures use spl_object_hash(), which will differ between requests.

  • Extensions using closures for these filters will still experience cache invalidation on each request
  • Recommendation: Extensions should use named functions or class methods, not closures
  • This is a limitation, not a regression (closures were already problematic)

5. Testing & Validation ✅ Comprehensive

Test Coverage: plugins/woocommerce/tests/php/src/Utilities/CallbackUtilTest.php (425 lines, 18 test methods)

Key test scenarios:

  • ✅ Function callbacks: test_get_callback_signature_with_function_name()
  • ✅ Instance methods: test_get_callback_signature_with_instance_method()
  • ✅ Static methods: test_get_callback_signature_with_static_method()
  • ✅ Closures: test_get_callback_signature_different_closures()
  • ✅ Invokable objects: test_get_callback_signature_with_invokable_object()
  • ✅ Consistency across instances: test_get_callback_signature_consistent_across_instances()
  • ✅ Multiple callbacks per hook: test_get_hook_callback_signatures_with_multiple_callbacks()
  • Corrupted $wp_filter: test_get_hook_callback_signatures_with_corrupted_wp_filter() (line 348)
  • Corrupted priority entries: test_get_hook_callback_signatures_with_corrupted_priority_entry() (line 371)
  • Corrupted callback entries: test_get_hook_callback_signatures_with_corrupted_callback_entry() (line 399)

Defensive Coding: Lines 81-93 and 108-117 in CallbackUtil.php handle corrupted $wp_filter structures gracefully.


6. CodeRabbit Review Feedback ✅ Addressed

Review Comment: Add defensive coding for corrupted $wp_filter structure

Resolution: Commit 8bb4524a62 - "Add robustness against corrupted $wp_filter"

  • Added validation in is_valid_wp_filter_priority_entry() (lines 108-117)
  • Fallback to serialization for invalid structures (lines 81-93)
  • Comprehensive test coverage for edge cases

Testing Focus

Critical Regression Testing Areas:

  1. Variable Product Price Display (High Priority)

    • Load product pages with variable products
    • Verify price ranges display correctly
    • Test with/without active filters on the three price hooks
    • Expected: Prices display correctly, faster on subsequent loads
  2. Transient Invalidation Behavior (High Priority)

    • Monitor wc_var_prices_{product_id} transients
    • Add/remove hooks to price filters dynamically
    • Expected: Hash changes when hooks change, stable when hooks unchanged
  3. Extension Compatibility (Medium Priority)

    • Test with WooCommerce Subscriptions
    • Test with WooCommerce Memberships
    • Test with dynamic pricing plugins
    • Expected: All continue functioning, benefit from stable caching
  4. Performance Validation (Medium Priority)

    • Measure price calculation time for variable products with many variations
    • First load vs. cached load
    • Expected: Significant performance improvement on cached loads

Test Steps (from PR description):

  1. Create/identify variable product with many variations
  2. Apply test snippet (adds filter that modifies object properties)
  3. Run wp eval 'WooPriceHashTester::test_price_hash(<id>, true);'
  4. Expected: Hash1 === Hash2 (stable across property changes)
  5. Run again with false parameter
  6. Expected: Same hashes, much faster (using cache)
  7. Verify shop display and checkout still work correctly

Cache Migration Considerations

One-Time Impact:

When this code deploys to production:

  1. All existing variation price transients will have old hash format in keys
  2. First request after deployment will not find matching hash in transient
  3. Prices will recalculate (correct behavior)
  4. New transient stored with stable hash
  5. Subsequent requests use cache (performance improvement)

No manual cache clearing needed - natural invalidation occurs automatically.


Confidence

95% - No breaking changes detected.

Justification:

  • All modified code is internal/protected (not public API)
  • New utility class adds functionality without conflicts
  • Comprehensive test coverage including edge cases
  • Price hash format change is intentional fix, not regression
  • Defensive coding handles corrupted data structures
  • No hook contract modifications
  • Extensions benefit from fix without code changes

Remaining 5% uncertainty:

  • Edge cases with exotic callback types not covered in tests
  • Potential unforeseen extension patterns that inspect $wp_filter directly for these specific hooks
  • Performance impact on sites with thousands of variable products (should be positive but not measured)

Recommendations

  1. Approve for merge - No blocking BC concerns
  2. 📊 Monitor post-deployment:
    • Transient regeneration patterns
    • Price display accuracy
    • Cache hit rates
    • Error logs for callback signature failures
  3. 📝 Documentation suggestion: Add developer note to Woo docs about avoiding closures in price filters for optimal caching

Conclusion

This PR is backwards compatible. The price hash format change is an intentional bug fix that will cause one-time cache invalidation—this is correct behavior. Extensions hooking the variation price filters require no code changes and will automatically benefit from the stable caching. The new CallbackUtil class provides useful utilities for the ecosystem without introducing conflicts.

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.

Variable data store read_price_data bypassing transients when hooked using add_filter(..., [$this, 'hookname'])

2 participants