$count ) {
$types[] = ucfirst( (string) $type ) . ': ' . number_format_i18n( (int) $count );
}
echo '
';
$head = class_exists( 'AIEO_Chrome' ) ? AIEO_Chrome::page_head( 'aieo-recommendations-cache', __( 'Recommendations Cache', 'ai-eshop-optimizer' ), 'database', 'aieo-recommendations-cache' ) : '';
echo '' !== $head ? $head : '
' . esc_html__( 'Recommendations Cache', 'ai-eshop-optimizer' ) . '
';
$board = class_exists( '\Rooster\Admin\Board' ) && \Rooster\Admin\Board::start( 'aieo-recommendations-cache' );
list( $open, $close ) = AIEO_DMM_UI::card( array(
'anchor' => 'rec-cache-stats',
'icon' => 'database',
'title' => __( 'Cache Statistics', 'ai-eshop-optimizer' ),
'footer' => \Rooster\Admin\Ui::button( __( 'Clear Expired Entries', 'ai-eshop-optimizer' ), array( 'attrs' => array( 'id' => 'aieo-cache-clear-expired' ) ) )
. ' ' . \Rooster\Admin\Ui::button( __( 'Clear All Cache', 'ai-eshop-optimizer' ), array( 'attrs' => array( 'id' => 'aieo-cache-clear-all' ) + \Rooster\Admin\States::confirm_attrs( 'aieo-cache-clear-all-confirm' ) ) )
. ' ' . \Rooster\Admin\States::outcome( '', '', array( 'id' => 'aieo-cache-message' ) ),
) );
echo $open;
echo \Rooster\Admin\Ui::facts( array(
array(
'label' => __( 'Object Cache', 'ai-eshop-optimizer' ),
'value' => $stats['object_cache_enabled'] ? (string) $stats['object_cache_type'] : __( 'Not available', 'ai-eshop-optimizer' ),
'tone' => $stats['object_cache_enabled'] ? 'success' : '',
),
array( 'label' => __( 'Total Cached Products', 'ai-eshop-optimizer' ), 'value' => number_format_i18n( (int) $stats['total_entries'] ) ),
array( 'label' => __( 'Cache Size', 'ai-eshop-optimizer' ), 'value' => (string) $stats['size_formatted'] ),
array( 'label' => __( 'Expired Entries', 'ai-eshop-optimizer' ), 'value' => number_format_i18n( (int) $stats['expired_entries'] ) ),
array( 'label' => __( 'Entries by Type', 'ai-eshop-optimizer' ), 'value' => $types ? implode( ' · ', $types ) : __( 'No cached entries', 'ai-eshop-optimizer' ) ),
) );
echo $close;
list( $open, $close ) = AIEO_DMM_UI::card( array(
'anchor' => 'rec-cache-how',
'icon' => 'info',
'title' => __( 'How It Works', 'ai-eshop-optimizer' ),
'description' => __( 'The recommendations cache stores pre-rendered HTML for upsells, cross-sells, and related products. This eliminates expensive database queries during AJAX requests, significantly improving page load performance.', 'ai-eshop-optimizer' ),
) );
echo $open;
$plain = static function ( $label ) {
return rtrim( (string) $label, ': ' );
};
echo \Rooster\Admin\Ui::facts( array(
array( 'label' => $plain( __( 'Cache Hierarchy:', 'ai-eshop-optimizer' ) ), 'value' => __( 'Object Cache (Redis/Memcached) → Custom DB Table', 'ai-eshop-optimizer' ) ),
array( 'label' => $plain( __( 'Automatic Invalidation:', 'ai-eshop-optimizer' ) ), 'value' => __( 'Cache is automatically cleared when products are updated or deleted.', 'ai-eshop-optimizer' ) ),
array( 'label' => $plain( __( 'Expiration:', 'ai-eshop-optimizer' ) ), 'value' => __( 'Cache entries expire after 7 days and are automatically cleaned up.', 'ai-eshop-optimizer' ) ),
array( 'label' => $plain( __( 'Recently Viewed:', 'ai-eshop-optimizer' ) ), 'value' => __( 'Not cached (per-user/cookie-based, varies by visitor).', 'ai-eshop-optimizer' ) ),
) );
echo $close;
if ( $board ) {
\Rooster\Admin\Board::end();
}
echo \Rooster\Admin\States::confirm(
'aieo-cache-clear-all-confirm',
'reset',
__( 'Are you sure you want to clear all cached recommendations?', 'ai-eshop-optimizer' ),
'
' . esc_html__( 'Every cached recommendation is removed; each is built again the next time its product is viewed.', 'ai-eshop-optimizer' ) . '
',
array( 'confirm' => __( 'Clear All Cache', 'ai-eshop-optimizer' ) )
);
echo '
';
?>
__( 'Permission denied', 'ai-eshop-optimizer' ) ) );
}
$stats = self::get_stats();
wp_send_json_success( $stats );
}
public static function ajax_clear_all() {
check_ajax_referer( 'aieo_cache_admin', '_wpnonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied', 'ai-eshop-optimizer' ) ) );
}
$deleted = self::delete_all();
wp_send_json_success( array(
'message' => sprintf(
__( 'Cleared %d cache entries.', 'ai-eshop-optimizer' ),
$deleted
),
'deleted' => $deleted,
) );
}
public static function ajax_clear_expired() {
check_ajax_referer( 'aieo_cache_admin', '_wpnonce' );
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( array( 'message' => __( 'Permission denied', 'ai-eshop-optimizer' ) ) );
}
$deleted = self::delete_expired();
wp_send_json_success( array(
'message' => sprintf(
__( 'Cleared %d expired entries.', 'ai-eshop-optimizer' ),
$deleted
),
'deleted' => $deleted,
) );
}
public static function on_product_save( $post_id, $post, $update ) {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( $post->post_type !== 'product' ) {
return;
}
self::invalidate_referencing( $post_id );
self::delete( $post_id );
self::$synced_this_request[ (int) $post_id ] = true;
}
public static function on_product_update( $product_id, $product = null ) {
self::invalidate_referencing( $product_id );
self::delete( $product_id );
self::$synced_this_request[ (int) $product_id ] = true;
}
public static function on_product_meta_update( $post_id, $post = null ) {
self::delete( $post_id );
}
public static function on_product_delete( $post_id, $post = null ) {
if ( $post && $post->post_type !== 'product' ) {
return;
}
self::invalidate_referencing( $post_id );
self::delete( $post_id );
self::$synced_this_request[ (int) $post_id ] = true;
}
public static function on_product_trash( $post_id ) {
$post = get_post( $post_id );
if ( ! $post || $post->post_type !== 'product' ) {
return;
}
self::invalidate_referencing( $post_id );
self::delete( $post_id );
self::$synced_this_request[ (int) $post_id ] = true;
}
public static function on_price_meta_write( $meta_id, $object_id, $meta_key, $meta_value = null ) {
static $price_keys = array(
'_price' => 1,
'_sale_price' => 1,
'_regular_price' => 1,
'_sale_price_dates_from' => 1,
'_sale_price_dates_to' => 1,
);
if ( ! isset( $price_keys[ $meta_key ] ) ) {
return;
}
if ( self::$storm_mode ) {
return;
}
$pid = self::normalize_to_parent_product_id( (int) $object_id );
if ( $pid > 0 ) {
self::queue_price_invalidation( $pid );
}
}
public static function on_scheduled_sales( $product_ids ) {
foreach ( (array) $product_ids as $id ) {
$pid = self::normalize_to_parent_product_id( (int) $id );
if ( $pid > 0 ) {
self::queue_price_invalidation( $pid );
}
}
}
public static function on_variation_update( $variation_id ) {
$pid = self::normalize_to_parent_product_id( (int) $variation_id );
if ( $pid > 0 ) {
self::queue_price_invalidation( $pid );
}
}
private static function normalize_to_parent_product_id( $object_id ) {
if ( $object_id <= 0 ) {
return 0;
}
$post = get_post( $object_id );
if ( ! $post ) {
return 0;
}
if ( 'product' === $post->post_type ) {
return (int) $object_id;
}
if ( 'product_variation' === $post->post_type ) {
return (int) $post->post_parent;
}
return 0;
}
public static function queue_price_invalidation( $product_id ) {
$product_id = (int) $product_id;
if ( $product_id <= 0 ) {
return;
}
unset( self::$synced_this_request[ $product_id ] );
if ( self::$storm_mode ) {
return;
}
self::$pending_price_invalidations[ $product_id ] = true;
if ( ! self::$shutdown_flush_registered ) {
add_action( 'shutdown', array( __CLASS__, 'flush_price_invalidations' ), 5 );
self::$shutdown_flush_registered = true;
}
$threshold = (int) apply_filters( 'aieo_reco_cache_storm_threshold', 100 );
if ( count( self::$pending_price_invalidations ) > $threshold ) {
self::$storm_mode = true;
self::$pending_price_invalidations = array();
}
}
public static function flush_price_invalidations() {
if ( self::$storm_mode ) {
self::delete_all();
self::$storm_mode = false;
do_action( 'aieo_reco_cache_price_storm_flushed' );
return;
}
if ( empty( self::$pending_price_invalidations ) ) {
return;
}
$pids = array_keys( self::$pending_price_invalidations );
self::$pending_price_invalidations = array();
$processed = array();
foreach ( $pids as $pid ) {
if ( isset( self::$synced_this_request[ $pid ] ) ) {
continue;
}
self::invalidate_referencing( $pid );
self::delete( $pid );
$processed[] = $pid;
}
if ( ! empty( $processed ) ) {
do_action( 'aieo_reco_cache_price_invalidated', $processed );
}
}
public static function get_table_name() {
global $wpdb;
return $wpdb->prefix . self::$table_name;
}
public static function is_object_cache_available() {
if ( self::$object_cache_available === null ) {
if ( function_exists( 'wp_using_ext_object_cache' ) ) {
self::$object_cache_available = wp_using_ext_object_cache();
} else {
self::$object_cache_available = file_exists( WP_CONTENT_DIR . '/object-cache.php' );
}
}
return self::$object_cache_available;
}
private static function get_current_locale() {
if ( defined( 'ICL_LANGUAGE_CODE' ) ) {
return ICL_LANGUAGE_CODE;
}
if ( function_exists( 'pll_current_language' ) ) {
$lang = pll_current_language();
if ( $lang ) {
return $lang;
}
}
return get_locale();
}
private static function get_cache_key( $product_id, $relation_type ) {
$locale = self::get_current_locale();
return 'aieo_rec_' . $product_id . '_' . $relation_type . '_' . $locale;
}
private static function get_localized_relation_type( $relation_type ) {
$locale = self::get_current_locale();
return $relation_type . '_' . $locale;
}
public static function create_table() {
global $wpdb;
$table_name = self::get_table_name();
$charset_collate = aieo_get_charset_collate();
if ( self::table_exists() ) {
return false;
}
$sql = "CREATE TABLE {$table_name} (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
product_id bigint(20) UNSIGNED NOT NULL,
relation_type varchar(50) NOT NULL,
recommended_products longtext DEFAULT NULL,
created datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
expires_at datetime DEFAULT NULL,
PRIMARY KEY (id),
UNIQUE KEY idx_product_relation (product_id, relation_type),
KEY idx_relation_type (relation_type),
KEY idx_expires_at (expires_at)
) $charset_collate;";
require_once( ABSPATH . 'wp-admin/includes/upgrade.php' );
dbDelta( $sql );
self::$table_exists_cache = true;
return true;
}
public static function table_exists( $force_check = false ) {
if ( self::$table_exists_cache !== null && ! $force_check ) {
return self::$table_exists_cache;
}
global $wpdb;
$table_name = self::get_table_name();
self::$table_exists_cache = $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) === $table_name;
return self::$table_exists_cache;
}
private static function ensure_table_exists() {
if ( ! self::table_exists() ) {
self::create_table();
return true;
}
return false;
}
private static function is_valid_relation_type( $relation_type ) {
$relation_type = preg_replace( '/_a[a-z0-9]{2,16}$/', '', $relation_type, 1 );
if ( in_array( $relation_type, self::$valid_relation_types, true ) ) {
return true;
}
if ( preg_match( '/^(.+)_r[0-9a-f]{6,32}$/', $relation_type, $m )
&& in_array( $m[1], self::$valid_relation_types, true ) ) {
return true;
}
return false;
}
public static function get( $product_id, $relation_type ) {
global $wpdb;
$product_id = absint( $product_id );
if ( ! $product_id || ! self::is_valid_relation_type( $relation_type ) ) {
return false;
}
$cache_key = self::get_cache_key( $product_id, $relation_type );
if ( self::is_object_cache_available() ) {
$found = false;
$value = wp_cache_get( $cache_key, self::$object_cache_group, false, $found );
if ( $found && $value !== false ) {
return $value;
}
}
$table_name = self::get_table_name();
if ( ! self::table_exists() ) {
return false;
}
$localized_relation_type = self::get_localized_relation_type( $relation_type );
$row = $wpdb->get_row( $wpdb->prepare(
"SELECT recommended_products, expires_at FROM {$table_name}
WHERE product_id = %d AND relation_type = %s",
$product_id,
$localized_relation_type
) );
if ( ! $row || empty( $row->recommended_products ) ) {
return false;
}
if ( $row->expires_at && strtotime( $row->expires_at ) < time() ) {
self::delete_by_localized_type( $product_id, $localized_relation_type );
return false;
}
$data = json_decode( $row->recommended_products, true );
if ( ! is_array( $data ) ) {
return false;
}
if ( self::is_object_cache_available() ) {
$remaining_ttl = $row->expires_at ? max( 0, strtotime( $row->expires_at ) - time() ) : 0;
wp_cache_set( $cache_key, $data, self::$object_cache_group, $remaining_ttl );
}
return $data;
}
public static function set( $product_id, $relation_type, $recommended_products, $expiration = 0 ) {
global $wpdb;
$product_id = absint( $product_id );
if ( ! $product_id || ! self::is_valid_relation_type( $relation_type ) ) {
return false;
}
if ( ! is_array( $recommended_products ) ) {
return false;
}
$cache_key = self::get_cache_key( $product_id, $relation_type );
$expiration = $expiration > 0 ? $expiration : self::$default_expiration;
$expires_at = gmdate( 'Y-m-d H:i:s', time() + $expiration );
if ( self::is_object_cache_available() ) {
wp_cache_set( $cache_key, $recommended_products, self::$object_cache_group, $expiration );
}
$table_name = self::get_table_name();
self::ensure_table_exists();
$localized_relation_type = self::get_localized_relation_type( $relation_type );
$json_data = wp_json_encode( $recommended_products, JSON_UNESCAPED_UNICODE );
$current_time = current_time( 'mysql', true );
$sql = $wpdb->prepare(
"INSERT INTO {$table_name} (product_id, relation_type, recommended_products, created, modified, expires_at)
VALUES (%d, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE
recommended_products = VALUES(recommended_products),
modified = VALUES(modified),
expires_at = VALUES(expires_at)",
$product_id,
$localized_relation_type,
$json_data,
$current_time,
$current_time,
$expires_at
);
$result = $wpdb->query( $sql );
if ( $result !== false && self::reverse_table_ready() ) {
$cache_entry_id = (int) $wpdb->get_var( $wpdb->prepare(
"SELECT id FROM {$table_name} WHERE product_id = %d AND relation_type = %s",
$product_id,
$localized_relation_type
) );
if ( $cache_entry_id > 0 ) {
self::sync_reverse_refs( $cache_entry_id, $recommended_products );
}
}
return $result !== false;
}
public static function delete( $product_id, $relation_type = null ) {
global $wpdb;
$product_id = absint( $product_id );
if ( ! $product_id ) {
return false;
}
if ( self::is_object_cache_available() ) {
if ( $relation_type !== null ) {
$cache_key = self::get_cache_key( $product_id, $relation_type );
wp_cache_delete( $cache_key, self::$object_cache_group );
} else {
$locales = self::get_common_locales();
foreach ( self::$valid_relation_types as $type ) {
foreach ( $locales as $locale ) {
$cache_key = 'aieo_rec_' . $product_id . '_' . $type . '_' . $locale;
wp_cache_delete( $cache_key, self::$object_cache_group );
}
}
}
}
$table_name = self::get_table_name();
if ( ! self::table_exists() ) {
return false;
}
if ( $relation_type !== null ) {
$localized_relation_type = self::get_localized_relation_type( $relation_type );
if ( self::reverse_table_ready() ) {
self::purge_reverse_refs_for_cache_ids( $wpdb->get_col( $wpdb->prepare(
"SELECT id FROM {$table_name} WHERE product_id = %d AND relation_type = %s",
$product_id,
$localized_relation_type
) ) );
}
$result = $wpdb->delete(
$table_name,
[
'product_id' => $product_id,
'relation_type' => $localized_relation_type,
],
[ '%d', '%s' ]
);
} else {
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT id, relation_type FROM {$table_name} WHERE product_id = %d",
$product_id
) );
if ( ! empty( $rows ) ) {
if ( self::is_object_cache_available() ) {
foreach ( $rows as $row ) {
$cache_key = 'aieo_rec_' . $product_id . '_' . $row->relation_type;
wp_cache_delete( $cache_key, self::$object_cache_group );
}
}
if ( self::reverse_table_ready() ) {
self::purge_reverse_refs_for_cache_ids( wp_list_pluck( $rows, 'id' ) );
}
}
$result = $wpdb->delete(
$table_name,
[ 'product_id' => $product_id ],
[ '%d' ]
);
}
return $result !== false;
}
private static function delete_by_localized_type( $product_id, $localized_relation_type ) {
global $wpdb;
$product_id = absint( $product_id );
if ( ! $product_id ) {
return false;
}
if ( self::is_object_cache_available() ) {
$cache_key = 'aieo_rec_' . $product_id . '_' . $localized_relation_type;
wp_cache_delete( $cache_key, self::$object_cache_group );
}
$table_name = self::get_table_name();
if ( ! self::table_exists() ) {
return false;
}
if ( self::reverse_table_ready() ) {
self::purge_reverse_refs_for_cache_ids( $wpdb->get_col( $wpdb->prepare(
"SELECT id FROM {$table_name} WHERE product_id = %d AND relation_type = %s",
$product_id,
$localized_relation_type
) ) );
}
$result = $wpdb->delete(
$table_name,
[
'product_id' => $product_id,
'relation_type' => $localized_relation_type,
],
[ '%d', '%s' ]
);
return $result !== false;
}
private static function get_common_locales() {
$locales = [ get_locale() ];
if ( function_exists( 'icl_get_languages' ) ) {
$languages = icl_get_languages( 'skip_missing=0' );
if ( is_array( $languages ) ) {
foreach ( $languages as $lang ) {
$locales[] = $lang['code'];
}
}
}
if ( function_exists( 'pll_languages_list' ) ) {
$pll_langs = pll_languages_list( [ 'fields' => 'slug' ] );
if ( is_array( $pll_langs ) ) {
$locales = array_merge( $locales, $pll_langs );
}
}
return array_unique( $locales );
}
public static function delete_expired() {
global $wpdb;
$table_name = self::get_table_name();
if ( ! self::table_exists() ) {
return 0;
}
$cutoff = $wpdb->get_var( "SELECT UTC_TIMESTAMP()" );
if ( self::is_object_cache_available() ) {
$expired = $wpdb->get_results( $wpdb->prepare(
"SELECT product_id, relation_type FROM {$table_name}
WHERE expires_at IS NOT NULL AND expires_at < %s",
$cutoff
) );
foreach ( $expired as $row ) {
$cache_key = 'aieo_rec_' . $row->product_id . '_' . $row->relation_type;
wp_cache_delete( $cache_key, self::$object_cache_group );
}
}
if ( self::reverse_table_ready() ) {
self::purge_reverse_refs_for_cache_ids( $wpdb->get_col( $wpdb->prepare(
"SELECT id FROM {$table_name}
WHERE expires_at IS NOT NULL AND expires_at < %s",
$cutoff
) ) );
}
$result = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$table_name}
WHERE expires_at IS NOT NULL AND expires_at < %s",
$cutoff
) );
return $result !== false ? $result : 0;
}
public static function delete_all() {
global $wpdb;
$table_name = self::get_table_name();
if ( self::is_object_cache_available() ) {
if ( function_exists( 'wp_cache_flush_group' ) ) {
wp_cache_flush_group( self::$object_cache_group );
} else {
if ( self::table_exists() ) {
$entries = $wpdb->get_results(
"SELECT product_id, relation_type FROM {$table_name}"
);
foreach ( $entries as $row ) {
$cache_key = 'aieo_rec_' . $row->product_id . '_' . $row->relation_type;
wp_cache_delete( $cache_key, self::$object_cache_group );
}
}
}
}
if ( ! self::table_exists() ) {
return 0;
}
$result = $wpdb->query( "TRUNCATE TABLE {$table_name}" );
if ( self::reverse_table_ready() ) {
$rev = self::reverse_table_name();
$wpdb->query( "TRUNCATE TABLE {$rev}" );
}
return $result !== false ? $result : 0;
}
public static function flush_all_cache() {
return self::delete_all();
}
public static function invalidate_referencing( $product_id ) {
global $wpdb;
$product_id = absint( $product_id );
if ( ! $product_id ) {
return 0;
}
$table_name = self::get_table_name();
if ( ! self::table_exists() ) {
return 0;
}
if ( self::reverse_table_ready() ) {
$rev = self::reverse_table_name();
$entry_ids = $wpdb->get_col( $wpdb->prepare(
"SELECT cache_entry_id FROM {$rev} WHERE referenced_product_id = %d",
$product_id
) );
if ( empty( $entry_ids ) ) {
return 0;
}
$entry_ids = array_values( array_filter( array_map( 'intval', $entry_ids ) ) );
$ph = implode( ', ', array_fill( 0, count( $entry_ids ), '%d' ) );
$referencing = $wpdb->get_results( $wpdb->prepare(
"SELECT id, product_id, relation_type FROM {$table_name} WHERE id IN ({$ph})",
...$entry_ids
) );
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$rev} WHERE cache_entry_id IN ({$ph})",
...$entry_ids
) );
} else {
$search_pattern = '%"product_id":' . $product_id . '%';
$alt_pattern = '%"product_id": ' . $product_id . '%';
$referencing = $wpdb->get_results( $wpdb->prepare(
"SELECT id, product_id, relation_type FROM {$table_name}
WHERE recommended_products LIKE %s OR recommended_products LIKE %s",
$search_pattern,
$alt_pattern
) );
}
if ( empty( $referencing ) ) {
return 0;
}
if ( self::is_object_cache_available() ) {
foreach ( $referencing as $row ) {
$cache_key = 'aieo_rec_' . $row->product_id . '_' . $row->relation_type;
wp_cache_delete( $cache_key, self::$object_cache_group );
}
}
$ids = array_map( fn( $row ) => $row->id, $referencing );
$placeholders = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
$result = $wpdb->query( $wpdb->prepare(
"DELETE FROM {$table_name} WHERE id IN ({$placeholders})",
...$ids
) );
return $result !== false ? $result : 0;
}
private static function reverse_table_name() {
global $wpdb;
return $wpdb->prefix . 'aieo_reco_reverse_refs';
}
private static function reverse_table_ready() {
static $ready = null;
if ( $ready !== null ) {
return $ready;
}
if ( get_option( 'aieo_reco_reverse_refs_ready' ) !== '1' ) {
$ready = false;
return $ready;
}
global $wpdb;
$t = self::reverse_table_name();
$ready = ( (string) $wpdb->get_var( $wpdb->prepare(
"SELECT table_name FROM information_schema.TABLES
WHERE table_schema = DATABASE() AND table_name = %s",
$t
) ) === $t );
return $ready;
}
private static function extract_referenced_ids( $recommended ) {
$ids = array();
self::collect_product_ids( $recommended, $ids );
return array_keys( $ids );
}
private static function collect_product_ids( $node, array &$ids ) {
if ( ! is_array( $node ) && ! is_object( $node ) ) {
return;
}
foreach ( (array) $node as $key => $val ) {
if ( 'product_id' === $key
&& ( is_int( $val ) || ( is_string( $val ) && ctype_digit( $val ) ) ) ) {
$pid = (int) $val;
if ( $pid > 0 ) {
$ids[ $pid ] = true;
}
}
if ( is_array( $val ) || is_object( $val ) ) {
self::collect_product_ids( $val, $ids );
}
}
}
private static function write_reverse_refs( $cache_entry_id, array $ids ) {
global $wpdb;
$cache_entry_id = (int) $cache_entry_id;
if ( $cache_entry_id <= 0 ) {
return;
}
$rev = self::reverse_table_name();
$wpdb->query( $wpdb->prepare( "DELETE FROM {$rev} WHERE cache_entry_id = %d", $cache_entry_id ) );
$values = array();
$args = array();
foreach ( $ids as $pid ) {
$pid = (int) $pid;
if ( $pid <= 0 ) {
continue;
}
$values[] = '(%d, %d)';
$args[] = $pid;
$args[] = $cache_entry_id;
}
if ( empty( $values ) ) {
return;
}
$wpdb->query( $wpdb->prepare(
"INSERT IGNORE INTO {$rev} (referenced_product_id, cache_entry_id) VALUES " . implode( ', ', $values ),
...$args
) );
}
private static function sync_reverse_refs( $cache_entry_id, $recommended ) {
if ( ! self::reverse_table_ready() ) {
return;
}
self::write_reverse_refs( $cache_entry_id, self::extract_referenced_ids( $recommended ) );
}
private static function purge_reverse_refs_for_cache_ids( $cache_ids ) {
if ( empty( $cache_ids ) || ! self::reverse_table_ready() ) {
return;
}
global $wpdb;
$rev = self::reverse_table_name();
$cache_ids = array_values( array_filter( array_unique( array_map( 'intval', (array) $cache_ids ) ) ) );
if ( empty( $cache_ids ) ) {
return;
}
$ph = implode( ', ', array_fill( 0, count( $cache_ids ), '%d' ) );
$wpdb->query( $wpdb->prepare(
"DELETE FROM {$rev} WHERE cache_entry_id IN ({$ph})",
...$cache_ids
) );
}
public static function backfill_reverse_refs( $start_id = 0, $max_rows = 0, $time_budget_seconds = 0 ) {
global $wpdb;
$table_name = self::get_table_name();
$rev = self::reverse_table_name();
$start_id = (int) $start_id;
if ( $wpdb->get_var( "SHOW TABLES LIKE '{$table_name}'" ) !== $table_name
|| $wpdb->get_var( "SHOW TABLES LIKE '{$rev}'" ) !== $rev ) {
return array( 'last_id' => $start_id, 'done' => true, 'processed' => 0 );
}
$last_id = $start_id;
$processed = 0;
$batch_size = 500;
$started = microtime( true );
do {
$rows = $wpdb->get_results( $wpdb->prepare(
"SELECT id, recommended_products FROM {$table_name}
WHERE id > %d ORDER BY id ASC LIMIT %d",
$last_id,
$batch_size
) );
if ( empty( $rows ) ) {
return array( 'last_id' => $last_id, 'done' => true, 'processed' => $processed );
}
$cache_ids = array();
$values = array();
$args = array();
foreach ( $rows as $row ) {
$cid = (int) $row->id;
$last_id = $cid;
$cache_ids[] = $cid;
$decoded = json_decode( (string) $row->recommended_products, true );
foreach ( self::extract_referenced_ids( $decoded ) as $pid ) {
$values[] = '(%d, %d)';
$args[] = (int) $pid;
$args[] = $cid;
}
++$processed;
}
$cph = implode( ', ', array_fill( 0, count( $cache_ids ), '%d' ) );
$wpdb->query( $wpdb->prepare( "DELETE FROM {$rev} WHERE cache_entry_id IN ({$cph})", ...$cache_ids ) );
if ( ! empty( $values ) ) {
$wpdb->query( $wpdb->prepare(
"INSERT IGNORE INTO {$rev} (referenced_product_id, cache_entry_id) VALUES " . implode( ', ', $values ),
...$args
) );
}
if ( $max_rows > 0 && $processed >= $max_rows ) {
return array( 'last_id' => $last_id, 'done' => false, 'processed' => $processed );
}
if ( $time_budget_seconds > 0 && ( microtime( true ) - $started ) >= $time_budget_seconds ) {
return array( 'last_id' => $last_id, 'done' => false, 'processed' => $processed );
}
} while ( count( $rows ) === $batch_size );
return array( 'last_id' => $last_id, 'done' => true, 'processed' => $processed );
}
public static function get_stats() {
global $wpdb;
$table_name = self::get_table_name();
$object_cache_enabled = self::is_object_cache_available();
$object_cache_type = 'None';
if ( $object_cache_enabled ) {
if ( class_exists( 'Redis' ) && defined( 'WP_REDIS_DISABLED' ) === false ) {
$object_cache_type = 'Redis';
} elseif ( class_exists( 'Memcached' ) ) {
$object_cache_type = 'Memcached';
} elseif ( class_exists( 'Memcache' ) ) {
$object_cache_type = 'Memcache';
} elseif ( class_exists( 'RedisCachePro\\ObjectCaches\\ObjectCache' ) ) {
$object_cache_type = 'Object Cache Pro (Redis)';
} else {
$object_cache_type = 'External (Unknown)';
}
}
if ( ! self::table_exists() ) {
return [
'total_entries' => 0,
'total_size' => 0,
'size_formatted' => '0 B',
'entries_by_type' => [],
'expired_entries' => 0,
'object_cache_enabled' => $object_cache_enabled,
'object_cache_type' => $object_cache_type,
];
}
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM {$table_name}" );
$size = (int) $wpdb->get_var( "SELECT SUM(LENGTH(recommended_products)) FROM {$table_name}" );
$by_type = $wpdb->get_results(
"SELECT relation_type, COUNT(*) as count
FROM {$table_name}
GROUP BY relation_type",
ARRAY_A
);
$entries_by_type = [];
foreach ( $by_type as $row ) {
$entries_by_type[ $row['relation_type'] ] = (int) $row['count'];
}
$expired = (int) $wpdb->get_var(
"SELECT COUNT(*) FROM {$table_name}
WHERE expires_at IS NOT NULL AND expires_at < UTC_TIMESTAMP()"
);
return [
'total_entries' => $total,
'total_size' => $size,
'size_formatted' => size_format( $size ),
'entries_by_type' => $entries_by_type,
'expired_entries' => $expired,
'object_cache_enabled' => $object_cache_enabled,
'object_cache_type' => $object_cache_type,
];
}
public static function warm_cache( $product_id, $fetcher ) {
$product_id = absint( $product_id );
if ( ! $product_id || ! is_callable( $fetcher ) ) {
return [];
}
$results = [];
$relation_types = [ 'upsells', 'crosssells', 'related' ];
foreach ( $relation_types as $relation_type ) {
try {
$recommendations = call_user_func( $fetcher, $product_id, $relation_type );
if ( is_array( $recommendations ) && ! empty( $recommendations ) ) {
$success = self::set( $product_id, $relation_type, $recommendations );
$results[ $relation_type ] = [
'success' => $success,
'count' => count( $recommendations ),
];
} else {
$results[ $relation_type ] = [
'success' => true,
'count' => 0,
'skipped' => true,
];
}
} catch ( Exception $e ) {
$results[ $relation_type ] = [
'success' => false,
'error' => $e->getMessage(),
];
}
}
return $results;
}
public static function get_valid_relation_types() {
return self::$valid_relation_types;
}
}