首页 文章

WordPress - pre_get_posts代替页面上的query_posts

提问于
浏览
12

我的情况有点复杂,我会尽量简洁地解释一下 .

我目前正在使用 query_posts 来修改我网站上自定义页面上的主要查询,据我所知,虽然我已经读到使用query_posts这是一种不好的做法,原因有很多 .

那么,为什么我使用 query_posts 而不是创建一个你可能会问的 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' );

好吧,这么简单 - 如果它是主页,修改主查询并排除两个类别 .

我感到困惑,无法弄清楚的是: -

  • 自定义页面模板的用例场景 . 通过我的 query_posts 修改,我可以在 if (have_posts()) 之前插入数组,选择我的页面模板,发布它然后我就去了 . 使用 pre_get_posts 我无法弄清楚如何说例如 $query->most-viewed

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

我怎么做 pre_get_posts 并确保's paginated, ie. works with infinite scroll? In all the examples I'看到 pre_get_posts 没有数组 .

2 回答

  • 13

    如何使用pre_get_posts挂钩通过自定义页面模板显示页面上的帖子列表?

    我一直在玩 pre_get_posts 钩子,这是一个想法

    Step #1:

    Ceate一个名为Show with the slug的页面:

    example.com/show
    

    Step #2:

    创建自定义页面模板:

    tpl_show.php
    

    位于当前主题目录中 .

    Step #3:

    我们构造以下 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的建议更新了答案并删除了查询对象部分修改,因为它可以例如打破他的设置上的面包屑 .

    还删除了 pre_get_posts 钩子内的 is_page() 检查,因为在某些情况下它可能仍会出现一些不规则现象 . 原因是查询对象并不总是可用 . 这正在研究中,参见例如#27015 . 如果我们想使用 is_page()is_front_page() ,则有workarounds possible .

    我构建了下表,只是为了更好地概述主 WP_Query 对象的一些属性和查询变量,对于给定的slug:

    table

    interesting to noteWP_Query 中的分页取决于 nopaging 未设置且当前页面不是单数(来自4.4 source):

    // 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'];
    }
    

    我们可以看到生成的SQL查询的 LIMIT 部分在条件检查范围内 . 这解释了为什么我们修改上面的 is_singular 属性 .

    我们可以使用其他过滤器/钩子,但是在这里我们使用OP提到的 pre_get_posts .

    希望这有帮助 .

  • 2

    从@birgire回答的灵感,我提出了以下想法 . (注:This is a copy of my answer from this answer over at WPSE

    我在这里尝试做的是宁愿使用后注入而不是完全改变主要查询并且遇到所有上述问题,比如直接改变全局,全局值问题和重新分配页面模板 .

    通过使用后期注入,我能够保持完整的帖子完整性,因此 $wp_the_query->post$wp_query->post$posts$post 在整个模板中保持不变,它们都只保存当前页面对象,就像真实页面的情况一样 . 这样,像breadcrumbs这样的函数仍然认为当前页面是真正的页面而不是某种存档

    我不得不稍微改变主要查询(通过过滤器和动作)以调整分页,但我们将会这样做 .

    POST INJECTION QUERY

    为了完成后期注入,我使用自定义查询来返回注入所需的帖子 . 我还使用自定义查询的 $found_pages 属性来调整主查询的属性,以便从主查询中获取分页 . 帖子通过 loop_end 动作注入主查询 .

    为了使自定义查询可以在课外访问和使用,我介绍了几个操作 .

    • 分页挂钩以挂钩分页功能:甲

    • pregetgostsforgages_before_loop_pagination

    • pregetgostsforgages_after_loop_pagination

    • 自定义计数器,用于计算循环中的帖子 . 这些操作可用于根据帖子编号更改帖子在循环内的显示方式

    • pregetgostsforgages_counter_before_template_part

    • pregetgostsforgages_counter_after_template_part

    • 常规钩子,用于访问查询对象和当前帖子对象

    • pregetgostsforgages_current_post_and_object

    这些钩子为您提供完全不干涉的体验,因为您不需要在页面模板本身中更改任何内容,这从一开始就是我的初衷 . 可以从插件或函数文件中完全更改页面,这使得它非常动态

    我还使用 get_template_part() 来加载模板部分,用于显示帖子 . 今天的大多数主题都使用模板部分,这使得它在课堂上非常有用 . 如果您的主题使用 content.php ,则只需将 content 传递给 $templatePart 即可加载 content.php .

    如果你需要模板部分的后期格式支持,这很简单,你仍然只需将 content 传递给 $templatePart 并简单地将 $postFormatSupport 设置为 true ,并且将为帖子格式为 video 的帖子加载模板部分 content-video.php

    主要问题

    通过相应的过滤器和操作对主查询进行了以下更改

    • 为了对主查询进行分页:

    • 注入器查询的 $found_posts 属性值通过 found_posts 过滤器传递给主查询对象的值

    • 通过 pre_get_posts 将用户传递参数 posts_per_page 的值设置为主查询

    • $max_num_pages 是使用 $found_postsposts_per_page 中的帖子数量计算的 . 因为 is_singular 在页面上为true,所以它禁止设置 LIMIT 子句 . 简单地将 is_singular 设置为false会导致一些问题,因此我决定通过 post_limits 过滤器设置 LIMIT 子句 . 我将 LIMIT 子句的 offset 设置为 0 以避免在分页页面上显示404

    这样可以处理分页以及后注射可能引起的任何问题

    页面对象

    当前页面对象可以通过使用页面上的默认循环显示为帖子,单独并在注入的帖子之上 . 如果您不需要,可以将 $removePageFromLoop 设置为true,这将隐藏页面内容的显示 .

    在这个阶段,我使用CSS通过 loop_startloop_end 动作隐藏页面对象,因为我找不到另一种方法 . 这种方法的缺点是,如果隐藏页面对象,默认情况下,隐藏在主查询中的 the_post 动作钩子的任何内容都将被隐藏 .

    THE CLASS

    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++;       
        }
    }
    

    用法

    您现在可以启动课程(也在您的插件或函数文件中),如下所示,以ID为251定位页面,我们将在 post 帖子类型上显示每页2个帖子

    $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();
    

    添加分页和定制风格

    正如我所说,在注入器查询中有一些操作,以添加分页或自定义样式 . 在这里,我使用my own pagination function from the linked answer在循环后添加了分页 . 此外,使用内置计数器,我添加了一个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() 这样的内置函数也应该有效 .

    这是最终结果

    enter image description here

    静态前页

    一切都按照预期在静态首页上与我的分页功能一起工作,而不必进行任何修改

    结论

    这似乎是一个非常多的开销,可能是,但专业人士超过了骗局的大时间

    BIG PRO'S

    • 您无需以任何方式更改特定页面的页面模板 . 这使得一切都变得动态,并且可以在主题之间轻松转移,而无需修改代码,如果一切都在插件中完成的话 .

    • 最多只需要在主题中创建一个 content.php 模板部分,如果你的主题还没有

    • 任何适用于主查询的分页都将在页面上起作用,而不会将任何类型的更改或任何额外的查询传递给函数 .

    那里我现在想不到更多的亲,但这些是重要的

    我希望这将有助于将来的某些人

相关问题