WordPress – pre_get_posts вместо query_posts на страницах

Моя ситуация несколько сложная, я попытаюсь объяснить ее как можно более лаконично.

В настоящее время я использую query_posts для изменения основного запроса на пользовательских страницах на моем сайте, который, насколько я могу сказать, работает достаточно хорошо, хотя я читал, что использование query_posts – это плохая практика по ряду причин.

Итак, почему я использую query_posts и не WP_Query объект WP_Query вы можете задать?

Это потому, что я использую плагин с бесконечным прокруткой, бесконечный прокрутка не очень хорошо работает с WP_query, но он отлично работает, когда вы просто изменяете основной запрос с помощью query_posts. Например, разбиение на страницы не работает, используя бесконечную прокрутку + WP_query (главное беспокойство).

На одной странице я изменяю запрос, чтобы получать наиболее просматриваемые сообщения.

 <?php $paged = get_query_var( 'paged' ) ? get_query_var( 'paged' ) : 1; ?> <?php query_posts( array( 'meta_key' => 'wpb_post_views_count', 'orderby' => 'meta_value_num', 'order' => 'DESC' , 'paged' => $paged, ) ); ?> <?php if (have_posts()) : ?> <?php while ( have_posts() ) : the_post() ?> <?php if ( has_post_format( 'video' )) { get_template_part( 'video-post' ); }elseif ( has_post_format( 'image' )) { get_template_part( 'image-post' ); } else { get_template_part( 'standard-post' ); } ?> <?php endwhile;?> <?php endif; ?> 

Поэтому после большого чтения я понимаю, что мой другой вариант изменения основного запроса – использование pre_get_posts , хотя я несколько не уверен, как это сделать.

Возьмите это, например:

 function textdomain_exclude_category( $query ) { if ( $query->is_home() && $query->is_main_query() ) { $query->set( 'cat', '-1,-2' ); } } add_action( 'pre_get_posts', 'textdomain_exclude_category' ); 

Хорошо, достаточно просто – если это домашняя страница, измените основной запрос и исключите две категории.

Я смущен и не могу понять:

  1. сценарий использования для пользовательских шаблонов страниц. С моей модификацией query_posts я могу просто отбросить массив до if (have_posts()) , выберите мой шаблон страницы, опубликуйте его и отпустите. С pre_get_posts я не могу понять, как сказать, например, $query->most-viewed т. Д.

  2. array( 'meta_key' => 'wpb_post_views_count', 'orderby' => 'meta_value_num', 'order' => 'DESC' , 'paged' => $paged, ) );

Как, черт возьми, я делаю это с помощью pre_get_posts и удостоверяюсь, что он разбит на страницы, т.е. работает с бесконечным свитком? Во всех примерах, которые я видел с помощью pre_get_posts , нет массивов.

Как использовать крюк pre_get_posts для отображения списка сообщений на странице с помощью настраиваемого шаблона страницы?

Я играл с крюком pre_get_posts и вот одна идея

Шаг 1:

Призвать страницу, вызванную, например, Show with the slug:

 example.com/show 

Шаг 2:

Создайте собственный шаблон страницы:

 tpl_show.php 

расположенный в текущем каталоге тем.

Шаг 3:

