*/ private array $post_types; /** * @var array */ private array $post_statuses; /** * @since 1.18.1 * * @param TargetKeywordManager $manager Imports and stores keywords. * @param array $post_types Post types to import. * @param array $post_statuses Post statuses to import. */ public function __construct( TargetKeywordManager $manager, array $post_types, array $post_statuses ) { $this->manager = $manager; $this->post_types = array() !== $post_types ? $post_types : array( 'post' ); $this->post_statuses = array() !== $post_statuses ? $post_statuses : array( 'publish' ); } /** * @since 1.18.1 */ public function id(): string { return 'target-keywords'; } /** * Takes part only when a supported SEO plugin is active. * * Without one there is nothing to import, and walking every post to find * that out would cost a full pass for no result. A site that activates an * SEO plugin later enrols on its next run. * * @since 1.18.1 */ public function should_enrol(): bool { return $this->manager->has_importable_sources(); } /** * @since 1.18.1 */ public function total(): int { global $wpdb; if ( ! isset( $wpdb ) ) { return 0; } $types = $this->placeholders( $this->post_types ); $statuses = $this->placeholders( $this->post_statuses ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Placeholder lists are built from %s only; this counts live corpus state that changes as the job runs. $count = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->posts} WHERE post_type IN ({$types}) AND post_status IN ({$statuses})", array_merge( $this->post_types, $this->post_statuses ) ) ); // phpcs:enable return (int) $count; } /** * Imports keywords for posts after $cursor until the deadline or the last * post. * * @since 1.18.1 * * @param Deadline $deadline Time budget for this batch. * @param int $cursor Highest post id already imported. */ public function process( Deadline $deadline, int $cursor ): StageResult { global $wpdb; if ( ! isset( $wpdb ) ) { return StageResult::failed( 'No database connection available.' ); } $processed = 0; $last_id = $cursor; while ( true ) { $ids = $this->next_chunk( $last_id ); if ( array() === $ids ) { return StageResult::finished( $processed ); } foreach ( $ids as $id ) { // Checked before each post, not each chunk: importing reads // several meta fields and writes per keyword, so a chunk can // outlast what is left of the batch. if ( $deadline->expired() ) { return StageResult::progress( $last_id, $processed ); } $this->manager->sync( $id, PostModel::TYPE_POST ); if ( $this->manager->last_sync_failed() ) { // The cursor stays at the last post fully saved, so the // retry starts with this one. $reason = (string) ( $wpdb->last_error ?? '' ); return StageResult::failed( '' !== $reason ? sprintf( 'Could not save the focus keywords for post %d: %s', $id, $reason ) : sprintf( 'Could not save the focus keywords for post %d.', $id ) ); } $last_id = $id; ++$processed; } if ( count( $ids ) < self::CHUNK_SIZE ) { return StageResult::finished( $processed ); } } } /** * Reads the next chunk of post ids after $cursor. * * @since 1.18.1 * * @param int $cursor Highest post id already imported. * * @return array */ private function next_chunk( int $cursor ): array { global $wpdb; $types = $this->placeholders( $this->post_types ); $statuses = $this->placeholders( $this->post_statuses ); // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Placeholder lists are built from %s only; this reads live corpus state the job is walking. $ids = $wpdb->get_col( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE ID > %d AND post_type IN ({$types}) AND post_status IN ({$statuses}) ORDER BY ID ASC LIMIT %d", array_merge( array( $cursor ), $this->post_types, $this->post_statuses, array( self::CHUNK_SIZE ) ) ) ); // phpcs:enable return is_array( $ids ) ? array_map( 'intval', $ids ) : array(); } /** * Builds a `%s` placeholder list of the given length. * * @since 1.18.1 * * @param array $values Values the placeholders stand for. */ private function placeholders( array $values ): string { return implode( ',', array_fill( 0, max( 1, count( $values ) ), '%s' ) ); } }