*/ private static array $created = array(); /** * Active pre_option callback, if any. * * @var callable|null */ private static $mode_filter = null; /** * Override entity modes for this process and make sure the Hook Bus is hooked. * * @param array $modes e.g. array( 'user' => 'shadow_read' ). */ public static function use_modes( array $modes ): void { self::clear_mode_filter(); $stored = get_option( 'wpdo_bridge_modes', array() ); $merged = array_merge( is_array( $stored ) ? $stored : array(), $modes ); self::$mode_filter = static function () use ( $merged ) { return $merged; }; add_filter( 'pre_option_wpdo_bridge_modes', self::$mode_filter ); self::reset_caches(); // The Hook Bus only boots when wpdo_hook_bus_enabled is on; the suite // must not depend on (or change) that site option. if ( false === has_filter( 'update_user_metadata', array( 'TMDO_Hook_Bus', 'intercept_update' ) ) ) { TMDO_Hook_Bus::init(); } } /** * Create a throwaway user. */ public static function user(): int { $login = 'tmdo_live_' . wp_generate_password( 6, false, false ); $id = wp_insert_user( array( 'user_login' => $login, 'user_email' => $login . '@example.test', 'user_pass' => wp_generate_password(), ) ); if ( is_wp_error( $id ) ) { throw new TMDO_Live_Failure( 'fixture user: ' . $id->get_error_message() ); } self::$created[] = array( 'user', (int) $id ); return (int) $id; } /** * Create a throwaway term. * * @param string $taxonomy Taxonomy. */ public static function term( string $taxonomy ): int { if ( ! taxonomy_exists( $taxonomy ) ) { throw new TMDO_Live_Skip( "taxonomy {$taxonomy} not registered on this site" ); } $t = wp_insert_term( 'tmdo-live-' . wp_generate_password( 6, false, false ), $taxonomy ); if ( is_wp_error( $t ) ) { throw new TMDO_Live_Failure( 'fixture term: ' . $t->get_error_message() ); } self::$created[] = array( 'term', (int) $t['term_id'], $taxonomy ); return (int) $t['term_id']; } /** * Create a throwaway post. * * @param string $post_type Post type. */ public static function post( string $post_type ): int { if ( ! post_type_exists( $post_type ) ) { throw new TMDO_Live_Skip( "post type {$post_type} not registered on this site" ); } $id = wp_insert_post( array( 'post_type' => $post_type, 'post_title' => 'tmdo-live-' . wp_generate_password( 6, false, false ), 'post_status' => 'draft', ), true ); if ( is_wp_error( $id ) ) { throw new TMDO_Live_Failure( 'fixture post: ' . $id->get_error_message() ); } self::$created[] = array( 'post', (int) $id ); return (int) $id; } /** * Create a throwaway comment (on a throwaway post of $post_type). * * @param string $post_type Post type of the parent post. * @param string $comment_type Comment type. */ public static function comment( string $post_type, string $comment_type = 'comment' ): int { $post_id = self::post( $post_type ); $id = wp_insert_comment( array( 'comment_post_ID' => $post_id, 'comment_content' => 'tmdo-live', 'comment_type' => $comment_type, 'comment_approved' => 1, ) ); if ( ! $id ) { throw new TMDO_Live_Failure( 'fixture comment could not be created' ); } self::$created[] = array( 'comment', (int) $id ); return (int) $id; } /** * Native meta values for a key, read with SQL (no filters, no cache). * * @param string $type Entity type (post|user|term|comment). * @param int $id Entity ID. * @param string $key Meta key. * @return string[] */ public static function native( string $type, int $id, string $key ): array { global $wpdb; $table = _get_meta_table( $type ); $id_col = $type . '_id'; $pk = 'user' === $type ? 'umeta_id' : 'meta_id'; // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column names are WP internals. return $wpdb->get_col( $wpdb->prepare( "SELECT meta_value FROM {$table} WHERE {$id_col} = %d AND meta_key = %s ORDER BY {$pk}", $id, $key ) ); } /** * Raw flat-table value for a key; 'NO-ROW' when the entity has no row. * * @param string $type Entity type. * @param int $id Entity ID. * @param string $key Meta key. * @param string $post_type For posts: the post type whose group owns the key. * @return string|null */ public static function flat( string $type, int $id, string $key, string $post_type = '' ) { global $wpdb; $field = self::field( $type, $key, $post_type ); $table = TMDO_Schema_Manager::get_table_name( $type, $field['group'] ); $col = TMDO_Schema_Manager::sanitize_column_name( $key ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column come from the registry. $row = $wpdb->get_row( $wpdb->prepare( "SELECT `{$col}` AS v FROM `{$table}` WHERE {$type}_id = %d", $id ), ARRAY_A ); return null === $row ? 'NO-ROW' : $row['v']; } /** * Registry field for a key, or skip the test when the site does not register it. * * A key can live in several post groups (hp_rating in listing and vendor * groups); pass the post type to pick the group scoped to it. * * @param string $type Entity type. * @param string $key Meta key. * @param string $post_type For posts: the post type whose group owns the key. */ public static function field( string $type, string $key, string $post_type = '' ): array { $field = TMDO_Entity_Registry::get_field( $type, $key ); if ( '' !== $post_type ) { foreach ( TMDO_Entity_Registry::get_groups_for_type( $type ) as $group ) { if ( in_array( $post_type, TMDO_Entity_Registry::get_group_post_types( $type, $group ), true ) && in_array( $key, TMDO_Entity_Registry::get_group_keys( $type, $group ), true ) ) { $field = array( 'group' => $group ) + (array) $field; break; } } } if ( ! $field ) { throw new TMDO_Live_Skip( "{$type} key {$key} is not registered on this site" ); } if ( ! TMDO_Schema_Manager::table_exists( TMDO_Schema_Manager::get_table_name( $type, $field['group'] ) ) ) { throw new TMDO_Live_Skip( "flat table for {$type}/{$field['group']} does not exist" ); } return $field; } /** * Delete fixtures and drop the mode override. */ public static function teardown(): void { require_once ABSPATH . 'wp-admin/includes/user.php'; foreach ( array_reverse( self::$created ) as $c ) { if ( 'user' === $c[0] ) { wp_delete_user( $c[1] ); } elseif ( 'term' === $c[0] ) { wp_delete_term( $c[1], $c[2] ); } elseif ( 'post' === $c[0] ) { wp_delete_post( $c[1], true ); } elseif ( 'comment' === $c[0] ) { wp_delete_comment( $c[1], true ); } } self::$created = array(); self::clear_mode_filter(); self::reset_caches(); } /** * Remove the pre_option override, if any. */ private static function clear_mode_filter(): void { if ( null !== self::$mode_filter ) { remove_filter( 'pre_option_wpdo_bridge_modes', self::$mode_filter ); self::$mode_filter = null; } } /** * Reset request-level caches that memoise modes or routing decisions. */ private static function reset_caches(): void { TMDO_Mode_Manager::reset_cache(); TMDO_Routing_Predicate::flush_cache(); } }