d36bb954d1
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
206 lines
7.3 KiB
PHP
206 lines
7.3 KiB
PHP
<?php
|
||
/**
|
||
* TMDO_Cache_Orchestrator - 多層快取協調器
|
||
*
|
||
* 層級:
|
||
* L1: PHP Process Memory(同一請求內,靜態陣列)
|
||
* L2: WP Object Cache(搭配 Redis/Memcached 外掛時為跨請求)
|
||
* L3: Transient(fallback,資料庫持久化)
|
||
*
|
||
* @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,
|
||
);
|
||
}
|
||
}
|