Files
2meet-data-optimizer-hivepr…/includes/class-tmdo-listing-stats.php
T
wpdev b4400a68e5 chore: initial snapshot of 2meet-data-optimizer-hivepress-addon v0.1.0
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
2026-07-31 05:06:36 +08:00

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();
}
}