'AND', 'status_clause' => array( 'key' => SWCC_Topic::META_STATUS, 'value' => 'planned', 'compare' => '=', ), 'due_clause' => array( 'key' => SWCC_Topic::META_SCHEDULED, 'value' => $as_of, 'compare' => '<=', 'type' => 'DATE', ), 'unset_clause' => array( 'key' => SWCC_Topic::META_SCHEDULED, 'value' => '', 'compare' => '!=', ), array( 'relation' => 'OR', array( 'key' => SWCC_Topic::META_GENERATED, 'compare' => 'NOT EXISTS', ), array( 'key' => SWCC_Topic::META_GENERATED, 'value' => 0, 'compare' => '=', ), ), ); } /** * Retrieve the next topic due for generation. * * Ordered by scheduled date, then priority order, then ID for a stable tiebreak. * * @param string $as_of Optional Y-m-d cutoff; topics scheduled later are skipped. * Defaults to today in site time. * @return WP_Post|null The next topic, or null if none are due. */ public static function get_next( $as_of = '' ) { $as_of = self::normalize_as_of( $as_of ); $query = new WP_Query( array( 'post_type' => SWCC_Topic::POST_TYPE, 'post_status' => self::eligible_post_statuses(), 'posts_per_page' => 1, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_term_cache' => false, // A meta_query is unavoidable for this selection. It is bounded to a single // row, hits indexed meta keys, and runs once per generation cycle. // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => self::due_meta_query( $as_of ), 'orderby' => array( 'due_clause' => 'ASC', 'menu_order' => 'ASC', 'ID' => 'ASC', ), ) ); if ( empty( $query->posts ) ) { return null; } return $query->posts[0]; } /** * Retrieve every topic due for generation, in dispatch order. * * The plural counterpart to get_next(), for the due-entries endpoint (MMBC-55): dispatch * enqueues one Cloud Task per due entry rather than walking the queue one at a time. It * shares due_meta_query() and the ordering with get_next() deliberately, so "due" cannot come * to mean two different things depending on which caller asked. * * @param string $as_of Optional Y-m-d cutoff; topics scheduled later are skipped. The caller * supplies this so the lead-time window is the dispatcher's decision, * not a constant baked into the plugin. * @param int $limit Maximum entries to return. * @return WP_Post[] */ public static function get_due( $as_of = '', $limit = 50 ) { $as_of = self::normalize_as_of( $as_of ); $limit = max( 1, (int) $limit ); $query = new WP_Query( array( 'post_type' => SWCC_Topic::POST_TYPE, 'post_status' => self::eligible_post_statuses(), 'posts_per_page' => $limit, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'update_post_term_cache' => false, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- same bounded, indexed selection as get_next(), run once per dispatch cycle. 'meta_query' => self::due_meta_query( $as_of ), 'orderby' => array( 'due_clause' => 'ASC', 'menu_order' => 'ASC', 'ID' => 'ASC', ), ) ); return empty( $query->posts ) ? array() : $query->posts; } /** * Whether a specific topic currently meets generation eligibility: status * `planned`, scheduled on or before `$as_of`, and not already generated. * * Answers "is this topic eligible", not "which topic wins overall" — useful * anywhere the caller already has a topic in hand (an admin screen, a test) * rather than needing the queue's top pick. * * @param int $post_id Topic post ID. * @param string $as_of Optional Y-m-d cutoff. Defaults to today in site time. * @return bool */ public static function is_eligible( $post_id, $as_of = '' ) { $as_of = self::normalize_as_of( $as_of ); $query = new WP_Query( array( 'post_type' => SWCC_Topic::POST_TYPE, 'post_status' => self::eligible_post_statuses(), 'p' => (int) $post_id, 'posts_per_page' => 1, 'no_found_rows' => true, 'ignore_sticky_posts' => true, 'fields' => 'ids', 'update_post_term_cache' => false, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => self::due_meta_query( $as_of ), ) ); return ! empty( $query->posts ); } /** * Count topics still waiting to be generated. * * Unlike get_next(), this has no due-date cutoff: a low-queue alert needs the size of * the whole backlog, not just what happens to be due today. * * @return int */ public static function count_remaining() { $query = new WP_Query( array( 'post_type' => SWCC_Topic::POST_TYPE, 'post_status' => self::eligible_post_statuses(), 'posts_per_page' => 1, 'no_found_rows' => false, 'ignore_sticky_posts' => true, 'fields' => 'ids', 'update_post_term_cache' => false, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( 'relation' => 'AND', 'status_clause' => array( 'key' => SWCC_Topic::META_STATUS, 'value' => 'planned', 'compare' => '=', ), array( 'relation' => 'OR', array( 'key' => SWCC_Topic::META_GENERATED, 'compare' => 'NOT EXISTS', ), array( 'key' => SWCC_Topic::META_GENERATED, 'value' => 0, 'compare' => '=', ), ), ), ) ); return (int) $query->found_posts; } }