Виджет «Популярное за неделю» с кешем

Настройки в админке

Класс виджета

<?php
/**
 * Виджет популярных новостей за неделю на основе оставленных за этот период комментариев.
 */

namespace RusDTP;

use DateTime;
use Exception;
use WP_Post;
use WP_Widget;

class Popular_News extends WP_Widget {

	public $widget_args, $widget_instance;
	private $default_per_posts = 5;
	private $cron_hook = 'popular-news-event';

	public function __construct() {
		parent::__construct( 'popular-news', 'Популярные новости', [
			'classname'   => 'widget_popular_news',
			'description' => 'Популярные новости портала за неделю.',
		] );

		$this->alt_option_name = 'widget_popular_news';

		add_action( $this->cron_hook, [ $this, 'update_cache' ] );
		add_action( 'wp_insert_comment', [ $this, 'cron_update_cache_before_insert_comment' ] );
		add_action( 'transition_comment_status', [ $this, 'cron_update_cache_before_transition_comment' ], 11, 2 );
	}

	/**
	 * Обновляет настройки виджета.
	 *
	 * @param array $new_instance
	 * @param array $old_instance
	 *
	 * @return array
	 */
	public function update( $new_instance, $old_instance ): array {
		$instance           = $old_instance;
		$instance['title']  = sanitize_text_field( $new_instance['title'] );
		$instance['number'] = absint( $new_instance['number'] );

		if ( abs( $new_instance['number'] ) !== abs( $old_instance['number'] ?? 0 ) ) {
			$this->update_cache();
		}

		return $instance;
	}

	/**
	 * Выводит на экран настройки виджета.
	 *
	 * @param array $instance Текущие настройки.
	 *
	 * @return void
	 */
	public function form( $instance ): void {
		$title  = $instance['title'] ?? '';
		$number = $instance['number'] ?? $this->default_per_posts;
		?>

		<p class="desc">
			<?= $this->widget_options['description'] ?>
		</p>

		<p>
			<label for="<?= $this->get_field_id( 'title' ) ?>">
				<?php _e( 'Title:' ) ?>
			</label>
			<input class="widefat"
				   id="<?= $this->get_field_id( 'title' ) ?>"
				   name="<?= $this->get_field_name( 'title' ) ?>"
				   type="text"
				   value="<?= esc_attr( $title ) ?>"
			/>
		</p>

		<p>
			<label for="<?= $this->get_field_id( 'number' ) ?>">
				Количество выводимых новостей:
			</label>
			<input class="tiny-text"
				   id="<?= $this->get_field_id( 'number' ) ?>"
				   name="<?= $this->get_field_name( 'number' ) ?>"
				   type="number"
				   step="1"
				   min="1"
				   value="<?= absint( $number ) ?>"
				   size="3"
			/>
		</p>

		<?php
	}

	/**
	 * Выводит на экран контент виджета.
	 *
	 * @param array $args
	 * @param array $instance
	 *
	 * @return void
	 */
	public function widget( $args, $instance ): void {
		$this->widget_args     = $args;
		$this->widget_instance = $instance;

		get_template_part( '/templates/widgets/widget-popular-news', null, [ 'module' => $this ] );
	}

	/**
	 * Возвращает заголовок виджета.
	 *
	 * @return string
	 */
	public function get_widget_title(): string {
		$title = empty( $this->widget_instance['title'] ) ? 'Популярные записи' : $this->widget_instance['title'];
		$title = apply_filters( 'widget_title', $title, $this->widget_instance, $this->id_base );

		return esc_html( $title );
	}

	/**
	 * Возвращает количество отображаемых в виджете новостей.
	 *
	 * @return int
	 */
	public function get_widget_per_posts(): int {
		$per_posts = absint( $this->widget_instance['number'] ?? 0 );

		if ( $per_posts ) {
			return $per_posts;
		}

		return absint( $this->get_settings()[ $this->number ]['number'] ?? $this->default_per_posts );
	}

