Skip to content

Commit 096976a

Browse files
committed
Add logic to sort posts by hierarchy
1 parent ffceac5 commit 096976a

File tree

5 files changed

+408
-2
lines changed

5 files changed

+408
-2
lines changed
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
/**
4+
* Implements sorting posts by parent-child relationship.
5+
*
6+
* @package WordPress
7+
* @since 6.8.0
8+
*/
9+
10+
/**
11+
* Sort post by hierarchy (parent-child relationship).
12+
*
13+
* @since 6.8.0
14+
*/
15+
class Hierarchical_Sort {
16+
17+
private static $post_ids = array();
18+
private static $levels = array();
19+
private static $instance;
20+
21+
public static function get_instance() {
22+
if ( null === self::$instance ) {
23+
self::$instance = new self();
24+
}
25+
26+
return self::$instance;
27+
}
28+
29+
public function run( $args ) {
30+
$new_args = array_merge(
31+
$args,
32+
array(
33+
'fields' => 'id=>parent',
34+
'posts_per_page' => -1,
35+
)
36+
);
37+
$query = new WP_Query( $new_args );
38+
$posts = $query->posts;
39+
$result = self::sort( $posts );
40+
41+
self::$post_ids = $result['post_ids'];
42+
self::$levels = $result['levels'];
43+
}
44+
45+
/**
46+
* Check if the request is eligible for hierarchical sorting.
47+
*
48+
* @param array $request The request data.
49+
*
50+
* @return bool Return true if the request is eligible for hierarchical sorting.
51+
*/
52+
public static function is_eligible( $request ) {
53+
if ( ! isset( $request['orderby_hierarchy'] ) || true !== $request['orderby_hierarchy'] ) {
54+
return false;
55+
}
56+
57+
return true;
58+
}
59+
60+
public static function get_ancestor( $post_id ) {
61+
return get_post( $post_id )->post_parent ?? 0;
62+
}
63+
64+
/**
65+
* Sort posts by hierarchy.
66+
*
67+
* Takes an array of posts and sorts them based on their parent-child relationships.
68+
* It also tracks the level depth of each post in the hierarchy.
69+
*
70+
* Example input:
71+
* ```
72+
* [
73+
* ['ID' => 4, 'post_parent' => 2],
74+
* ['ID' => 2, 'post_parent' => 0],
75+
* ['ID' => 3, 'post_parent' => 2],
76+
* ]
77+
* ```
78+
*
79+
* Example output:
80+
* ```
81+
* [
82+
* 'post_ids' => [2, 4, 3],
83+
* 'levels' => [0, 1, 1]
84+
* ]
85+
* ```
86+
*
87+
* @param array $posts Array of post objects containing ID and post_parent properties.
88+
*
89+
* @return array {
90+
* Sorted post IDs and their hierarchical levels
91+
*
92+
* @type array $post_ids Array of post IDs
93+
* @type array $levels Array of levels for the corresponding post ID in the same index
94+
* }
95+
*/
96+
public static function sort( $posts ) {
97+
/*
98+
* Arrange pages in two arrays:
99+
*
100+
* - $top_level: posts whose parent is 0
101+
* - $children: post ID as the key and an array of children post IDs as the value.
102+
* Example: $children[10][] contains all sub-pages whose parent is 10.
103+
*
104+
* Additionally, keep track of the levels of each post in $levels.
105+
* Example: $levels[10] = 0 means the post ID is a top-level page.
106+
*
107+
*/
108+
$top_level = array();
109+
$children = array();
110+
foreach ( $posts as $post ) {
111+
if ( empty( $post->post_parent ) ) {
112+
$top_level[] = $post->ID;
113+
} else {
114+
$children[ $post->post_parent ][] = $post->ID;
115+
}
116+
}
117+
118+
$ids = array();
119+
$levels = array();
120+
self::add_hierarchical_ids( $ids, $levels, 0, $top_level, $children );
121+
122+
// Process remaining children.
123+
if ( ! empty( $children ) ) {
124+
foreach ( $children as $parent_id => $child_ids ) {
125+
$level = 0;
126+
$ancestor = $parent_id;
127+
while ( 0 !== $ancestor ) {
128+
++$level;
129+
$ancestor = self::get_ancestor( $ancestor );
130+
}
131+
self::add_hierarchical_ids( $ids, $levels, $level, $child_ids, $children );
132+
}
133+
}
134+
135+
return array(
136+
'post_ids' => $ids,
137+
'levels' => $levels,
138+
);
139+
}
140+
141+
private static function add_hierarchical_ids( &$ids, &$levels, $level, $to_process, $children ) {
142+
foreach ( $to_process as $id ) {
143+
if ( in_array( $id, $ids, true ) ) {
144+
continue;
145+
}
146+
$ids[] = $id;
147+
$levels[ $id ] = $level;
148+
149+
if ( isset( $children[ $id ] ) ) {
150+
self::add_hierarchical_ids( $ids, $levels, $level + 1, $children[ $id ], $children );
151+
unset( $children[ $id ] );
152+
}
153+
}
154+
}
155+
156+
public static function get_post_ids() {
157+
return self::$post_ids;
158+
}
159+
160+
public static function get_levels() {
161+
return self::$levels;
162+
}
163+
}
164+
165+
function rest_page_query_hierarchical_sort_filter_by_post_in( $args, $request ) {
166+
if ( ! Hierarchical_Sort::is_eligible( $request ) ) {
167+
return $args;
168+
}
169+
170+
$hs = Hierarchical_Sort::get_instance();
171+
$hs->run( $args );
172+
173+
// Reconfigure the args to display only the ids in the list.
174+
$args['post__in'] = $hs->get_post_ids();
175+
$args['orderby'] = 'post__in';
176+
177+
return $args;
178+
}
179+
180+
function rest_prepare_page_hierarchical_sort_add_levels( $response, $post, $request ) {
181+
if ( ! Hierarchical_Sort::is_eligible( $request ) ) {
182+
return $response;
183+
}
184+
185+
$hs = Hierarchical_Sort::get_instance();
186+
$response->data['level'] = $hs->get_levels()[ $post->ID ];
187+
188+
return $response;
189+
}

