Force comment pagination on single posts.

Previously, the 'page_comments' toggle allowed users to disable comment
pagination. This toggle was only superficial, however. Even with
'page_comments' turned on, `comments_template()` loaded all of a post's
comments into memory, and passed them to `wp_list_comments()` and
`Walker_Comment`, the latter of which produced markup for only the
current page of comments. In other words, it was possible to enable
'page_comments', thereby showing only a subset of a post's comments on a given
page, but all comments continued to be loaded in the background. This technique
scaled poorly. Posts with hundreds or thousands of comments would load slowly,
or not at all, even when the 'comments_per_page' setting was set to a
reasonable number.

Recent changesets have addressed this problem through more efficient tree-
walking, better descendant caching, and more selective queries for top-level
post comments. The current changeset completes the project by addressing the
root issue: that loading a post causes all of its comments to be loaded too.

Here's the breakdown:

* Comment pagination is now forced. Setting 'page_comments' to false leads to evil things when you have many comments. If you want to avoid pagination, set 'comments_per_page' to something high.
* The 'page_comments' setting has been expunged from options-discussion.php, and from places in the codebase where it was referenced. For plugins relying on 'page_comments', we now force the value to `true` with a `pre_option` filter.
* `comments_template()` now queries for an appropriately small number of comments. Usually, this means the `comments_per_page` value.
* To preserve the current (odd) behavior for comment pagination links, some unholy hacks have been inserted into `comments_template()`. The ugliness is insulated in this function for backward compatibility and to minimize collateral damage. A side-effect is that, for certain settings of 'default_comments_page', up to 2x the value of `comments_per_page` might be fetched at a time.
* In support of these changes, a `$format` parameter has been added to `WP_Comment::get_children()`. This param allows you to request a flattened array of comment children, suitable for feeding into `Walker_Comment`.
* `WP_Query` loops are now informed about total available comment counts and comment pages by the `WP_Comment_Query` (`found_comments`, `max_num_pages`), instead of by `Walker_Comment`.

Aside from radical performance improvements in the case of a post with many
comments, this changeset fixes a bug that caused the first page of comments to
be partial (`found_comments` % `comments_per_page`), rather than the last, as
you'd expect.

Props boonebgorges, wonderboymusic.
Fixes #8071.

git-svn-id: https://develop.svn.wordpress.org/trunk@34561 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Boone Gorges
2015-09-25 20:39:18 +00:00
parent 653ababa6f
commit 0b7e8399b0
13 changed files with 122 additions and 50 deletions

View File

