From 82ac76c0a790384f4efb5ced48749294293f8479 Mon Sep 17 00:00:00 2001 From: John Blackbourn Date: Wed, 11 Mar 2015 20:45:17 +0000 Subject: [PATCH] Introduce a new algorithm for displaying a hierarchical list of post objects in the `WP_Posts_List_Table`. This reduces processing time, reduces database queries, and substantially reduces memory use on sites with a high number of Pages. Props nofearinc, rodrigosprimo, nacin, johnbillion. Fixes #15459 git-svn-id: https://develop.svn.wordpress.org/trunk@31730 602fd350-edb4-49c9-b593-d223f7449a82 --- .../includes/class-wp-posts-list-table.php | 57 ++++-- src/wp-admin/includes/post.php | 1 + .../phpunit/tests/admin/includesListTable.php | 179 ++++++++++++++++++ 3 files changed, 224 insertions(+), 13 deletions(-) create mode 100644 tests/phpunit/tests/admin/includesListTable.php diff --git a/src/wp-admin/includes/class-wp-posts-list-table.php b/src/wp-admin/includes/class-wp-posts-list-table.php index 353252cb45..022d0293ff 100644 --- a/src/wp-admin/includes/class-wp-posts-list-table.php +++ b/src/wp-admin/includes/class-wp-posts-list-table.php @@ -86,6 +86,17 @@ class WP_Posts_List_Table extends WP_List_Table { } } + /** + * Sets whether the table layout should be hierarchical or not. + * + * @since 4.2.0 + * + * @param bool $display Whether the table layout should be hierarchical. + */ + public function set_hierarchical_display( $display ) { + $this->hierarchical_display = $display; + } + public function ajax_user_can() { return current_user_can( get_post_type_object( $this->screen->post_type )->cap->edit_posts ); } @@ -95,7 +106,7 @@ class WP_Posts_List_Table extends WP_List_Table { $avail_post_stati = wp_edit_posts_query(); - $this->hierarchical_display = ( is_post_type_hierarchical( $this->screen->post_type ) && 'menu_order title' == $wp_query->query['orderby'] ); + $this->set_hierarchical_display( is_post_type_hierarchical( $this->screen->post_type ) && 'menu_order title' == $wp_query->query['orderby'] ); $total_items = $this->hierarchical_display ? $wp_query->post_count : $wp_query->found_posts; @@ -478,20 +489,20 @@ class WP_Posts_List_Table extends WP_List_Table { $count = 0; $start = ( $pagenum - 1 ) * $per_page; $end = $start + $per_page; + $to_display = array(); foreach ( $pages as $page ) { if ( $count >= $end ) break; if ( $count >= $start ) { - echo "\t"; - $this->single_row( $page, $level ); + $to_display[$page->ID] = $level; } $count++; if ( isset( $children_pages ) ) - $this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page ); + $this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page, $to_display ); } // If it is the last pagenum and there are orphaned pages, display them with paging as well. @@ -502,14 +513,25 @@ class WP_Posts_List_Table extends WP_List_Table { break; if ( $count >= $start ) { - echo "\t"; - $this->single_row( $op, 0 ); + $to_display[$op->ID] = 0; } $count++; } } } + + $ids = array_keys( $to_display ); + _prime_post_caches( $ids ); + + if ( ! isset( $GLOBALS['post'] ) ) { + $GLOBALS['post'] = array_shift( $ids ); + } + + foreach ( $to_display as $page_id => $level ) { + echo "\t"; + $this->single_row( $page_id, $level ); + } } /** @@ -517,6 +539,7 @@ class WP_Posts_List_Table extends WP_List_Table { * together with paging support * * @since 3.1.0 (Standalone function exists since 2.6.0) + * @since 4.2.0 Added the `$to_display` parameter. * * @param array $children_pages * @param int $count @@ -524,8 +547,9 @@ class WP_Posts_List_Table extends WP_List_Table { * @param int $level * @param int $pagenum * @param int $per_page + * @param array $to_display list of pages to be displayed */ - private function _page_rows( &$children_pages, &$count, $parent, $level, $pagenum, $per_page ) { + private function _page_rows( &$children_pages, &$count, $parent, $level, $pagenum, $per_page, &$to_display ) { if ( ! isset( $children_pages[$parent] ) ) return; @@ -543,7 +567,13 @@ class WP_Posts_List_Table extends WP_List_Table { $my_parents = array(); $my_parent = $page->post_parent; while ( $my_parent ) { - $my_parent = get_post( $my_parent ); + // Get the ID from the list or the attribute if my_parent is an object + $parent_id = $my_parent; + if ( is_object( $my_parent ) ) { + $parent_id = $my_parent->ID; + } + + $my_parent = get_post( $parent_id ); $my_parents[] = $my_parent; if ( !$my_parent->post_parent ) break; @@ -551,20 +581,18 @@ class WP_Posts_List_Table extends WP_List_Table { } $num_parents = count( $my_parents ); while ( $my_parent = array_pop( $my_parents ) ) { - echo "\t"; - $this->single_row( $my_parent, $level - $num_parents ); + $to_display[$my_parent->ID] = $level - $num_parents; $num_parents--; } } if ( $count >= $start ) { - echo "\t"; - $this->single_row( $page, $level ); + $to_display[$page->ID] = $level; } $count++; - $this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page ); + $this->_page_rows( $children_pages, $count, $page->ID, $level + 1, $pagenum, $per_page, $to_display ); } unset( $children_pages[$parent] ); //required in order to keep track of orphans @@ -579,6 +607,9 @@ class WP_Posts_List_Table extends WP_List_Table { global $mode; $global_post = get_post(); + + $post = get_post( $post ); + $GLOBALS['post'] = $post; setup_postdata( $post ); diff --git a/src/wp-admin/includes/post.php b/src/wp-admin/includes/post.php index 545c57f56e..4dc46ac3df 100644 --- a/src/wp-admin/includes/post.php +++ b/src/wp-admin/includes/post.php @@ -1039,6 +1039,7 @@ function wp_edit_posts_query( $q = false ) { $query['order'] = 'asc'; $query['posts_per_page'] = -1; $query['posts_per_archive_page'] = -1; + $query['fields'] = 'id=>parent'; } if ( ! empty( $q['show_sticky'] ) ) diff --git a/tests/phpunit/tests/admin/includesListTable.php b/tests/phpunit/tests/admin/includesListTable.php new file mode 100644 index 0000000000..9b95b1fac1 --- /dev/null +++ b/tests/phpunit/tests/admin/includesListTable.php @@ -0,0 +1,179 @@ +table = _get_list_table( 'WP_Posts_List_Table' ); + + parent::setUp(); + + // note that our top/children/grandchildren arrays are 1-indexed + + // create top level pages + $num_posts = 5; + foreach ( range( 1, $num_posts ) as $i ) { + $this->top[$i] = $this->factory->post->create_and_get( array( + 'post_type' => 'page', + 'post_title' => sprintf( 'Top Level Page %d', $i ), + ) ); + } + + // create child pages + $num_children = 3; + foreach ( $this->top as $top => $top_page ) { + foreach ( range( 1, $num_children ) as $i ) { + $this->children[$top][$i] = $this->factory->post->create_and_get( array( + 'post_type' => 'page', + 'post_parent' => $top_page->ID, + 'post_title' => sprintf( 'Child %d', $i ), + ) ); + } + } + + // create grand-child pages for the third and fourth top-level pages + $num_grandchildren = 3; + foreach ( range( 3, 4 ) as $top ) { + foreach ( $this->children[$top] as $child => $child_page ) { + foreach ( range( 1, $num_grandchildren ) as $i ) { + $this->grandchildren[$top][$child][$i] = $this->factory->post->create_and_get( array( + 'post_type' => 'page', + 'post_parent' => $child_page->ID, + 'post_title' => sprintf( 'Grandchild %d', $i ), + ) ); + } + } + } + } + + /** + * @ticket 15459 + */ + function test_list_hierarchical_pages_first_page() { + $this->_test_list_hierarchical_page( array( + 'paged' => 1, + 'posts_per_page' => 2, + ), array( + $this->top[1]->ID, + $this->children[1][1]->ID, + ) ); + } + + /** + * @ticket 15459 + */ + function test_list_hierarchical_pages_second_page() { + $this->_test_list_hierarchical_page( array( + 'paged' => 2, + 'posts_per_page' => 2, + ), array( + $this->top[1]->ID, + $this->children[1][2]->ID, + $this->children[1][3]->ID, + ) ); + } + + /** + * @ticket 15459 + */ + function test_search_hierarchical_pages_first_page() { + $this->_test_list_hierarchical_page( array( + 'paged' => 1, + 'posts_per_page' => 2, + 's' => 'Child', + ), array( + $this->children[1][1]->ID, + $this->children[1][2]->ID, + ) ); + } + + /** + * @ticket 15459 + */ + function test_search_hierarchical_pages_second_page() { + $this->_test_list_hierarchical_page( array( + 'paged' => 2, + 'posts_per_page' => 2, + 's' => 'Top', + ), array( + $this->top[3]->ID, + $this->top[4]->ID, + ) ); + } + + /** + * @ticket 15459 + */ + function test_grandchildren_hierarchical_pages_first_page() { + // page 6 is the first page with grandchildren + $this->_test_list_hierarchical_page( array( + 'paged' => 6, + 'posts_per_page' => 2, + ), array( + $this->top[3]->ID, + $this->children[3][1]->ID, + $this->grandchildren[3][1][1]->ID, + $this->grandchildren[3][1][2]->ID, + ) ); + } + + /** + * @ticket 15459 + */ + function test_grandchildren_hierarchical_pages_second_page() { + // page 7 is the second page with grandchildren + $this->_test_list_hierarchical_page( array( + 'paged' => 7, + 'posts_per_page' => 2, + ), array( + $this->top[3]->ID, + $this->children[3][1]->ID, + $this->grandchildren[3][1][3]->ID, + $this->children[3][2]->ID, + ) ); + } + + /** + * Helper function to test the output of a page which uses `WP_Posts_List_Table`. + * + * @param array $args Query args for the list of pages. + * @param array $expected_ids Expected IDs of pages returned. + */ + protected function _test_list_hierarchical_page( array $args, array $expected_ids ) { + $matches = array(); + + $_REQUEST['paged'] = $args['paged']; + $GLOBALS['per_page'] = $args['posts_per_page']; + + $args = array_merge( array( + 'post_type' => 'page', + ), $args ); + + // Mimic the behaviour of `wp_edit_posts_query()`: + if ( ! isset( $args['orderby'] ) ) { + $args['orderby'] = 'menu_order title'; + $args['order'] = 'asc'; + $args['posts_per_page'] = -1; + $args['posts_per_archive_page'] = -1; + } + + $pages = new WP_Query( $args ); + + ob_start(); + $this->table->set_hierarchical_display( true ); + $this->table->display_rows( $pages->posts ); + $output = ob_get_clean(); + + preg_match_all( '|]*>|', $output, $matches ); + + $this->assertCount( count( $expected_ids ), array_keys( $matches[0] ) ); + + foreach ( $expected_ids as $id ) { + $this->assertContains( sprintf( 'id="post-%d"', $id ), $output ); + } + } + +}