47288db826
Baseline before backporting wp-data-optimizer v3.0.1-v3.4.6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
237 lines
7.8 KiB
PHP
237 lines
7.8 KiB
PHP
<?php
|
|
// phpcs:ignore WPDO.AntiEAV -- platform integration with WooCommerce: term count cache transform
|
|
/**
|
|
* TMDO_WC_Term_Count_Filter — Reroute WooCommerce term count cache from
|
|
* wp_termmeta to wp_options (v2.12.3 Phase 3).
|
|
*
|
|
* Background
|
|
* ----------
|
|
* WooCommerce caches the number of products per term as a wp_termmeta row:
|
|
*
|
|
* wp_termmeta(term_id=5, meta_key='product_count_product_cat', meta_value='42')
|
|
*
|
|
* Each `product_count_<taxonomy>` row is a transient-like cache — WC
|
|
* recalculates and writes whenever a product is added/removed from a term.
|
|
* Pattern is identical to HivePress's per-post TTL cache anti-pattern that
|
|
* v2.11.5 solved: the data is genuine cache, but storage location is wrong.
|
|
*
|
|
* Strategy
|
|
* --------
|
|
* Mirror v2.11.5 `TMDO_Hivepress_Transient_Filter`. Intercept term metadata
|
|
* writes/reads where meta_key starts with `product_count_` and route to
|
|
* wp_options as native transient (no per-key TTL — WC manages its own
|
|
* invalidation; we just provide indistinguishable storage).
|
|
*
|
|
* Translation key:
|
|
* wp_options('_transient_wpdo_wc_termcount_<term_id>_<md5(meta_key)>')
|
|
*
|
|
* Namespacing by term_id keeps caches scoped to the right term; md5 of the
|
|
* meta_key handles arbitrary-length taxonomy slugs (`product_count_my_long_taxonomy_with_many_chars`).
|
|
*
|
|
* Default: enabled. Toggle: `wpdo_wc_term_count_filter_enabled` option.
|
|
*
|
|
* 🔒 Lessons applied (v2.11.7): added to TMDO_Hook_Bus_Bridge::COEXIST_WHITELIST
|
|
* so `wp wpdo conflict-scan` does not report a false positive.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.12.3
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Routes WooCommerce term count cache out of wp_termmeta into wp_options.
|
|
*/
|
|
final class TMDO_WC_Term_Count_Filter {
|
|
|
|
/** Option toggle key. */
|
|
public const OPT_ENABLED = 'wpdo_wc_term_count_filter_enabled';
|
|
|
|
/** Prefix that identifies a WooCommerce term count cache row. */
|
|
public const PREFIX = 'product_count_';
|
|
|
|
/** Namespace prefix for translated wp_options entries. */
|
|
public const TRANSLATED_NAMESPACE = 'wpdo_wc_termcount_';
|
|
|
|
/**
|
|
* Register the four metadata filters on `init` priority 5.
|
|
* Idempotent — safe to call multiple times.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function init(): void {
|
|
if ( ! self::is_enabled() ) {
|
|
return;
|
|
}
|
|
|
|
// Filter priority 9 (before Hook Bus at 10), same as HivePress filter.
|
|
add_filter( 'get_term_metadata', array( self::class, 'on_read' ), 9, 4 );
|
|
add_filter( 'add_term_metadata', array( self::class, 'on_add' ), 9, 5 );
|
|
add_filter( 'update_term_metadata', array( self::class, 'on_update' ), 9, 5 );
|
|
add_filter( 'delete_term_metadata', array( self::class, 'on_delete' ), 9, 5 );
|
|
}
|
|
|
|
/**
|
|
* Check the admin toggle. Defaults to enabled.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function is_enabled(): bool {
|
|
return (bool) get_option( self::OPT_ENABLED, '1' );
|
|
}
|
|
|
|
/**
|
|
* Test whether a meta_key is a WC term count cache row.
|
|
*
|
|
* @param mixed $meta_key Candidate meta_key.
|
|
* @return bool
|
|
*/
|
|
public static function is_target_key( $meta_key ): bool {
|
|
return is_string( $meta_key ) && str_starts_with( $meta_key, self::PREFIX );
|
|
}
|
|
|
|
/**
|
|
* Translate (term_id, meta_key) → namespaced wp_options option_name root.
|
|
*
|
|
* @param int $term_id Term ID.
|
|
* @param string $meta_key Original `product_count_*` meta_key.
|
|
* @return string Translated option_name root (without `_transient_` prefix).
|
|
*/
|
|
public static function translate_key( int $term_id, string $meta_key ): string {
|
|
return self::TRANSLATED_NAMESPACE . $term_id . '_' . md5( $meta_key );
|
|
}
|
|
|
|
/**
|
|
* Filter callback: get_term_metadata.
|
|
*
|
|
* @param mixed $pre Filter accumulator (null at our priority).
|
|
* @param int $object_id Term ID.
|
|
* @param string $meta_key Meta key being read.
|
|
* @param bool $single Whether single value was requested.
|
|
* @return mixed
|
|
*/
|
|
public static function on_read( $pre, $object_id, $meta_key, $single ) {
|
|
unset( $single );
|
|
if ( ! self::is_target_key( $meta_key ) ) {
|
|
return $pre;
|
|
}
|
|
$translated = self::translate_key( (int) $object_id, (string) $meta_key );
|
|
$option_name = '_transient_' . $translated;
|
|
$value = get_option( $option_name, null );
|
|
|
|
if ( null === $value ) {
|
|
// Cache miss — fall through to wp_termmeta (back-compat).
|
|
return $pre;
|
|
}
|
|
|
|
// WP unwraps [0] for single=true callers; return array of values.
|
|
return array( $value );
|
|
}
|
|
|
|
/**
|
|
* Filter callback: add_term_metadata.
|
|
*
|
|
* @param mixed $check Filter accumulator (null at our priority).
|
|
* @param int $object_id Term ID.
|
|
* @param string $meta_key Meta key being written.
|
|
* @param mixed $meta_value Value being written.
|
|
* @param bool $unique Whether the unique flag was set (unused).
|
|
* @return mixed
|
|
*/
|
|
public static function on_add( $check, $object_id, $meta_key, $meta_value, $unique ) {
|
|
unset( $unique );
|
|
if ( ! self::is_target_key( $meta_key ) ) {
|
|
return $check;
|
|
}
|
|
self::write_translated( (int) $object_id, (string) $meta_key, $meta_value );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Filter callback: update_term_metadata.
|
|
*
|
|
* @param mixed $check Filter accumulator (null at our priority).
|
|
* @param int $object_id Term ID.
|
|
* @param string $meta_key Meta key being written.
|
|
* @param mixed $meta_value Value being written.
|
|
* @param mixed $prev_value Previous value scope (unused).
|
|
* @return mixed
|
|
*/
|
|
public static function on_update( $check, $object_id, $meta_key, $meta_value, $prev_value ) {
|
|
unset( $prev_value );
|
|
if ( ! self::is_target_key( $meta_key ) ) {
|
|
return $check;
|
|
}
|
|
self::write_translated( (int) $object_id, (string) $meta_key, $meta_value );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Filter callback: delete_term_metadata.
|
|
*
|
|
* @param mixed $check Filter accumulator (null at our priority).
|
|
* @param int $object_id Term ID.
|
|
* @param string $meta_key Meta key being deleted.
|
|
* @param mixed $meta_value Value-scoped delete (unused).
|
|
* @param bool $delete_all Whether to delete from all objects (unused).
|
|
* @return mixed
|
|
*/
|
|
public static function on_delete( $check, $object_id, $meta_key, $meta_value, $delete_all ) {
|
|
unset( $meta_value, $delete_all );
|
|
if ( ! self::is_target_key( $meta_key ) ) {
|
|
return $check;
|
|
}
|
|
$translated = self::translate_key( (int) $object_id, (string) $meta_key );
|
|
$option_name = '_transient_' . $translated;
|
|
delete_option( $option_name );
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Internal: persist value to wp_options as a no-expiry transient.
|
|
*
|
|
* `autoload=no` because WC reads on demand only — never via wp_load_alloptions().
|
|
*
|
|
* @param int $term_id Term ID.
|
|
* @param string $meta_key Original `product_count_*` meta_key.
|
|
* @param mixed $meta_value Value to store.
|
|
* @return void
|
|
*/
|
|
private static function write_translated( int $term_id, string $meta_key, $meta_value ): void {
|
|
$translated = self::translate_key( $term_id, $meta_key );
|
|
$option_name = '_transient_' . $translated;
|
|
update_option( $option_name, $meta_value, false );
|
|
}
|
|
|
|
/**
|
|
* One-time migration helper: count `product_count_*` rows in wp_termmeta.
|
|
*
|
|
* @return int
|
|
*/
|
|
public static function count_legacy_termmeta_rows(): int {
|
|
global $wpdb;
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
return (int) $wpdb->get_var(
|
|
"SELECT COUNT(*) FROM {$wpdb->termmeta} WHERE meta_key LIKE 'product\\_count\\_%'"
|
|
);
|
|
// phpcs:enable
|
|
}
|
|
|
|
/**
|
|
* One-time migration: DELETE all historical `product_count_*` rows from
|
|
* wp_termmeta. Future writes auto-route to wp_options.
|
|
*
|
|
* @return int Rows deleted.
|
|
*/
|
|
public static function purge_legacy_termmeta_rows(): int {
|
|
global $wpdb;
|
|
// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
|
return (int) $wpdb->query(
|
|
"DELETE FROM {$wpdb->termmeta} WHERE meta_key LIKE 'product\\_count\\_%'"
|
|
);
|
|
// phpcs:enable
|
|
}
|
|
}
|