Files
2meet-data-optimizer/includes/integrations/class-tmdo-demo-entity-counter.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

228 lines
7.6 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
*/
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;
}
}