Files
2meet-data-optimizer/includes/zones/class-tmdo-zone-archive.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

272 lines
7.4 KiB
PHP

<?php
/**
* Zone D (Archive) handler for historical data archival.
*
* @package WP_Data_Optimizer
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Zone D (Archive) handler — historical data archival with optional compression.
*
* Single table: wpdo_archive
* Stores old/infrequently accessed postmeta entries with optional gzip compression.
*
* Archive flow:
* 1. Identify cold/stale postmeta entries (by age, post_status, or manual selection)
* 2. Copy to wpdo_archive with original_meta_id reference
* 3. Optionally compress meta_value with gzencode()
* 4. Delete from wp_postmeta (only during cleanup phase)
*
* Retrieval: decompress on read, restore to postmeta if needed.
*/
class TMDO_Zone_Archive {
/**
* Get the archive table name.
*/
public static function table(): string {
return TMDO_DB::table( 'wpdo_archive' );
}
/**
* Archive a single postmeta entry.
*
* @param int $post_id Post ID.
* @param string $post_type Post type.
* @param string $meta_key Meta key.
* @param string $meta_value Meta value.
* @param int $meta_id Original meta_id from wp_postmeta.
* @param bool $compress Whether to gzip-compress the value.
*/
public static function archive( int $post_id, string $post_type, string $meta_key, string $meta_value, int $meta_id = 0, bool $compress = false ): void {
global $wpdb;
$table = self::table();
$compressed = 0;
if ( $compress && function_exists( 'gzencode' ) ) {
$meta_value = base64_encode( gzencode( $meta_value, 6 ) );
$compressed = 1;
}
$wpdb->insert(
$table,
array(
'post_id' => $post_id,
'post_type' => $post_type,
'meta_key' => $meta_key,
'meta_value' => $meta_value,
'compressed' => $compressed,
'archived_at' => TMDO_DB::now(),
'original_meta_id' => $meta_id,
),
array( '%d', '%s', '%s', '%s', '%d', '%s', '%d' )
);
}
/**
* Archive multiple postmeta entries in batch.
*
* @param array $entries Array of [post_id, post_type, meta_key, meta_value, meta_id].
* @param bool $compress Whether to compress values.
* @return void
* @throws \Throwable When a batch insert fails and the transaction is rolled back.
*/
public static function archive_batch( array $entries, bool $compress = false ): void {
TMDO_DB::begin();
try {
foreach ( $entries as $entry ) {
self::archive(
(int) $entry['post_id'],
$entry['post_type'],
$entry['meta_key'],
$entry['meta_value'],
(int) ( $entry['meta_id'] ?? 0 ),
$compress
);
}
TMDO_DB::commit();
} catch ( \Throwable $e ) {
TMDO_DB::rollback();
throw $e;
}
}
/**
* Retrieve archived values for a post.
*
* @param int $post_id Post ID.
* @param string|null $meta_key Optional specific meta_key filter.
* @return array Array of [meta_key, meta_value, archived_at, compressed].
*/
public static function get( int $post_id, ?string $meta_key = null ): array {
global $wpdb;
$table = self::table();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- Table name from TMDO_Zone_Archive::table() via TMDO_DB::table().
if ( $meta_key ) {
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT meta_key, meta_value, compressed, archived_at FROM `{$table}` WHERE post_id = %d AND meta_key = %s ORDER BY archived_at DESC",
$post_id,
$meta_key
),
ARRAY_A
);
} else {
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT meta_key, meta_value, compressed, archived_at FROM `{$table}` WHERE post_id = %d ORDER BY archived_at DESC",
$post_id
),
ARRAY_A
);
}
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
// Decompress where needed.
// Note: must iterate $rows directly (not $rows ?: []) to allow &$row to modify the original array.
foreach ( $rows as &$row ) {
if ( (int) $row['compressed'] && function_exists( 'gzdecode' ) ) {
$decoded = base64_decode( $row['meta_value'] );
if ( false !== $decoded ) {
$decompressed = gzdecode( $decoded );
if ( false !== $decompressed ) {
$row['meta_value'] = $decompressed;
}
}
}
unset( $row['compressed'] );
}
return $rows ?: array();
}
/**
* Restore archived entries back to wp_postmeta.
*
* @param int $post_id Post ID.
* @param string|null $meta_key Optional meta_key filter (null = restore all).
* @return int Number of entries restored.
*/
public static function restore( int $post_id, ?string $meta_key = null ): int {
$entries = self::get( $post_id, $meta_key );
$count = 0;
foreach ( $entries as $entry ) {
update_post_meta( $post_id, $entry['meta_key'], $entry['meta_value'] );
++$count;
}
// Delete restored entries from archive.
if ( $count > 0 ) {
global $wpdb;
$table = self::table();
if ( $meta_key ) {
$wpdb->delete(
$table,
array(
'post_id' => $post_id,
'meta_key' => $meta_key,
),
array( '%d', '%s' )
);
} else {
$wpdb->delete( $table, array( 'post_id' => $post_id ), array( '%d' ) );
}
}
return $count;
}
/**
* Delete all archived entries for a post.
*
* @param int $post_id Post ID.
* @return void
*/
public static function delete( int $post_id ): void {
global $wpdb;
$wpdb->delete( self::table(), array( 'post_id' => $post_id ), array( '%d' ) );
}
/**
* Sweep: archive stale postmeta entries by age.
*
* Finds postmeta for trashed/deleted posts older than $days and archives them.
*
* @param int $days Minimum age in days.
* @param bool $compress Whether to compress.
* @param int $limit Maximum rows per sweep.
* @return int Number of entries archived.
*/
public static function sweep( int $days = 90, bool $compress = true, int $limit = 500 ): int {
global $wpdb;
$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( $days * DAY_IN_SECONDS ) );
$rows = $wpdb->get_results(
$wpdb->prepare(
"SELECT pm.meta_id, pm.post_id, pm.meta_key, pm.meta_value, p.post_type
FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.post_status = 'trash'
AND p.post_modified_gmt < %s
LIMIT %d",
$cutoff,
$limit
),
ARRAY_A
);
if ( ! $rows ) {
return 0;
}
$entries = array();
foreach ( $rows as $row ) {
$entries[] = array(
'post_id' => $row['post_id'],
'post_type' => $row['post_type'],
'meta_key' => $row['meta_key'],
'meta_value' => $row['meta_value'],
'meta_id' => $row['meta_id'],
);
}
self::archive_batch( $entries, $compress );
return count( $entries );
}
/**
* Get archive statistics.
*
* @return array{total_rows: int, compressed_rows: int, post_types: array}
*/
public static function stats(): array {
global $wpdb;
$table = self::table();
// phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from internal self::table()
$total = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" );
$compressed = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}` WHERE compressed = 1" );
$types = $wpdb->get_results(
"SELECT post_type, COUNT(*) as cnt FROM `{$table}` GROUP BY post_type ORDER BY cnt DESC",
ARRAY_A
);
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
return array(
'total_rows' => $total,
'compressed_rows' => $compressed,
'post_types' => $types ?: array(),
);
}
}