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