-
Notifications
You must be signed in to change notification settings - Fork 10.7k
Add the CallbackUtil class and fix get_price_hash for variations #61779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Conversation
Testing GuidelinesHi @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:
|
…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.
📝 WalkthroughWalkthroughAdds 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
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
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Pre-merge checks and finishing touches✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ 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)
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. Comment |
e1baf80 to
dd3f7c6
Compare
Test using WordPress PlaygroundThe changes in this pull request can be previewed and tested using a WordPress Playground instance. 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. |
There was a problem hiding this 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
📒 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 indeeddraftorcheckout-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 encountered an error —— View job Backwards Compatibility Review
|
1c94b1a to
8bb4524
Compare
|
Claude finished @Konamiman's task —— View job Backwards Compatibility ReviewComprehensive analysis of PR #61779 for breaking changes and ecosystem impacts. SummaryRisk 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:
Detailed Analysis1. Public API Changes ✅ Safe - New additions onlyNew Class: File: Two new public static methods: // Line 26-55
public static function get_callback_signature( $callback ): string
// Line 72-99
public static function get_hook_callback_signatures( string $hook_name ): array
Assessment: These are new utility methods with no conflicts with existing APIs. Extensions can safely adopt them. 2. Modified Internal Implementation ✅ Safe - Protected methodFile: Method: 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_filterBC Assessment:
Price Hash Format Change: Before (buggy): $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
// Serialized entire callback objects including dynamic propertiesAfter (fixed): $price_hash[ $filter_name ] = CallbackUtil::get_hook_callback_signatures( $filter_name );
// Returns: array( priority => array( 'ClassName::method', 'function_name', ... ) )Impact:
3. Hook Modifications ✅ No changesChecked hooks:
Assessment: No hooks added, removed, or changed. Filter contracts remain identical. 4. Ecosystem Impact Analysis ✅ Minimal riskExtensions hooking into variation price filters: The three price filters analyzed:
Common Extension Patterns:
Closure Usage Warning: Per
5. Testing & Validation ✅ ComprehensiveTest Coverage: Key test scenarios:
Defensive Coding: Lines 81-93 and 108-117 in 6. CodeRabbit Review Feedback ✅ AddressedReview Comment: Add defensive coding for corrupted Resolution: Commit
Testing FocusCritical Regression Testing Areas:
Test Steps (from PR description):
Cache Migration ConsiderationsOne-Time Impact: When this code deploys to production:
No manual cache clearing needed - natural invalidation occurs automatically. Confidence95% - No breaking changes detected. Justification:
Remaining 5% uncertainty:
Recommendations
ConclusionThis 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 |
|
Claude finished @Konamiman's task —— View job Backwards Compatibility ReviewComprehensive analysis of PR #61779 for breaking changes and ecosystem impacts. SummaryRisk 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:
Detailed Analysis1. Public API Changes ✅ Safe - New additions onlyNew Class: File: Two new public static methods: // Line 26-55
public static function get_callback_signature( $callback ): string
// Line 72-99
public static function get_hook_callback_signatures( string $hook_name ): array
Assessment: These are new utility methods with no conflicts with existing APIs. Extensions can safely adopt them. 2. Modified Internal Implementation ✅ Safe - Protected methodFile: Method: 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_filterBC Assessment:
Price Hash Format Change: Before (buggy): $price_hash[ $filter_name ][] = array_values( wp_list_pluck( $callbacks, 'function' ) );
// Serialized entire callback objects including dynamic propertiesAfter (fixed): $price_hash[ $filter_name ] = CallbackUtil::get_hook_callback_signatures( $filter_name );
// Returns: array( priority => array( 'ClassName::method', 'function_name', ... ) )Impact:
3. Hook Modifications ✅ No changesChecked hooks:
Assessment: No hooks added, removed, or changed. Filter contracts remain identical. 4. Ecosystem Impact Analysis ✅ Minimal riskExtensions hooking into variation price filters: The three price filters analyzed:
Common Extension Patterns:
Closure Usage Limitation: Per
5. Testing & Validation ✅ ComprehensiveTest Coverage: Key test scenarios:
Defensive Coding: Lines 81-93 and 108-117 in 6. CodeRabbit Review Feedback ✅ AddressedReview Comment: Add defensive coding for corrupted Resolution: Commit
Testing FocusCritical Regression Testing Areas:
Test Steps (from PR description):
Cache Migration ConsiderationsOne-Time Impact: When this code deploys to production:
No manual cache clearing needed - natural invalidation occurs automatically. Confidence95% - No breaking changes detected. Justification:
Remaining 5% uncertainty:
Recommendations
ConclusionThis 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 |

Changes proposed in this Pull Request:
The problem
WC_Product_Variable_Data_Store_CPTuses 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_hashmethod) includes information about the current usages of these filters: ifadd_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:
CallbackUtilclass with aget_callback_signaturemethod that will generate a proper signature string for any callback, irrespective of any state in the involved objects; and aget_hook_callback_signaturesmethod that given a hook name will return an array ofpriority => [callback signature1, callback signature 2...]WC_Product_Variable_Data_Store_CPT::get_price_hashso that it usesCallbackUtil::get_hook_callback_signaturesfor 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:
Make sure that your store has at least one variable product (preferably with a large number of variations), take note of the product id.
Add the following code snippet to your server:
wp eval 'WooPriceHashTester::test_price_hash(<product id>, true);'The two hashes printed should be the same ones.
Now run the following command but passing
falseto the function. You should get the exact same hashes and this time it should have finished much faster.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.
Changelog entry
Changelog Entry Details
Significance
Type
Message
Changelog Entry Comment
Comment