在@birgire 回答的启发下,我想出了以下想法。 (注意:This is a copy of my answer from this answer over at WPSE)
我在这里尝试做的是使用后注入,而不是完全更改主查询并陷入所有上述问题,例如直接更改全局变量、全局值问题和重新分配页面模板。
通过使用帖子注入,我能够保持完整的帖子完整性,因此$wp_the_query->post、$wp_query->post、$posts 和$post 在整个模板中保持不变,它们都只保存当前页面对象真实页面的情况。这样,像面包屑这样的功能仍然认为当前页面是真实页面而不是某种存档
我不得不稍微改变主查询(通过过滤器和操作)以调整分页,但我们会做到这一点。
注射后查询
为了完成后期注入,我使用自定义查询返回注入所需的帖子。我还使用自定义查询的$found_pages 属性来调整主查询的属性,以使分页从主查询工作。帖子通过loop_end 操作注入到主查询中。
为了使自定义查询在类外可访问和可用,我引入了一些操作。
这些钩子为您提供了完全不干涉的体验,因为您不需要更改页面模板本身的任何内容,这是我从一开始的初衷。页面可以完全从插件或函数文件中更改,这使得它非常动态
我还使用了get_template_part() 来加载一个模板部分,该部分将用于显示帖子。今天的大多数主题都使用模板部分,这使得这在课堂上非常有用。如果您的主题使用content.php,您可以简单地将content 传递给$templatePart 以加载content.php。
如果您需要模板部分的帖子格式支持,这很容易,您仍然可以将content 传递给$templatePart 并将$postFormatSupport 设置为true,然后将加载模板部分content-video.php帖子格式为video的帖子
主要查询
通过相应的过滤器和操作对主查询进行了以下更改
-
为了对主查询进行分页:
注入器查询的$found_posts 属性值通过found_posts 过滤器传递给主查询对象的值
通过pre_get_posts将用户传递参数posts_per_page的值设置给主查询
$max_num_pages 是使用 $found_posts 和 posts_per_page 中的帖子数量计算得出的。因为is_singular 在页面上为真,所以它禁止设置LIMIT 子句。简单地将is_singular 设置为false 会导致一些问题,因此我决定通过post_limits 过滤器设置LIMIT 子句。我将LIMIT 子句的offset 设置为0 以避免分页页面上出现404
这会处理分页和后期注入可能引起的任何问题
页面对象
当前页面对象可通过使用页面上的默认循环显示为帖子,独立于注入帖子的顶部。如果您不需要这个,您可以简单地将$removePageFromLoop 设置为true,这将隐藏页面内容而不显示。
在这个阶段,我使用 CSS 通过 loop_start 和 loop_end 操作隐藏页面对象,因为我找不到其他方法。此方法的缺点是,如果您隐藏页面对象,则默认情况下,主查询中与 the_post 操作挂钩的任何内容也将被隐藏。
课程
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() 这样的内置函数也应该可以工作。
这是最终结果
静态首页
在静态首页以及我的分页功能上,一切都按预期工作,无需进行任何修改
结论
这可能看起来确实是很多开销,也可能是这样,但是专业人士的胜过骗局的重要时间
BIG PRO'S
您无需以任何方式更改特定页面的页面模板。这使得一切都是动态的,并且可以轻松地在主题之间转移,而无需修改代码,无论一切都在插件中完成。
如果你的主题还没有,你最多只需要在你的主题中创建一个content.php模板部分
在主查询上工作的任何分页都将在页面上工作,无需任何类型的更改或传递给函数的查询之外的任何额外内容。
我现在想不到还有更多的专业人士,但这些是重要的
我希望这对将来的某人有所帮助