Мы pre_get_posts следующий pre_get_posts вызов действия pre_get_posts :

 function b2e_pre_get_posts( $query ) { $target_page = 'show'; // EDIT to your needs if ( ! is_admin() // front-end only && $query->is_main_query() // main query only && $target_page === $query->get( 'pagename' ) // matching pagename only ) { // modify query_vars: $query->set( 'post_type', 'post' ); // override 'post_type' $query->set( 'pagename', null ); // override 'pagename' $query->set( 'posts_per_page', 10 ); $query->set( 'meta_key', 'wpb_post_views_count' ); $query->set( 'orderby', 'meta_value_num' ); $query->set( 'order', 'DESC' ); // Support for paging $query->is_singular = 0; // custom page template add_filter( 'template_include', 'b2e_template_include', 99 ); } } add_action( 'pre_get_posts', 'b2e_pre_get_posts' ); 

где

 function b2e_template_include( $template ) { $target_tpl = 'tpl_show.php'; // EDIT to your needs remove_filter( 'template_include', 'b2e_template_include', 99 ); $new_template = locate_template( array( $target_tpl ) ); if ( ! empty( $new_template ) ) $template = $new_template; ; return $template; } 

Это также должно указывать на разбиение на страницы:

 example.com/show/page/2 example.com/show/page/3 

и т.п.

Заметки

Я обновил ответ и удалил модификацию части объекта запроса, основываясь на предположении от @PieterGoosen, так как он мог бы, например, сломать панировочные сухари на его настройке.

Также удалена is_page() внутри крюка pre_get_posts , поскольку в некоторых случаях она может по-прежнему давать некоторые нарушения. Причина в том, что объект запроса не всегда доступен. Это работает, см., Например, # 27015 . Возможны обходные пути, если мы хотим использовать is_page() или is_front_page() .

Я построил следующую таблицу, чтобы получить лучший обзор некоторых свойств и запросов varaables основного объекта WP_Query для данного пула:

Таблица

Интересно отметить, что разбиение на страницы в WP_Query зависит от того, что nopaging не задано, а текущая страница не является единственной (из источника 4.4):

 // Paging if ( empty($q['nopaging']) && !$this->is_singular ) { $page = absint($q['paged']); if ( !$page ) $page = 1; // If 'offset' is provided, it takes precedence over 'paged'. if ( isset( $q['offset'] ) && is_numeric( $q['offset'] ) ) { $q['offset'] = absint( $q['offset'] ); $pgstrt = $q['offset'] . ', '; } else { $pgstrt = absint( ( $page - 1 ) * $q['posts_per_page'] ) . ', '; } $limits = 'LIMIT ' . $pgstrt . $q['posts_per_page']; } 

где мы видим, что часть LIMIT сгенерированного SQL-запроса находится в пределах условной проверки. Это объясняет, почему мы модифицируем свойство is_singular выше.

Мы могли бы использовать другие фильтры / крючки, но здесь мы использовали pre_get_posts как упоминалось в OP.

Надеюсь, эта помощь.

С вдохновением от ответа @birgire, я придумал следующую идею. ( ПРИМЕЧАНИЕ. Это копия моего ответа из этого ответа на WPSE )

То, что я пытался сделать здесь, это скорее использовать пост-инъекцию, чем полностью изменить основной запрос и застрять со всеми вышеперечисленными проблемами, например, напрямую изменить глобальные значения, проблему глобального значения и переназначить шаблоны страниц.

Используя post-инъекцию, я могу сохранить целостность почты, поэтому $wp_the_query->post , $wp_query->post , $posts и $post остаются постоянными по всему шаблону, все они содержат только текущий объект страницы, с истинными страницами. Таким образом, такие функции, как панировочные сундуки, по-прежнему считают, что текущая страница – это настоящая страница, а не какой-то архив

Мне пришлось немного изменить основной запрос (с помощью фильтров и действий ), чтобы приспособиться к разбивке на страницы, но мы придем к этому.

ВОПРОС ПОСЛЕ ИНЪЕКЦИИ

Чтобы выполнить пост-инъекцию, я использовал специальный запрос для возврата сообщений, необходимых для инъекций. Я также использовал свойство $found_pages пользовательского запроса, чтобы настроить основной запрос, чтобы получить разбиение на страницы из основного запроса. Сообщения вводятся в основной запрос посредством действия loop_end .

Чтобы сделать пользовательский запрос доступным и полезным вне класса, я ввел несколько действий.

  • Клочки для разбивки на страницы, чтобы перехватывать функции разбивки на страницы:

    • pregetgostsforgages_before_loop_pagination

    • pregetgostsforgages_after_loop_pagination

  • Пользовательский счетчик, который подсчитывает сообщения в цикле. Эти действия могут использоваться для изменения того, как сообщения отображаются внутри цикла в соответствии с номером сообщения

    • pregetgostsforgages_counter_before_template_part

    • pregetgostsforgages_counter_after_template_part

  • Общий доступ для доступа к объекту запроса и текущему объекту post

    • pregetgostsforgages_current_post_and_object

Эти крючки дают вам полный опыт работы с руками, так как вам не нужно ничего менять в шаблоне страницы, что было моим первоначальным намерением с самого начала. Страница полностью может быть изменена из плагина или файла функции, что делает эту очень динамичную

Я также использовал get_template_part() , чтобы загрузить часть шаблона, которая будет использоваться для отображения сообщений. Большинство тем сегодня используют шаблонные части, что делает это очень полезным в классе. Если ваша тема использует content.php , вы можете просто передать content в $templatePart для загрузки content.php .

Если вам нужна поддержка формата post для шаблонных частей, это легко, вы все равно можете просто передать content в $templatePart и просто установить $postFormatSupport в true а content-video.php части шаблона content-video.php будет загружено для сообщения с почтовым форматом video

ГЛАВНЫЙ ЗАПРОС

Следующие изменения были внесены в основной запрос через соответствующие фильтры и действия

  • Для разбивки основного запроса:

    • $found_posts свойства $found_posts запроса инжектора переходит к значению основного объекта запроса через фильтр found_posts

    • Задайте значение переданного пользователем параметра posts_per_page для основного запроса через pre_get_posts

    • $max_num_pages рассчитывается с использованием количества сообщений в $found_posts и posts_per_page . Поскольку is_singular верно на страницах, он запрещает установку LIMIT . Просто установка is_singular в false вызвало несколько проблем, поэтому я решил установить предложение LIMIT через фильтр post_limits . Я сохранил offset предложения LIMIT равным 0 чтобы избежать 404 на страничных страницах

Это касается разбиения на страницы и любой проблемы, которая может возникнуть в результате пост-инъекции

ОБЪЕКТ СТРАНИЦЫ

Текущий объект страницы доступен для отображения в виде сообщения, используя цикл по умолчанию на странице, отдельно и поверх введенных сообщений. Если вам это не нужно, вы можете просто установить для параметра $removePageFromLoop значение true, это скроет содержимое страницы.

На этом этапе я использую CSS, чтобы скрыть объект страницы через действия loop_start и loop_end поскольку я не могу найти другой способ сделать это. Недостатком этого метода является то, что все, что the_post крюком действия the_post внутри основного запроса, также будет скрыто по умолчанию, если вы the_post объект страницы.

КЛАСС

Класс PreGetPostsForPages может быть улучшен и должен быть должным образом PreGetPostsForPages Хотя вы можете просто отказаться от этого в файле функций вашей темы, было бы лучше отбросить его в пользовательский плагин.

Используйте, изменяйте и злоупотребляйте по своему усмотрению. Код хорошо прокомментирован, поэтому его следует легко отслеживать и настраивать

 class PreGetPostsForPages { /** * @var string|int $pageID * @access protected * @since 1.0.0 */ protected $pageID; /** * @var string $templatePart * @access protected * @since 1.0.0 */ protected $templatePart; /** * @var bool $postFormatSupport * @access protected * @since 1.0.0 */ protected $postFormatSupport; /** * @var bool $removePageFromLoop * @access protected * @since 1.0.0 */ protected $removePageFromLoop; /** * @var array $args * @access protected * @since 1.0.0 */ protected $args; /** * @var array $mergedArgs * @access protected * @since 1.0.0 */ protected $mergedArgs = []; /** * @var NULL|\stdClass $injectorQuery * @access protected * @since 1.0.0 */ protected $injectorQuery = NULL; /** * @var int $validatedPageID * @access protected * @since 1.0.0 */ protected $validatedPageID = 0; /** * Constructor method * * @param string|int $pageID The ID of the page we would like to target * @param string $templatePart The template part which should be used to display posts * @param string $postFormatSupport Should get_template_part support post format specific template parts * @param bool $removePageFromLoop Should the page content be displayed or not * @param array $args An array of valid arguments compatible with WP_Query * * @since 1.0.0 */ public function __construct( $pageID = NULL, $templatePart = NULL, $postFormatSupport = false, $removePageFromLoop = false, $args = [] ) { $this->pageID = $pageID; $this->templatePart = $templatePart; $this->postFormatSupport = $postFormatSupport; $this->removePageFromLoop = $removePageFromLoop; $this->args = $args; } /** * Public method init() * * The init method will be use to initialize our pre_get_posts action * * @since 1.0.0 */ public function init() { // Initialise our pre_get_posts action add_action( 'pre_get_posts', [$this, 'preGetPosts'] ); } /** * Private method validatePageID() * * Validates the page ID passed * * @since 1.0.0 */ private function validatePageID() { $validatedPageID = filter_var( $this->pageID, FILTER_VALIDATE_INT ); $this->validatedPageID = $validatedPageID; } /** * Private method mergedArgs() * * Merge the default args with the user passed args * * @since 1.0.0 */ private function mergedArgs() { // Set default arguments if ( get_query_var( 'paged' ) ) { $currentPage = get_query_var( 'paged' ); } elseif ( get_query_var( 'page' ) ) { $currentPage = get_query_var( 'page' ); } else { $currentPage = 1; } $default = [ 'suppress_filters' => true, 'ignore_sticky_posts' => 1, 'paged' => $currentPage, 'posts_per_page' => get_option( 'posts_per_page' ), // Set posts per page here to set the LIMIT clause etc 'nopaging' => false ]; $mergedArgs = wp_parse_args( (array) $this->args, $default ); $this->mergedArgs = $mergedArgs; } /** * Public method preGetPosts() * * This is the callback method which will be hooked to the * pre_get_posts action hook. This method will be used to alter * the main query on the page specified by ID. * * @param \stdClass WP_Query The query object passed by reference * @since 1.0.0 */ public function preGetPosts( \WP_Query $q ) { if ( !is_admin() // Only target the front end && $q->is_main_query() // Only target the main query && $q->is_page( filter_var( $this->validatedPageID, FILTER_VALIDATE_INT ) ) // Only target our specified page ) { // Remove the pre_get_posts action to avoid unexpected issues remove_action( current_action(), [$this, __METHOD__] ); // METHODS: // Initialize our method which will return the validated page ID $this->validatePageID(); // Initiale our mergedArgs() method $this->mergedArgs(); // Initiale our custom query method $this->injectorQuery(); /** * We need to alter a couple of things here in order for this to work * - Set posts_per_page to the user set value in order for the query to * to properly calculate the $max_num_pages property for pagination * - Set the $found_posts property of the main query to the $found_posts * property of our custom query we will be using to inject posts * - Set the LIMIT clause to the SQL query. By default, on pages, `is_singular` * returns true on pages which removes the LIMIT clause from the SQL query. * We need the LIMIT clause because an empty limit clause inhibits the calculation * of the $max_num_pages property which we need for pagination */ if ( $this->mergedArgs['posts_per_page'] && true !== $this->mergedArgs['nopaging'] ) { $q->set( 'posts_per_page', $this->mergedArgs['posts_per_page'] ); } elseif ( true === $this->mergedArgs['nopaging'] ) { $q->set( 'posts_per_page', -1 ); } // FILTERS: add_filter( 'found_posts', [$this, 'foundPosts'], PHP_INT_MAX, 2 ); add_filter( 'post_limits', [$this, 'postLimits']); // ACTIONS: /** * We can now add all our actions that we will be using to inject our custom * posts into the main query. We will not be altering the main query or the * main query's $posts property as we would like to keep full integrity of the * $post, $posts globals as well as $wp_query->post. For this reason we will use * post injection */ add_action( 'loop_start', [$this, 'loopStart'], 1 ); add_action( 'loop_end', [$this, 'loopEnd'], 1 ); } } /** * Public method injectorQuery * * This will be the method which will handle our custom * query which will be used to * - return the posts that should be injected into the main * query according to the arguments passed * - alter the $found_posts property of the main query to make * pagination work * * @link https://codex.wordpress.org/Class_Reference/WP_Query * @since 1.0.0 * @return \stdClass $this->injectorQuery */ public function injectorQuery() { //Define our custom query $injectorQuery = new \WP_Query( $this->mergedArgs ); $this->injectorQuery = $injectorQuery; return $this->injectorQuery; } /** * Public callback method foundPosts() * * We need to set found_posts in the main query to the $found_posts * property of the custom query in order for the main query to correctly * calculate $max_num_pages for pagination * * @param string $found_posts Passed by reference by the filter * @param stdClass \WP_Query Sq The current query object passed by refence * @since 1.0.0 * @return $found_posts */ public function foundPosts( $found_posts, \WP_Query $q ) { if ( !$q->is_main_query() ) return $found_posts; remove_filter( current_filter(), [$this, __METHOD__] ); // Make sure that $this->injectorQuery actually have a value and is not NULL if ( $this->injectorQuery instanceof \WP_Query && 0 != $this->injectorQuery->found_posts ) return $found_posts = $this->injectorQuery->found_posts; return $found_posts; } /** * Public callback method postLimits() * * We need to set the LIMIT clause as it it is removed on pages due to * is_singular returning true. Witout the limit clause, $max_num_pages stays * set 0 which avoids pagination. * * We will also leave the offset part of the LIMIT cluase to 0 to avoid paged * pages returning 404's * * @param string $limits Passed by reference in the filter * @since 1.0.0 * @return $limits */ public function postLimits( $limits ) { $posts_per_page = (int) $this->mergedArgs['posts_per_page']; if ( $posts_per_page && -1 != $posts_per_page // Make sure that posts_per_page is not set to return all posts && true !== $this->mergedArgs['nopaging'] // Make sure that nopaging is not set to true ) { $limits = "LIMIT 0, $posts_per_page"; // Leave offset at 0 to avoid 404 on paged pages } return $limits; } /** * Public callback method loopStart() * * Callback function which will be hooked to the loop_start action hook * * @param \stdClass \WP_Query $q Query object passed by reference * @since 1.0.0 */ public function loopStart( \WP_Query $q ) { /** * Although we run this action inside our preGetPosts methods and * and inside a main query check, we need to redo the check here aswell * because failing to do so sets our div in the custom query output as well */ if ( !$q->is_main_query() ) return; /** * Add inline style to hide the page content from the loop * whenever $removePageFromLoop is set to true. You can * alternatively alter the page template in a child theme by removing * everything inside the loop, but keeping the loop * Example of how your loop should look like: * while ( have_posts() ) { * the_post(); * // Add nothing here * } */ if ( true === $this->removePageFromLoop ) echo '<div style="display:none">'; } /** * Public callback method loopEnd() * * Callback function which will be hooked to the loop_end action hook * * @param \stdClass \WP_Query $q Query object passed by reference * @since 1.0.0 */ public function loopEnd( \WP_Query $q ) { /** * Although we run this action inside our preGetPosts methods and * and inside a main query check, we need to redo the check here as well * because failing to do so sets our custom query into an infinite loop */ if ( !$q->is_main_query() ) return; // See the note in the loopStart method if ( true === $this->removePageFromLoop ) echo '</div>'; //Make sure that $this->injectorQuery actually have a value and is not NULL if ( !$this->injectorQuery instanceof \WP_Query ) return; // Setup a counter as wee need to run the custom query only once static $count = 0; /** * Only run the custom query on the first run of the loop. Any consecutive * runs (like if the user runs the loop again), the custom posts won't show. */ if ( 0 === (int) $count ) { // We will now add our custom posts on loop_end $this->injectorQuery->rewind_posts(); // Create our loop if ( $this->injectorQuery->have_posts() ) { /** * Fires before the loop to add pagination. * * @since 1.0.0 * * @param \stdClass $this->injectorQuery Current object (passed by reference). */ do_action( 'pregetgostsforgages_before_loop_pagination', $this->injectorQuery ); // Add a static counter for those who need it static $counter = 0; while ( $this->injectorQuery->have_posts() ) { $this->injectorQuery->the_post(); /** * Fires before get_template_part. * * @since 1.0.0 * * @param int $counter (passed by reference). */ do_action( 'pregetgostsforgages_counter_before_template_part', $counter ); /** * Fires before get_template_part. * * @since 1.0.0 * * @param \stdClass $this->injectorQuery-post Current post object (passed by reference). * @param \stdClass $this->injectorQuery Current object (passed by reference). */ do_action( 'pregetgostsforgages_current_post_and_object', $this->injectorQuery->post, $this->injectorQuery ); /** * Load our custom template part as set by the user * * We will also add template support for post formats. If $this->postFormatSupport * is set to true, get_post_format() will be automatically added in get_template part * * If you have a template called content-video.php, you only need to pass 'content' * to $template part and then set $this->postFormatSupport to true in order to load * content-video.php for video post format posts */ $part = ''; if ( true === $this->postFormatSupport ) $part = get_post_format( $this->injectorQuery->post->ID ); get_template_part( filter_var( $this->templatePart, FILTER_SANITIZE_STRING ), $part ); /** * Fires after get_template_part. * * @since 1.0.0 * * @param int $counter (passed by reference). */ do_action( 'pregetgostsforgages_counter_after_template_part', $counter ); $counter++; //Update the counter } wp_reset_postdata(); /** * Fires after the loop to add pagination. * * @since 1.0.0 * * @param \stdClass $this->injectorQuery Current object (passed by reference). */ do_action( 'pregetgostsforgages_after_loop_pagination', $this->injectorQuery ); } } // Update our static counter $count++; } } 

ПРИМЕНЕНИЕ

Теперь вы можете инициировать класс ( также в своем плагине или файле функций ), как показано ниже, для целевой страницы с идентификатором 251, на котором мы покажем 2 сообщения на страницу из типа post post

 $query = new PreGetPostsForPages( 251, // Page ID we will target 'content', //Template part which will be used to display posts, name should be without .php extension true, // Should get_template_part support post formats false, // Should the page object be excluded from the loop [ // Array of valid arguments that will be passed to WP_Query/pre_get_posts 'post_type' => 'post', 'posts_per_page' => 2 ] ); $query->init(); 

ДОБАВЛЕНИЕ ПЕЧАТИ И ТАМОЖЕННЫЙ СТИЛЬ

Как я уже сказал, в запросе инжектора есть несколько действий, чтобы добавить разбивку на страницы или пользовательский стиль. Здесь я добавил разбивку на страницы после цикла, используя мою собственную функцию разбиения на страницы из связанного ответа . Кроме того, используя встроенный счетчик, я добавил div для отображения моих сообщений в двух столбцах.

Вот действия, которые я использовал

 add_action( 'pregetgostsforgages_counter_before_template_part', function ( $counter ) { $class = $counter%2 ? ' right' : ' left'; echo '<div class="entry-column' . $class . '">'; }); add_action( 'pregetgostsforgages_counter_after_template_part', function ( $counter ) { echo '</div>'; }); add_action( 'pregetgostsforgages_after_loop_pagination', function ( \WP_Query $q ) { paginated_numbers(); }); 

Обратите внимание, что разбиение на страницы задается основным запросом, а не запросом инжектора, поэтому также должны работать встроенные функции, такие как the_posts_pagination() .

Это конечный результат

введите описание изображения здесь

СТАТИЧЕСКИЕ ПЕРЕДНИЕ СТРАНИЦЫ

Все работает так, как ожидалось, на статических передних страницах вместе с моей функцией разбиения на страницы без необходимости делать никаких изменений

ВЫВОД

Это может показаться действительно большим количеством накладных расходов, и это может быть, но профессионалы перевешивают большое время конфликта

BIG PRO'S

  • Вам не нужно каким-либо образом изменять шаблон страницы для конкретной страницы. Это делает все динамичным и легко переносится между темами без внесения изменений в код вообще, если все сделано в плагине.

  • В лучшем случае вам нужно только создать часть шаблона content.php в своей теме, если в вашей теме еще нет

  • Любая разбивка на страницы, которая работает в основном запросе, будет работать на странице без каких-либо изменений или чего-либо еще от запроса, передаваемого функции.

Есть больше pro, о которых я не могу сейчас думать, но это важные

Я надеюсь, что это поможет кому-то в будущем