bind_anti_eav_hooks(); } /** Adapter slug (matches wp.org plugin slug). */ public function plugin_slug(): string { return 'hivepress-favorites'; } /** Class probed for addon presence. */ public function detection_class(): string { return 'HivePress\\Favorites\\Plugin'; } /** Version constant probed for addon presence. */ public function detection_const(): string { return 'HIVEPRESS_FAVORITES_VERSION'; } /** Minimum addon version supported. */ public function minimum_addon_version(): string { return '1.2.0'; } /** * Declare the shadow table to the Custom_Table_Registry. * * @param TMDO_Custom_Table_Registry $registry Registry singleton. */ public function on_register_custom_tables( TMDO_Custom_Table_Registry $registry ): void { $this->register_custom_table( $registry, array( 'table_name' => self::TABLE, 'primary_key' => 'comment_id', 'post_type_link' => null, 'expected_columns' => array( 'comment_id' => 'BIGINT UNSIGNED NOT NULL PRIMARY KEY', 'user_id' => 'BIGINT UNSIGNED NOT NULL', 'listing_id' => 'BIGINT UNSIGNED NOT NULL', 'created_at' => 'DATETIME NOT NULL', ), 'indexes' => array( 'unique_user_listing' => 'UNIQUE (user_id, listing_id)', 'idx_listing' => '(listing_id)', ), 'doctor_callback' => array( $this, 'doctor_check' ), ) ); } /** * Sync wp_comments → shadow table on insert. * * Bound during `on_register_event_hooks()`. The comment-router uses * the shadow table for "is favorited" lookups; sync keeps it consistent. */ public function on_register_event_hooks(): void { add_action( 'wp_insert_comment', array( $this, 'mirror_insert' ), 10, 2 ); add_action( 'deleted_comment', array( $this, 'mirror_delete' ), 10, 2 ); } /** * Mirror a newly-inserted hp_favorite into the shadow table. * * @param int $comment_id Comment ID. * @param object|\WP_Comment $comment Comment object. */ public function mirror_insert( int $comment_id, $comment ): void { if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { return; } global $wpdb; $user_id = (int) ( $comment->user_id ?? 0 ); $listing_id = (int) ( $comment->comment_post_ID ?? 0 ); if ( $user_id <= 0 || $listing_id <= 0 ) { return; } $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared $wpdb->prepare( 'INSERT IGNORE INTO `' . $wpdb->prefix . self::TABLE . '` (comment_id, user_id, listing_id, created_at) VALUES (%d, %d, %d, %s)', $comment_id, $user_id, $listing_id, $comment->comment_date ?? current_time( 'mysql' ) ) ); } /** * Remove the shadow row when a hp_favorite comment is deleted. * * @param int $comment_id Comment ID. * @param object|\WP_Comment $comment Comment object. */ public function mirror_delete( int $comment_id, $comment ): void { if ( ! is_object( $comment ) || ( $comment->comment_type ?? '' ) !== self::COMMENT_TYPE ) { return; } global $wpdb; $wpdb->delete( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prefix . self::TABLE, array( 'comment_id' => $comment_id ), array( '%d' ) ); } /** * Doctor probe — verify shadow table exists. * * @return array{ok:bool, message:string, details?:array} */ public function doctor_check(): array { global $wpdb; if ( ! isset( $wpdb ) || ! is_object( $wpdb ) ) { return array( 'ok' => false, 'message' => 'wpdb global not available', ); } $table = $wpdb->prefix . self::TABLE; try { $exists = (bool) $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ); if ( ! $exists ) { return array( 'ok' => false, 'message' => sprintf( 'hivepress-favorites: shadow table %s missing', $table ), 'details' => array( 'table' => $table ), ); } $rows = (int) $wpdb->get_var( "SELECT COUNT(*) FROM `{$table}`" ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared return array( 'ok' => true, 'message' => sprintf( 'hivepress-favorites: %s rows=%d', $table, $rows ), 'details' => array( 'table' => $table, 'rows' => $rows, ), ); } catch ( \Throwable $e ) { return array( 'ok' => false, 'message' => sprintf( 'hivepress-favorites doctor failed: %s', $e->getMessage() ), ); } } /** * 8-D self-score. * * D1: 1.0 — adapter never calls *_meta() * D2: 1.0 — uses wpdb prefix, no hardcoded table literals * D3: 1.0 — shadow table covers the model * D4: 1.0 — expected_columns + indexes registered * D5: 1.0 — listens to wp_insert_comment for cross-write consistency * D6: 1.0 — zero options/transients * D7: 1.0 — only queries own shadow table * D8: 1.0 — UNIQUE(user_id, listing_id) covering index */ public function suitability_score(): array { return $this->compose_score( array() ); } /** * FSM modules this adapter contributes. * * @return array */ public function migrations(): array { return array( 'comment_hp_favorite' ); } } } // end if ( ! class_exists )