src/wp-includes/default-filters.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,4 +770,8 @@
770770
add_filter( 'rest_prepare_post', 'insert_hooked_blocks_into_rest_response', 10, 2 );
771771
add_filter( 'rest_prepare_wp_navigation', 'insert_hooked_blocks_into_rest_response', 10, 2 );
772772

773+
// Filter the pages endpoint to consider orderby_hierarchy.
774+
add_filter( 'rest_page_query', 'rest_page_query_hierarchical_sort_filter_by_post_in', 10, 2 );
775+
add_filter( 'rest_prepare_page', 'rest_prepare_page_hierarchical_sort_add_levels', 10, 3 );
776+
773777
unset( $filter, $action );

src/wp-includes/rest-api/endpoints/class-wp-rest-posts-controller.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2991,22 +2991,27 @@ public function get_collection_params() {
29912991
$post_type = get_post_type_object( $this->post_type );
29922992

29932993
if ( $post_type->hierarchical || 'attachment' === $this->post_type ) {
2994-
$query_params['parent'] = array(
2994+
$query_params['parent'] = array(
29952995
'description' => __( 'Limit result set to items with particular parent IDs.' ),
29962996
'type' => 'array',
29972997
'items' => array(
29982998
'type' => 'integer',
29992999
),
30003000
'default' => array(),
30013001
);
3002-
$query_params['parent_exclude'] = array(
3002+
$query_params['parent_exclude'] = array(
30033003
'description' => __( 'Limit result set to all items except those of a particular parent ID.' ),
30043004
'type' => 'array',
30053005
'items' => array(
30063006
'type' => 'integer',
30073007
),
30083008
'default' => array(),
30093009
);
3010+
$query_params['orderby_hierarchy'] = array(
3011+
'description' => 'Sort pages by hierarchy.',
3012+
'type' => 'boolean',
3013+
'default' => false,
3014+
);
30103015
}
30113016

30123017
$query_params['search_columns'] = array(

src/wp-settings.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@
404404
require ABSPATH . WPINC . '/interactivity-api/class-wp-interactivity-api-directives-processor.php';
405405
require ABSPATH . WPINC . '/interactivity-api/interactivity-api.php';
406406
require ABSPATH . WPINC . '/class-wp-plugin-dependencies.php';
407+
require ABSPATH . WPINC . '/class-hierarchical-sort.php';
407408

408409
add_action( 'after_setup_theme', array( wp_script_modules(), 'add_hooks' ) );
409410
add_action( 'after_setup_theme', array( wp_interactivity(), 'add_hooks' ) );

0 commit comments

Comments
 (0)