@@ -683,7 +683,7 @@ function get_comment_link( $comment = null, $args = array() ) {
$defaults = array( 'type' => 'all', 'page' => '', 'per_page' => '', 'max_depth' => '' );
$args = wp_parse_args( $args, $defaults );
if ( '' === $args['per_page'] && get_option('page_comments') )
if ( '' === $args['per_page'] )
$args['per_page'] = get_option('comments_per_page');
if ( empty($args['per_page']) ) {
@@ -1214,10 +1214,11 @@ function comments_template( $file = '/comments.php', $separate_comments = false
$comment_author_url = esc_url($commenter['comment_author_url']);
$comment_args = array(
'order' => 'ASC',
'orderby' => 'comment_date_gmt',
'status' => 'approve',
'post_id' => $post->ID,
'hierarchical' => 'threaded',
'no_found_rows' => false,
'update_comment_meta_cache' => false, // We lazy-load comment meta for performance.
);
@@ -1227,7 +1228,87 @@ function comments_template( $file = '/comments.php', $separate_comments = false
$comment_args['include_unapproved'] = array( $comment_author_email );
}
$comments = get_comments( $comment_args );
$per_page = (int) get_query_var( 'comments_per_page' );
if ( 0 === $per_page ) {
$per_page = (int) get_option( 'comments_per_page' );
}
$flip_comment_order = $trim_comments_on_page = false;
if ( $post->comment_count > $per_page ) {
$comment_args['number'] = $per_page;
/*
* For legacy reasons, higher page numbers always mean more recent comments, regardless of sort order.
* Since we don't have full pagination info until after the query, we use some tricks to get the
* right comments for the current page.
*
* Abandon all hope, ye who enter here!
*/
$page = (int) get_query_var( 'cpage' );
if ( 'newest' === get_option( 'default_comments_page' ) ) {
if ( $page ) {
$comment_args['order'] = 'ASC';
/*
* We don't have enough data (namely, the total number of comments) to calculate an
* exact offset. We'll fetch too many comments, and trim them as needed
* after the query.
*/
$offset = ( $page - 2 ) * $per_page;
if ( 0 > $offset ) {
// `WP_Comment_Query` doesn't support negative offsets.
$comment_args['offset'] = 0;
} else {
$comment_args['offset'] = $offset;
}
// Fetch double the number of comments we need.
$comment_args['number'] += $per_page;
$trim_comments_on_page = true;
} else {
$comment_args['order'] = 'DESC';
$comment_args['offset'] = 0;
$flip_comment_order = true;
}
} else {
$comment_args['order'] = 'ASC';
if ( $page ) {
$comment_args['offset'] = ( $page - 1 ) * $per_page;
} else {
$comment_args['offset'] = 0;
}
}
}
$comment_query = new WP_Comment_Query( $comment_args );
$_comments = $comment_query->comments;
// Delightful pagination quirk #1: first page of results sometimes needs reordering.
if ( $flip_comment_order ) {
$_comments = array_reverse( $_comments );
}
// Delightful pagination quirk #2: reverse chronological order requires page shifting.
if ( $trim_comments_on_page ) {
// Correct the value of max_num_pages, which is wrong because we manipulated the per_page 'number'.
$comment_query->max_num_pages = ceil( $comment_query->found_comments / $per_page );
// Identify the number of comments that should appear on page 1.
$page_1_count = $comment_query->found_comments - ( ( $comment_query->max_num_pages - 1 ) * $per_page );
// Use that value to shift the matched comments.
if ( 1 === $page ) {
$_comments = array_slice( $_comments, 0, $page_1_count );
} else {
$_comments = array_slice( $_comments, $page_1_count, $per_page );
}
}
// Trees must be flattened before they're passed to the walker.
$comments_flat = array();
foreach ( $_comments as $_comment ) {
$comments_flat = array_merge( $comments_flat, array( $_comment ), $_comment->get_children( 'flat' ) );
}
/**
* Filter the comments array.
@@ -1237,9 +1318,10 @@ function comments_template( $file = '/comments.php', $separate_comments = false
* @param array $comments Array of comments supplied to the comments template.
* @param int $post_ID Post ID.
*/
$wp_query->comments = apply_filters( 'comments_array', $comments, $post->ID );
$wp_query->comments = apply_filters( 'comments_array', $comments_flat, $post->ID );
$comments = &$wp_query->comments;
$wp_query->comment_count = count($wp_query->comments);
$wp_query->max_num_comment_pages = $comment_query->max_num_pages;
if ( $separate_comments ) {
$wp_query->comments_by_type = separate_comments($comments);
@@ -1249,7 +1331,7 @@ function comments_template( $file = '/comments.php', $separate_comments = false
}
$overridden_cpage = false;
if ( '' == get_query_var('cpage') && get_option('page_comments') ) {
if ( '' == get_query_var('cpage') ) {
set_query_var( 'cpage', 'newest' == get_option('default_comments_page') ? get_comment_pages_count() : 1 );
$overridden_cpage = true;
}
@@ -1825,9 +1907,14 @@ function wp_list_comments( $args = array(), $comments = null ) {
} else {
$_comments = $wp_query->comments;
}
// Pagination is already handled by `WP_Comment_Query`, so we tell Walker not to bother.
if ( 1 < $wp_query->max_num_comment_pages ) {
$r['page'] = 1;
}
}
if ( '' === $r['per_page'] && get_option('page_comments') )
if ( '' === $r['per_page'] )
$r['per_page'] = get_query_var('comments_per_page');
if ( empty($r['per_page']) ) {
@@ -1866,7 +1953,6 @@ function wp_list_comments( $args = array(), $comments = null ) {
}
$output = $walker->paged_walk( $_comments, $r['max_depth'], $r['page'], $r['per_page'], $r );
$wp_query->max_num_comment_pages = $walker->max_pages;
$in_comment_loop = false;