Files
2meet-data-optimizer/includes/zones/class-tmdo-zone-cold.php
T
wpdev 76c01e44df refactor: 全部 128 個生產檔加入 declare(strict_types=1)(PR-H)
對齊 A v3.2.0。型別強制會把隱式轉換變成 TypeError,所以一次全檔加入
並跑完整測試(unit 451 / integration 398 全綠,無迴歸)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TbG1keQQ7XBa7qMQY16KCY
2026-07-31 06:13:33 +08:00

311 lines
10 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
*/
declare(strict_types=1);
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 {
self::save_patch( $post_id, $post_type, array( $meta_key => $value ) );
}
/**
* Set multiple cold meta values at once.
*
* Uses JSON_MERGE_PATCH on MySQL to avoid a read-then-write round trip.
* Falls back to read-modify-write on SQLite.
*
* @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 {
self::save_patch( $post_id, $post_type, $values );
}
/**
* Remove a key from the cold blob.
*
* Uses JSON_REMOVE on MySQL for an atomic single-key removal without a full
* blob read. Falls back to read-modify-write on SQLite.
*
* @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 {
if ( defined( 'TMDO_IS_SQLITE' ) && TMDO_IS_SQLITE ) {
$data = self::get_blob_raw( $post_id, $post_type );
unset( $data[ $meta_key ] );
self::save_blob( $post_id, $post_type, $data );
return;
}
global $wpdb;
$table = self::table( $post_type );
$path = '$.' . $meta_key;
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from TMDO_Zone_Cold::table() via TMDO_DB::table().
$wpdb->query(
$wpdb->prepare(
"UPDATE `{$table}` SET data = JSON_REMOVE(COALESCE(data, '{}'), %s), updated_at = %s WHERE post_id = %d",
$path,
TMDO_DB::now(),
$post_id
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) );
}
/**
* 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 {
$table = self::table( $post_type );
$json = wp_json_encode( $data );
TMDO_DB::upsert(
$table,
array(
'post_id' => $post_id,
'data' => $json,
'updated_at' => TMDO_DB::now(),
),
array( 'data', 'updated_at' ),
'post_id',
array( '%d', '%s', '%s' )
);
// Invalidate object cache.
wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) );
}
/**
* Merge a partial key-value map into the cold blob atomically.
*
* Uses INSERT ... ON DUPLICATE KEY UPDATE with JSON_MERGE_PATCH so concurrent
* writers cannot clobber each other's keys (the read-modify-write path did).
* On SQLite, falls back to the classic read-modify-write path.
*
* @param int $post_id Post ID.
* @param string $post_type Post type slug.
* @param array $patch Partial key-value map to merge in.
*/
private static function save_patch( int $post_id, string $post_type, array $patch ): void {
if ( defined( 'TMDO_IS_SQLITE' ) && TMDO_IS_SQLITE ) {
$data = self::get_blob_raw( $post_id, $post_type );
$data = array_merge( $data, $patch );
self::save_blob( $post_id, $post_type, $data );
return;
}
global $wpdb;
$table = self::table( $post_type );
$json = wp_json_encode( $patch );
$now = TMDO_DB::now();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Table name from TMDO_Zone_Cold::table() via TMDO_DB::table().
$wpdb->query(
$wpdb->prepare(
"INSERT INTO `{$table}` (post_id, data, updated_at) VALUES (%d, %s, %s)
ON DUPLICATE KEY UPDATE data = JSON_MERGE_PATCH(COALESCE(data, '{}'), %s), updated_at = %s",
$post_id,
$json,
$now,
$json,
$now
)
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) );
}
}