Files
2meet-data-optimizer/includes/zones/class-tmdo-zone-cold.php
T
wpdev d36bb954d1 chore: initial snapshot of 2meet-data-optimizer 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

263 lines
8.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Zone C (Cold) handler for JSON blob storage with Object Cache integration.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Zone C (Cold) handler — JSON blob storage with Object Cache integration.
*
* Each post type gets its own table: wpdo_cold_{post_type}
* All cold meta_keys for a post are stored as a single JSON blob in the `data` column.
*
* Read path: Object Cache → cold table → wp_postmeta fallback
* Write path: cold table + Object Cache invalidation
*
* Best for profile/display data that is read often but queried rarely
* (e.g. hp_description, social links, extended profile fields).
*/
class TMDO_Zone_Cold {
/**
* Get the cold table name for a post type.
*
* @param string $post_type Post type slug.
* @return string Full table name.
*/
public static function table( string $post_type ): string {
return TMDO_DB::table( 'wpdo_cold_' . sanitize_key( $post_type ) );
}
/**
* Cache group for a post type.
*
* @param string $post_type Post type slug.
* @return string Cache group name.
*/
private static function cache_group( string $post_type ): string {
return 'wpdo_cold_' . sanitize_key( $post_type );
}
/**
* Ensure the cold table exists for a post type.
*
* @param string $post_type Post type slug.
* @return void
*/
public static function ensure_table( string $post_type ): void {
global $wpdb;
$table = self::table( $post_type );
if ( TMDO_IS_SQLITE ) {
$exists = $wpdb->get_var(
$wpdb->prepare( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=%s", $table )
);
} else {
$exists = $wpdb->get_var(
$wpdb->prepare( 'SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = %s', $table )
);
}
if ( ! $exists ) {
TMDO_Installer::create_cold_table( $post_type );
}
}
/**
* Get a single cold meta value for a post.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param string $meta_key Meta key.
* @return mixed|null
*/
public static function get( int $post_id, string $post_type, string $meta_key ): mixed {
$data = self::get_blob( $post_id, $post_type );
return $data[ $meta_key ] ?? null;
}
/**
* Get the full JSON blob for a post, with Object Cache layer.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @return array Decoded JSON data (meta_key => value).
*/
public static function get_blob( int $post_id, string $post_type ): array {
$group = self::cache_group( $post_type );
$cache_key = "cold_{$post_id}";
$cached = wp_cache_get( $cache_key, $group );
if ( false !== $cached ) {
return is_array( $cached ) ? $cached : array();
}
global $wpdb;
$table = self::table( $post_type );
$json = $wpdb->get_var(
$wpdb->prepare( "SELECT data FROM `{$table}` WHERE post_id = %d LIMIT 1", $post_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Cold::table() via TMDO_DB::table().
);
$data = $json ? json_decode( $json, true ) : array();
if ( ! is_array( $data ) ) {
$data = array();
}
// Get cache TTL from registry — 粒度化策略:取所有 cold fields 的最小 TTL
// 確保任何短 TTL 欄位(如 IG tokenv2.0.4 起預期 < 5 min)能在保護期內失效,
// 而非被某個長 TTL 欄位拖拽到 1h 才更新。原本的「取第一個」策略會視註冊順序
// 而異,無法保證安全。
// 同時提供 `wpdo_cold_cache_ttl` filter 讓 partner integration 進一步覆蓋.
$fields = TMDO_Schema_Registry::instance()->get_zone_fields_for_type( 'cold', $post_type );
$ttls = array();
foreach ( $fields as $field ) {
if ( ! empty( $field['cache_ttl'] ) ) {
$ttls[] = (int) $field['cache_ttl'];
}
}
$ttl = empty( $ttls ) ? HOUR_IN_SECONDS : min( $ttls );
/**
* Filter the effective cache TTL for a cold zone post type.
*
* @since 2.0.5
* @param int $ttl Computed TTL (min of all registered fields, or HOUR_IN_SECONDS default).
* @param string $post_type Post type being cached.
* @param int $post_id Post ID (entity being read).
* @param array $fields Fields registry config for this post_type.
*/
$ttl = (int) apply_filters( 'wpdo_cold_cache_ttl', $ttl, $post_type, $post_id, $fields );
wp_cache_set( $cache_key, $data, $group, $ttl );
return $data;
}
/**
* Set a single cold meta value for a post.
* Merges into the existing JSON blob.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param string $meta_key Meta key.
* @param mixed $value Value to store.
*/
public static function set( int $post_id, string $post_type, string $meta_key, mixed $value ): void {
$data = self::get_blob_raw( $post_id, $post_type );
$data[ $meta_key ] = $value;
self::save_blob( $post_id, $post_type, $data );
}
/**
* Set multiple cold meta values at once.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param array $values meta_key => value pairs.
*/
public static function set_many( int $post_id, string $post_type, array $values ): void {
$data = self::get_blob_raw( $post_id, $post_type );
$data = array_merge( $data, $values );
self::save_blob( $post_id, $post_type, $data );
}
/**
* Remove a key from the cold blob.
*
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
* @param string $meta_key Meta key to remove.
* @return void
*/
public static function remove( int $post_id, string $post_type, string $meta_key ): void {
$data = self::get_blob_raw( $post_id, $post_type );
unset( $data[ $meta_key ] );
self::save_blob( $post_id, $post_type, $data );
}
/**
* Delete the entire cold row for a post.
*
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
* @return void
*/
public static function delete( int $post_id, string $post_type ): void {
global $wpdb;
$wpdb->delete( self::table( $post_type ), array( 'post_id' => $post_id ), array( '%d' ) );
wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) );
}
// ── Private helpers ───────────────────────────────────────────────────
/**
* Get raw blob from DB (no cache).
*
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
* @return array Decoded JSON data array.
*/
private static function get_blob_raw( int $post_id, string $post_type ): array {
global $wpdb;
$table = self::table( $post_type );
$json = $wpdb->get_var(
$wpdb->prepare( "SELECT data FROM `{$table}` WHERE post_id = %d LIMIT 1", $post_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Cold::table() via TMDO_DB::table().
);
$data = $json ? json_decode( $json, true ) : array();
return is_array( $data ) ? $data : array();
}
/**
* Save the JSON blob and invalidate cache.
*
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
* @param array $data Key-value pairs to store as JSON.
* @return void
*/
private static function save_blob( int $post_id, string $post_type, array $data ): void {
global $wpdb;
$table = self::table( $post_type );
$now = TMDO_DB::now();
$json = wp_json_encode( $data );
$existing = $wpdb->get_var(
$wpdb->prepare( "SELECT id FROM `{$table}` WHERE post_id = %d LIMIT 1", $post_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Cold::table() via TMDO_DB::table().
);
if ( $existing ) {
$wpdb->update(
$table,
array(
'data' => $json,
'updated_at' => $now,
),
array( 'post_id' => $post_id ),
array( '%s', '%s' ),
array( '%d' )
);
} else {
$wpdb->insert(
$table,
array(
'post_id' => $post_id,
'data' => $json,
'updated_at' => $now,
),
array( '%d', '%s', '%s' )
);
}
// Invalidate object cache.
wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) );
}
}