	/**
	 * Возвращает популярные новости на основе комментариев в количестве, указанном в настройках виджета.
	 *
	 * @param $per_posts
	 *
	 * @return WP_Post[]
	 */
	private function get_posts( $per_posts ): array {
		global $wpdb;

		$posts = [];

		// Дата последнего комментария
		$date_start = $wpdb->get_var( "
			SELECT comment_date
			FROM $wpdb->comments
			WHERE comment_approved = '1' AND comment_type = 'comment'
			ORDER BY comment_date DESC
			LIMIT 0,1
		" );

		if ( $date_start ) {
			try {
				// Дата комментария на неделю ранее
				$date_end = new DateTime( $date_start );
				$date_end = $date_end->modify( '-1 weeks' )->format( 'Y-m-d H:i:s' );
			} catch ( Exception $e ) {
				return [];
			}

			// Запрос комментариев от последнего комметария до комментария неделю раннее
			$result = $wpdb->get_results( "
				SELECT  comment_ID,  comment_post_ID
				FROM $wpdb->comments
				WHERE comment_approved = '1'
				  AND comment_type = 'comment'
				  AND comment_date >= '$date_end'
				  AND comment_date <= '$date_start'
			" );

			if ( $result && is_array( $result ) ) {
				$post_ids = [];

				foreach ( $result as $item ) {
					if ( isset( $post_ids[ $item->comment_post_ID ] ) ) {
						++ $post_ids[ $item->comment_post_ID ];
					} else {
						$post_ids[ $item->comment_post_ID ] = 1;
					}
				}

				arsort( $post_ids, SORT_NUMERIC );

				$posts = get_posts( [
					'post_type'      => 'post',
					'posts_per_page' => $per_posts,
					'post__in'       => array_keys( $post_ids ),
					'orderby'        => 'post__in',
				] );
			}
		}

		return $posts;
	}

	/**
	 * Возвращает новости из кеша.
	 *
	 * @param bool $flush Пересоздать кеш
	 *
	 * @return WP_Post[]
	 */
	public function get_cached_posts( bool $flush = false ): array {
		$posts = get_transient( __CLASS__ );

		if ( false === $posts || $flush ) {
			$posts = $this->get_posts( $this->get_widget_per_posts() );

			if ( $posts ) {
				update_post_caches( $posts, 'post', false, false );
			}

			set_transient( __CLASS__, $posts );
		}

		return array_map( 'get_post', $posts );
	}

	/**
	 * Обновляет кеш.
	 *
	 * @return void
	 */
	public function update_cache(): void {
		$this->get_cached_posts( true );
	}

	public function add_cron_update_cache(): void {
		if ( ! wp_next_scheduled( $this->cron_hook ) ) {
			wp_schedule_single_event( time() + 1, $this->cron_hook );
		}
	}

	/**
	 * Добавляет крон задачу на обновлении кеша при добавлении нового комментария.
	 *
	 * @return void
	 */
	public function cron_update_cache_before_insert_comment(): void {
		$this->add_cron_update_cache();
	}

	/**
	 * Добавляет крон задачу на обновлении кеша при смене статуса любого комментария.
	 *
	 * @param string $new_status
	 * @param string $old_status
	 *
	 * @return void
	 */
	public function cron_update_cache_before_transition_comment( string $new_status, string $old_status ): void {
		in_array( 'approved', [ $new_status, $old_status ], true ) && $this->add_cron_update_cache();
	}

}

Файл шаблона

Пример оформление во фронте
<?php
/**
 * Шаблон виджета "Свежие комментарии".
 *
 * @var $module RusDTP\Popular_News
 */
$module = $args['module'] ?? null;

if ( ! $module ) {
	return;
}

$posts = $module->get_cached_posts();
?>

<?= $module->widget_args['before_widget'] ?>

<?= $module->widget_args['before_title'] . $module->get_widget_title() . $module->widget_args['after_title'] ?>

<?php if ( $posts ): ?>

	<div class="widget_popular_news__items">
		<?php
		global $post;

		foreach ( $posts as $post ) {
			setup_postdata( $post );

			// Ссылка на миниатюру
			preg_match( '/(<img[^>]+>)/i', get_the_excerpt(), $matches );
			$img_url = $matches[0] ?? null;
			?>
			<div class="widget_popular_news__item">
				<a class="widget_popular_news__item-thumb">
					<?= $img_url ?>
				</a>
				<div>
					<a class="widget_popular_news__item-title" href="<?php the_permalink( $post ) ?>">
						<?= get_the_title( $post ) ?>
					</a>

					<a class="widget_popular_news__item-comments-count icon__comment-count"
					   href="<?php the_permalink( $post ) ?>#comments"
					>
						<?= get_comments_number( $post ); ?>
					</a>
				</div>
			</div>
			<?php
		}
		wp_reset_postdata();
		?>
	</div>

<?php else: ?>
	<p class="widget_popular_news__empty">
		К сожалению, популярных постов за указанный период не нашлось.
	</p>
<?php endif; ?>

<?= $module->widget_args['after_widget'] ?>