76c01e44df
對齊 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
230 lines
7.7 KiB
PHP
230 lines
7.7 KiB
PHP
<?php
|
|
/**
|
|
* TMDO_Demo_Entity_Counter — production proof-of-life for entity adapters.
|
|
*
|
|
* Task D follow-up to PR-5: demonstrates that the entity adapter framework
|
|
* actually works end-to-end (not just stubs). Implements a "counter" pattern
|
|
* shared across post / user / term / comment entities — a common gamification
|
|
* primitive (user points, listing views, comment helpful_count, term usage).
|
|
*
|
|
* Storage:
|
|
* wp_wpdo_demo_entity_counters (entity_type, entity_id, counter_key, counter_value, updated_at)
|
|
*
|
|
* Lifecycle:
|
|
* - Plugin or test invokes ::set( 'user', 42, 'points', 50 )
|
|
* - This writes to BOTH wp_usermeta (native, preserved) AND wpdo_demo table
|
|
* - Reads come from wpdo_demo when feature flag entity_demo_counter == 'cutover',
|
|
* otherwise fall through to native usermeta (transparent fallback)
|
|
*
|
|
* @package WP_Data_Optimizer
|
|
* @since 2.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
// phpcs:disable Squiz.Commenting.FunctionComment.Missing,Squiz.Commenting.InlineComment.InvalidEndChar,Generic.Commenting.DocComment.MissingShort,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.PHP.YodaConditions,Generic.CodeAnalysis.EmptyStatement -- v2.0.0 partner integrations: pure registration helpers + intentional silent catches.
|
|
|
|
/**
|
|
* Demo entity counter — proves 4-entity adapter framework works end-to-end.
|
|
*/
|
|
final class TMDO_Demo_Entity_Counter {
|
|
|
|
/** Feature flag module name. */
|
|
public const MODULE = 'entity_demo_counter';
|
|
|
|
/** Custom table holding all counters. */
|
|
public const TABLE = 'wpdo_demo_entity_counters';
|
|
|
|
/**
|
|
* Idempotent install of the demo table.
|
|
*
|
|
* Called from TMDO_Installer::install_v2_tables() OR manually for demos.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function install_table(): void {
|
|
global $wpdb;
|
|
if ( ! function_exists( 'dbDelta' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
|
|
}
|
|
|
|
$charset = $wpdb->get_charset_collate();
|
|
$table = $wpdb->prefix . self::TABLE;
|
|
|
|
$sql = "CREATE TABLE {$table} (
|
|
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
|
|
entity_type varchar(20) NOT NULL DEFAULT '',
|
|
entity_id bigint(20) unsigned NOT NULL DEFAULT 0,
|
|
counter_key varchar(100) NOT NULL DEFAULT '',
|
|
counter_value bigint(20) NOT NULL DEFAULT 0,
|
|
updated_at datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
|
|
PRIMARY KEY (id),
|
|
UNIQUE KEY ui_entity_counter (entity_type, entity_id, counter_key),
|
|
KEY idx_lookup (entity_type, counter_key, counter_value),
|
|
KEY idx_entity (entity_type, entity_id)
|
|
) {$charset};";
|
|
dbDelta( $sql );
|
|
}
|
|
|
|
/**
|
|
* Drop the demo table — idempotent.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function drop_table(): void {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::TABLE;
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
|
$wpdb->query( "DROP TABLE IF EXISTS `{$table}`" );
|
|
}
|
|
|
|
/**
|
|
* Set a counter for any entity. Writes to BOTH native meta AND demo table
|
|
* when feature flag is in a write-active state. Otherwise writes native
|
|
* only (idle path — full backward compatibility).
|
|
*
|
|
* @param string $entity_type One of: post, user, term, comment.
|
|
* @param int $entity_id Entity ID.
|
|
* @param string $counter_key Counter slug (e.g. 'points', 'view_count').
|
|
* @param int $value New value.
|
|
* @return bool
|
|
*/
|
|
public static function set( string $entity_type, int $entity_id, string $counter_key, int $value ): bool {
|
|
if ( ! self::is_valid_entity( $entity_type ) ) {
|
|
return false;
|
|
}
|
|
|
|
// Always write the native meta first (durability anchor).
|
|
TMDO_API::set_entity( $entity_type, $entity_id, $counter_key, $value );
|
|
|
|
// Conditional dual-write to demo table.
|
|
if ( TMDO_Feature_Flags::is_write_active( self::MODULE ) ) {
|
|
self::write_to_table( $entity_type, $entity_id, $counter_key, $value );
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Read a counter value. Source depends on feature flag state:
|
|
* - read_custom (cutover/cleanup/complete) → demo table
|
|
* - otherwise → native meta (fallback)
|
|
*
|
|
* @param string $entity_type One of: post, user, term, comment.
|
|
* @param int $entity_id Entity ID.
|
|
* @param string $counter_key Counter slug.
|
|
* @return int
|
|
*/
|
|
public static function get( string $entity_type, int $entity_id, string $counter_key ): int {
|
|
if ( ! self::is_valid_entity( $entity_type ) ) {
|
|
return 0;
|
|
}
|
|
|
|
if ( TMDO_Feature_Flags::is_read_custom( self::MODULE ) ) {
|
|
$row = self::read_from_table( $entity_type, $entity_id, $counter_key );
|
|
if ( null !== $row ) {
|
|
return (int) $row;
|
|
}
|
|
// Fallback to native if zone row missing — graceful degradation.
|
|
}
|
|
|
|
return (int) TMDO_API::get_entity( $entity_type, $entity_id, $counter_key );
|
|
}
|
|
|
|
/**
|
|
* Top-N entities by counter value — the killer query that postmeta CANNOT
|
|
* do efficiently (requires full scan + filesort). Demonstrates the value of
|
|
* the entity adapter pattern.
|
|
*
|
|
* @param string $entity_type One of: post, user, term, comment.
|
|
* @param string $counter_key Counter slug.
|
|
* @param int $limit Max rows.
|
|
* @return array<int, array{entity_id:int, counter_value:int}>
|
|
*/
|
|
public static function top_n( string $entity_type, string $counter_key, int $limit = 10 ): array {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::TABLE;
|
|
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table name from constant.
|
|
$rows = $wpdb->get_results(
|
|
$wpdb->prepare(
|
|
"SELECT entity_id, counter_value FROM `{$table}` WHERE entity_type = %s AND counter_key = %s ORDER BY counter_value DESC LIMIT %d",
|
|
$entity_type,
|
|
$counter_key,
|
|
$limit
|
|
),
|
|
ARRAY_A
|
|
);
|
|
|
|
return array_map(
|
|
static fn( array $r ) => array(
|
|
'entity_id' => (int) $r['entity_id'],
|
|
'counter_value' => (int) $r['counter_value'],
|
|
),
|
|
$rows ?: array()
|
|
);
|
|
}
|
|
|
|
// ── Internals ──────────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* @param string $entity_type Entity type.
|
|
*/
|
|
private static function is_valid_entity( string $entity_type ): bool {
|
|
return in_array( $entity_type, array( 'post', 'user', 'term', 'comment' ), true );
|
|
}
|
|
|
|
/**
|
|
* Write a single counter value via UPSERT (1 round-trip).
|
|
*
|
|
* @param string $entity_type One of: post, user, term, comment.
|
|
* @param int $entity_id Entity ID.
|
|
* @param string $counter_key Counter slug.
|
|
* @param int $value New value.
|
|
* @return void
|
|
*/
|
|
private static function write_to_table( string $entity_type, int $entity_id, string $counter_key, int $value ): void {
|
|
global $wpdb;
|
|
TMDO_DB::upsert(
|
|
$wpdb->prefix . self::TABLE,
|
|
array(
|
|
'entity_type' => $entity_type,
|
|
'entity_id' => $entity_id,
|
|
'counter_key' => $counter_key,
|
|
'counter_value' => $value,
|
|
'updated_at' => current_time( 'mysql' ),
|
|
),
|
|
array( 'counter_value', 'updated_at' ),
|
|
array( 'entity_type', 'entity_id', 'counter_key' ),
|
|
array( '%s', '%d', '%s', '%d', '%s' )
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Read a single counter value from the demo table.
|
|
*
|
|
* @param string $entity_type Entity type.
|
|
* @param int $entity_id Entity ID.
|
|
* @param string $counter_key Counter slug.
|
|
* @return int|null Null when row absent.
|
|
*/
|
|
private static function read_from_table( string $entity_type, int $entity_id, string $counter_key ): ?int {
|
|
global $wpdb;
|
|
$table = $wpdb->prefix . self::TABLE;
|
|
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table from constant.
|
|
$value = $wpdb->get_var(
|
|
$wpdb->prepare(
|
|
"SELECT counter_value FROM `{$table}` WHERE entity_type = %s AND entity_id = %d AND counter_key = %s LIMIT 1",
|
|
$entity_type,
|
|
$entity_id,
|
|
$counter_key
|
|
)
|
|
);
|
|
return null === $value ? null : (int) $value;
|
|
}
|
|
}
|