Files
2meet-data-optimizer/includes/engine/class-tmdo-cache-orchestrator.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

206 lines
7.3 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
/**
* TMDO_Cache_Orchestrator - 多層快取協調器
*
* 層級:
* L1: PHP Process Memory(同一請求內,靜態陣列)
* L2: WP Object Cache(搭配 Redis/Memcached 外掛時為跨請求)
* L3: Transientfallback,資料庫持久化)
*
* @package WP_Data_Optimizer
*/
// phpcs:disable Squiz.Commenting,Generic.Commenting,WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,Generic.CodeAnalysis.UnusedFunctionParameter,Generic.CodeAnalysis.EmptyStatement,Squiz.PHP.DisallowMultipleAssignments,Squiz.PHP.DisallowSizeFunctionsInLoops,WordPress.WP.I18n.MissingTranslatorsComment,WordPress.PHP.NoSilencedErrors,WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents,Squiz.PHP.CommentedOutCode,Universal.NamingConventions.NoReservedKeywordParameterNames,WordPress.PHP.YodaConditions,Squiz.Commenting.InlineComment.InvalidEndChar -- PR-1 ported from UAE; cleanup PR scheduled.
defined( 'ABSPATH' ) || exit;
final class TMDO_Cache_Orchestrator {
/** @var array<string, mixed> L1 記憶體快取 */
private static array $l1_cache = array();
/** @var int L1 快取計數上限(防記憶體爆炸) */
private const L1_MAX_ITEMS = 1000;
/** @var int L2 TTL 秒數 */
private const L2_TTL = HOUR_IN_SECONDS;
public static function init(): void {
// 註冊 L1 清理(避免長命令列任務記憶體膨脹)
add_action( 'shutdown', array( self::class, 'clear_l1' ) );
}
/**
* 產生快取 key
*/
private static function make_key( string $type, int $id, string $group ): string {
return "{$type}:{$id}:{$group}";
}
// ─────────────────────────────────────────────────────────
// 讀取
// ─────────────────────────────────────────────────────────
public static function get_row( string $type, int $id, string $group ) {
$key = self::make_key( $type, $id, $group );
// L1
if ( array_key_exists( $key, self::$l1_cache ) ) {
return self::$l1_cache[ $key ];
}
// L2
$cached = wp_cache_get( $key, TMDO_CACHE_GROUP );
if ( $cached !== false ) {
self::set_l1( $key, $cached );
return $cached;
}
return false;
}
// ─────────────────────────────────────────────────────────
// 寫入
// ─────────────────────────────────────────────────────────
public static function set_row( string $type, int $id, string $group, array $data ): void {
$key = self::make_key( $type, $id, $group );
self::set_l1( $key, $data );
wp_cache_set( $key, $data, TMDO_CACHE_GROUP, self::L2_TTL );
}
private static function set_l1( string $key, $data ): void {
// 防 L1 無限膨脹
if ( count( self::$l1_cache ) >= self::L1_MAX_ITEMS ) {
// 簡易 FIFO:砍掉最舊的 20%
$keep = (int) ( self::L1_MAX_ITEMS * 0.8 );
self::$l1_cache = array_slice( self::$l1_cache, - $keep, null, true );
}
self::$l1_cache[ $key ] = $data;
}
// ─────────────────────────────────────────────────────────
// 失效
// ─────────────────────────────────────────────────────────
public static function invalidate( string $type, int $id, string $group ): void {
$key = self::make_key( $type, $id, $group );
unset( self::$l1_cache[ $key ] );
wp_cache_delete( $key, TMDO_CACHE_GROUP );
}
public static function flush_entity( string $type, ?int $id = null ): void {
// 若沒給 id → flush 整個 entity type(L1 中所有該 type 的 key)
if ( $id === null ) {
$prefix = 'uae:' . $type . ':';
foreach ( array_keys( self::$l1_cache ) as $key ) {
if ( strpos( $key, $prefix ) === 0 ) {
unset( self::$l1_cache[ $key ] );
}
}
// Object cache 沒法做 prefix flush,用版本號 bump
wp_cache_set( 'wpdo_cache_version', time(), TMDO_CACHE_GROUP );
return;
}
$groups = TMDO_Entity_Registry::get_groups_for_type( $type );
foreach ( $groups as $group ) {
self::invalidate( $type, $id, $group );
}
}
public static function flush_all(): void {
self::$l1_cache = array();
// WP Object Cache 無跨群組 flush,改用版本號方式
wp_cache_set( 'wpdo_cache_version', time(), TMDO_CACHE_GROUP );
}
public static function clear_l1(): void {
self::$l1_cache = array();
}
// ─────────────────────────────────────────────────────────
// 批次預熱(核心效能優化:解決清單頁 N+1)
// ─────────────────────────────────────────────────────────
/**
* 批次預載一組 entity IDs 的資料至快取
* 用於清單頁渲染前呼叫,避免每個 item 都去查 DB
*
* @param string $type 實體類型
* @param array $ids entity ID 陣列
* @param string $group 欄位群組
*/
public static function warm_batch( string $type, array $ids, string $group ): void {
if ( empty( $ids ) ) {
return;
}
// 找出未快取的 IDs
$uncached_ids = array_filter(
$ids,
function ( $id ) use ( $type, $group ) {
return self::get_row( $type, (int) $id, $group ) === false;
}
);
if ( empty( $uncached_ids ) ) {
return;
}
global $wpdb;
$adapter = TMDO_Entity_Registry::get_adapter( $type );
if ( ! $adapter ) {
return;
}
$table = TMDO_Schema_Manager::get_table_name( $type, $group );
$id_col = $adapter->get_entity_id_column();
if ( ! TMDO_Schema_Manager::table_exists( $table ) ) {
return;
}
$ids_in = implode( ',', array_map( 'intval', $uncached_ids ) );
$rows = $wpdb->get_results(
"SELECT * FROM `{$table}` WHERE `{$id_col}` IN ({$ids_in})",
ARRAY_A
);
if ( ! is_array( $rows ) ) {
return;
}
// 建立 id → row 索引
$indexed = array();
foreach ( $rows as $row ) {
$indexed[ (int) $row[ $id_col ] ] = $row;
}
// 填入快取(未找到者存空陣列避免重查)
foreach ( $uncached_ids as $id ) {
$id_int = (int) $id;
self::set_row( $type, $id_int, $group, $indexed[ $id_int ] ?? array() );
}
}
// ─────────────────────────────────────────────────────────
// 統計(供後台顯示)
// ─────────────────────────────────────────────────────────
public static function get_l1_stats(): array {
return array(
'count' => count( self::$l1_cache ),
'max' => self::L1_MAX_ITEMS,
'usage_pct' => self::L1_MAX_ITEMS > 0
? round( count( self::$l1_cache ) / self::L1_MAX_ITEMS * 100, 1 )
: 0,
);
}
}