Luigi Cavalieri - Coding, Sharing, Blogging

Raw Solution to Ordering an Array of Page Objects Hierarchically

A two steps approach to reordering an array of Page objects by `post_parent`

Raw Solution to Ordering an Array of Page Objects Hierarchically

Sometimes the need arises for an array of Page objects ordered hierarchically, but all there is available is just a collection of Page objects ordered by title for example, maybe because they were previously fetched from the database through the global $wpdb object. On these occasions, to hierarchically reorder an array as such, we have to be creative, because WordPress isn't equipped with an utility function made up on purpose — or at least I have not dug enough to uncover one. In truth, I simply enjoyed looking for my own solution, and that's the main reason why I am here now writing about it.

In a nutshell, I achieved my goal by approaching to the problem in two steps: the first consists in creating a temporary array where the elements are arrays of Page objects sharing the same value of post_parent (child pages), the second step boils down to combining the temporary array with an array of top-level Page objects (Page objects having post_parent = 0) also born inside the same loop that populated the temporary array.

Do not panic, I will go through each step in detail.

The Master Array

The code subject of this writing originated from the development cycle of SiteTree 5.3, while I was refactoring a block of code intended to produce a list of <option> tags for a drop-down control (<select> tag). The <option> tags had their visible part indented so as to look like a hierarchical list, that actually represented part of the Pages available on the website.

My intention now is just to give a context and a source to our array of Page objects.

Aforesaid list of <option> tags had been the result of the manipulation of an array of Page objects offspring of a $wpdb->get_results() call very similar to this:

1global $wpdb;
3// It is important that the objects resulting from the query
4// have at least an 'ID' and a 'post_parent' attribute.
5$the_query = "SELECT ID, post_title, post_parent
6              FROM {$wpdb->posts}
7              WHERE post_type = 'page' AND post_status = 'publish' AND
8                    post_password = '' 
9              ORDER BY menu_order, post_title ASC";
11// The array of Page objects to order hierarchically.
12$pages = $wpdb->get_results( $the_query );

I want to emphasise that for the code I am going to describe you to work, the Page objects in the array to reorder must have at least an ID and a post_parent attribute, regardless of whether such objects are instances of the WP_Post class or not.

First Step: The Sorting

In this first step we will loop through our Page objects to create two distinct arrays. The first, $ordered_pages, will be a plain array of top-level Page objects, the second, $pages_by_parent, will be a multidimensional array instead, its elements will be arrays of Page objects united by the same value of post_parent.

It is essential for the actualisation of the second step that the keys of $ordered_pages are equal to the ID attribute of the Page objects contained in the array itself, and that the first keys of the multidimensional $pages_by_parent array are equal to the values the post_parent attribute takes for each group of child pages contained in $pages_by_parent.

1// Variable that will store the ordered array 
2// of Page objects.
3$ordered_pages = array();
5// Temporary array where the elements are arrays 
6// of Page objects having all the same 
7// value of 'post_parent' (child pages).
8$pages_by_parent = array();
10// Associative array used to temporarily 
11// collect all the ids of the Page objects 
12// to order.
13$ids = array();
15// We populate the $ids array and, to make the most 
16// out of the loop, we also sanitise the Page objects.
17// This preliminary step is of paramount importance in 
18// ensuring that the loop we'll meet in the second section 
19// of this post it actually ends, instead of degenerating 
20// into an infinite iteration.
21foreach ( $pages as $page ) {
22  $page->ID          = (int) $page->ID;
23  $page->post_parent = (int) $page->post_parent;
25  // The ID of each Page object is stored 
26  // both as key and as value.
27  $ids[$page->ID] = $page->ID;
30foreach ( $pages as $page ) {
31  if (
32    // When 'post_parent' is 0, 
33    // it means that the object is a top-level Page.
34    ( $page->post_parent === 0 ) ||
36    // This condition checks whether the Page's parent 
37    // is among the elements of our array of Page objects, 
38    // if it isn't, the Page is an "orphan" 
39    // and so it is treated as a top-level Page.
40    !isset( $ids[$page->post_parent] )
41  ) {
42    $ordered_pages[$page->ID] = $page;
43  }
44  else {
45    // Notice that the first keys of $pages_by_parent 
46    // are equal to the values the 'post_parent' attribute 
47    // takes for each group of Page objects.
48    $pages_by_parent[$page->post_parent][$page->ID] = $page;
49  }

Second Step: The Merging

To recap, completing the reordering involves merging $pages_by_parent with $ordered_pages, array that still contains only top-level Page objects.

Thus what needs to be done now is to insert the groups of objects contained in $pages_by_parent in the appropriate positions of the $ordered_pages array, and repeat the task for each level of nesting.

The choice made in the first step about the keys used for the two arrays to merge is of prime importance in ensuring that the usage of server resources is kept at bay.

1// With each iteration, the content of 
2// one or more arrays making up $pages_by_parent 
3// is joined to the content of $ordered_pages.
4while( $pages_by_parent ) {
5  // Temporary array used to merge the groups 
6  // of child pages contained in $pages_by_parent 
7  // with the Page objects in $ordered_pages.
8  $array = array();
10  foreach( $ordered_pages as $page_id => $page ) {
11    $array[$page_id] = $page;
13    // Checks whether the current $page object 
14    // has child pages.
15    if ( isset( $pages_by_parent[$page_id] ) ) {
16      // Appends a group of child pages 
17      // to the temporary array.
18      foreach ( $pages_by_parent[$page_id] as $child_page_id => $child_page ) {
19          $array[$child_page_id] = $child_page;
20      }
22      // This is essential for the While loop 
23      // to draw to an end.
24      unset( $pages_by_parent[$page_id] );
25    }
26  }
28  // With each execution of the loop, 
29  // are inserted into $ordered_pages
30  // as much Page objects as each level of
31  // nesting is composed of. 
32  $ordered_pages = $array;

Using Native PHP Functions

Implementing this second step in a raw way has been only a personal preference, in fact the code can be rewritten by using native PHP functions without too much troubles, although, I wouldn't swear performance would benefit.

I leave you with an example of said alternative:

1// With each iteration, are inserted 
2// into $ordered_pages as much child pages 
3// as each level of nesting is composed of. 
4while( $pages_by_parent ) {
5  foreach( $pages_by_parent as $parent_id => $child_pages ) {
6    // Checks whether the current group of 
7    // child pages has a parent in $ordered_pages.
8    if ( isset( $ordered_pages[$parent_id] ) ) {
9      $insert_position = 1 + array_search( $parent_id, 
10                                           array_keys( $ordered_pages ) );
12      // We split $ordered_pages into two distinct arrays 
13      // according to the computed value of $insert_position.
14      // To perform such task we use array_slice() 
15      // so as to preserve the numeric keys.
16      $ordered_pages_head = array_slice( $ordered_pages, 0, 
17                                         $insert_position, true );
18      $ordered_pages_tail = array_slice( $ordered_pages, $insert_position,
19                                         null, true );
21      // Using the + operator for the merging
22      // ensures that the numeric keys are preserved.
23      $ordered_pages = $ordered_pages_head + $child_pages;
25      // Appends $ordered_pages_tail only 
26      // if it isn't an empty array.
27      if ( $ordered_pages_tail ) {
28        $ordered_pages += $ordered_pages_tail;
29      }
31      // This is essential for the While loop 
32      // to draw to an end.
33      unset( $pages_by_parent[$parent_id] );
34    }
35  }