b4400a68e5
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
265 lines
8.5 KiB
PHP
265 lines
8.5 KiB
PHP
<?php
|
|
/**
|
|
* Listing stats integration for Zone B view counting.
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
*/
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Zone B/D integration — Listing Statistics & Archival.
|
|
*
|
|
* Zone B (Warm): Tracks per-listing page view counts with a 24-hour TTL.
|
|
* - Reads from wpdo_warm first; falls back to postmeta if no warm entry.
|
|
* - Increments on each front-end singular hp_listing page load (non-admin, non-bot).
|
|
* - Keeps postmeta clean by flushing accumulated counts via cron (wpdo_flush_views).
|
|
*
|
|
* Zone D (Archive): Archives hot-zone fields for expired listings.
|
|
* - Triggered by the existing wpdo_archive_sweep cron (daily).
|
|
* - Finds listings where hp_expired_time < now and expired > TMDO_ARCHIVE_DAYS ago.
|
|
* - Archives hp_price, hp_featured, hp_verified, hp_expired_time with gzip compression.
|
|
*/
|
|
class TMDO_Listing_Stats {
|
|
|
|
/** Meta key used for the warm-zone view counter. */
|
|
const VIEW_KEY = 'wpdo_views';
|
|
|
|
/** TTL for warm-zone view entries (24 hours). */
|
|
const VIEW_TTL = DAY_IN_SECONDS;
|
|
|
|
/** How many days after expiry before archiving hot fields. */
|
|
const ARCHIVE_DAYS = 30;
|
|
|
|
/** Hot-zone meta keys to archive for expired listings. */
|
|
const ARCHIVE_KEYS = array( 'hp_price', 'hp_featured', 'hp_verified', 'hp_expired_time' );
|
|
|
|
/**
|
|
* Register all hooks.
|
|
*/
|
|
public static function register_hooks(): void {
|
|
// Zone B: increment view count on front-end singular listing pages.
|
|
add_action( 'wp', array( __CLASS__, 'maybe_increment_view' ) );
|
|
|
|
// Zone B: flush accumulated warm-zone counts to postmeta (hourly).
|
|
add_action( 'wpdo_flush_views', array( __CLASS__, 'flush_views_to_postmeta' ) );
|
|
|
|
// Zone D: archive expired listing fields (daily cron via wpdo_archive_sweep).
|
|
add_action( 'wpdo_archive_sweep', array( __CLASS__, 'archive_expired_listings' ), 20 );
|
|
}
|
|
|
|
/**
|
|
* Schedule the flush cron if not already registered.
|
|
*
|
|
* Called from TMDO_Core::schedule_cron().
|
|
*/
|
|
public static function schedule_cron(): void {
|
|
if ( ! wp_next_scheduled( 'wpdo_flush_views' ) ) {
|
|
wp_schedule_event( time(), 'hourly', 'wpdo_flush_views' );
|
|
}
|
|
}
|
|
|
|
// ── Zone B: View Counter ─────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Increment the warm-zone view counter for the current listing page.
|
|
*
|
|
* Only fires on front-end, singular hp_listing pages, for non-bot requests.
|
|
*/
|
|
public static function maybe_increment_view(): void {
|
|
if ( is_admin() || ! is_singular( 'hp_listing' ) ) {
|
|
return;
|
|
}
|
|
|
|
// Skip common bots by checking for empty or known bot user-agents.
|
|
$ua = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) );
|
|
if ( empty( $ua ) || preg_match( '/bot|crawl|slurp|spider|mediapartners/i', $ua ) ) {
|
|
return;
|
|
}
|
|
|
|
$post_id = (int) get_the_ID();
|
|
if ( $post_id <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
self::increment_view( $post_id );
|
|
}
|
|
|
|
/**
|
|
* Increment the warm-zone view count for a listing.
|
|
*
|
|
* @param int $post_id Listing post ID.
|
|
*/
|
|
public static function increment_view( int $post_id ): void {
|
|
$current = (int) TMDO_Zone_Warm::get( $post_id, self::VIEW_KEY );
|
|
TMDO_Zone_Warm::set( $post_id, self::VIEW_KEY, (string) ( $current + 1 ), self::VIEW_TTL );
|
|
}
|
|
|
|
/**
|
|
* Read the view count for a listing.
|
|
*
|
|
* Returns warm-zone value if available; falls back to postmeta hp_view_count.
|
|
*
|
|
* @param int $post_id Listing post ID.
|
|
* @return int
|
|
*/
|
|
public static function get_view_count( int $post_id ): int {
|
|
$warm = TMDO_Zone_Warm::get( $post_id, self::VIEW_KEY );
|
|
if ( null !== $warm ) {
|
|
return (int) $warm;
|
|
}
|
|
return (int) get_post_meta( $post_id, 'hp_view_count', true );
|
|
}
|
|
|
|
/**
|
|
* Flush all warm-zone view counts to postmeta and reset warm entries.
|
|
*
|
|
* Called hourly by wpdo_flush_views cron.
|
|
*
|
|
* @return int Number of listings flushed.
|
|
*/
|
|
public static function flush_views_to_postmeta(): int {
|
|
global $wpdb;
|
|
|
|
$table = TMDO_Zone_Warm::table();
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT post_id, meta_value FROM `{$table}` WHERE meta_key = %s", // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
self::VIEW_KEY
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
if ( ! $rows ) {
|
|
return 0;
|
|
}
|
|
|
|
// Batch-fetch existing hp_view_count for all post IDs in one query.
|
|
$post_ids = array_map( 'intval', array_column( $rows, 'post_id' ) );
|
|
$placeholders = implode( ',', array_fill( 0, count( $post_ids ), '%d' ) );
|
|
$existing_rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = 'hp_view_count' AND post_id IN ({$placeholders})", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $wpdb->postmeta is core. phpcs:ignore WPDO.AntiEAV.no-direct-postmeta-select -- Listing Stats: Zone B → postmeta view-count sync path.
|
|
...$post_ids
|
|
),
|
|
ARRAY_A
|
|
);
|
|
$existing_map = array();
|
|
foreach ( $existing_rows as $er ) {
|
|
$existing_map[ (int) $er['post_id'] ] = (int) $er['meta_value'];
|
|
}
|
|
|
|
$flushed = 0;
|
|
foreach ( $rows as $row ) {
|
|
$post_id = (int) $row['post_id'];
|
|
$new_views = (int) $row['meta_value'];
|
|
|
|
// Add to existing postmeta total.
|
|
$existing = $existing_map[ $post_id ] ?? 0;
|
|
update_post_meta( $post_id, 'hp_view_count', $existing + $new_views );
|
|
|
|
// Remove the warm entry after flush.
|
|
TMDO_Zone_Warm::delete( $post_id, self::VIEW_KEY );
|
|
++$flushed;
|
|
}
|
|
|
|
return $flushed;
|
|
}
|
|
|
|
// ── Zone D: Expired Listing Archival ─────────────────────────────────────
|
|
|
|
/**
|
|
* Archive hot-zone fields for listings that expired ARCHIVE_DAYS ago.
|
|
*
|
|
* Reads hp_expired_time from postmeta (or hot zone via Sync Bridge).
|
|
* Archives ARCHIVE_KEYS with gzip compression into wpdo_archive.
|
|
*
|
|
* @param int $limit Max listings to process per run.
|
|
* @return int Number of listing fields archived.
|
|
*/
|
|
public static function archive_expired_listings( int $limit = 200 ): int {
|
|
global $wpdb;
|
|
|
|
$cutoff = (int) ( time() - self::ARCHIVE_DAYS * DAY_IN_SECONDS );
|
|
|
|
// Find published listings with hp_expired_time before cutoff.
|
|
$listings = $wpdb->get_col(
|
|
$wpdb->prepare(
|
|
"SELECT DISTINCT pm.post_id
|
|
FROM {$wpdb->postmeta} pm
|
|
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
|
|
WHERE pm.meta_key = 'hp_expired_time'
|
|
AND pm.meta_value > '0'
|
|
AND CAST(pm.meta_value AS UNSIGNED) < %d
|
|
AND p.post_type = 'hp_listing'
|
|
AND p.post_status IN ('publish','private')
|
|
LIMIT %d",
|
|
$cutoff,
|
|
$limit
|
|
)
|
|
);
|
|
|
|
if ( ! $listings ) {
|
|
return 0;
|
|
}
|
|
|
|
$listing_ids = array_map( 'intval', $listings );
|
|
$id_list = implode( ',', $listing_ids );
|
|
$key_list = implode( ',', array_fill( 0, count( self::ARCHIVE_KEYS ), '%s' ) );
|
|
|
|
// Batch-fetch all relevant postmeta rows in one query.
|
|
$meta_rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE post_id IN ({$id_list}) AND meta_key IN ({$key_list})", // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- $wpdb->postmeta is core. phpcs:ignore WPDO.AntiEAV.no-direct-postmeta-select -- Archive sweep: must read postmeta to migrate to Zone D.
|
|
...self::ARCHIVE_KEYS
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
// Build lookup: $meta_map[ post_id ][ meta_key ] = [ meta_id, meta_value ].
|
|
$meta_map = array();
|
|
foreach ( $meta_rows as $mr ) {
|
|
$meta_map[ (int) $mr['post_id'] ][ $mr['meta_key'] ] = array(
|
|
'meta_id' => (int) $mr['meta_id'],
|
|
'meta_value' => $mr['meta_value'],
|
|
);
|
|
}
|
|
|
|
$archived = 0;
|
|
foreach ( $listing_ids as $post_id ) {
|
|
foreach ( self::ARCHIVE_KEYS as $meta_key ) {
|
|
if ( ! isset( $meta_map[ $post_id ][ $meta_key ] ) ) {
|
|
continue;
|
|
}
|
|
$entry = $meta_map[ $post_id ][ $meta_key ];
|
|
$meta_value = $entry['meta_value'];
|
|
if ( '' === $meta_value || false === $meta_value ) {
|
|
continue;
|
|
}
|
|
TMDO_Zone_Archive::archive(
|
|
$post_id,
|
|
'hp_listing',
|
|
$meta_key,
|
|
(string) $meta_value,
|
|
$entry['meta_id'],
|
|
true // compress.
|
|
);
|
|
++$archived;
|
|
}
|
|
}
|
|
|
|
return $archived;
|
|
}
|
|
|
|
/**
|
|
* Get archive statistics for expired listings.
|
|
*
|
|
* @return array{total_rows: int, compressed_rows: int, post_types: array}
|
|
*/
|
|
public static function stats(): array {
|
|
return TMDO_Zone_Archive::stats();
|
|
}
|
|
}
|