Skip to content

Commit

Permalink
Add logic to sort posts by hierarchy
Browse files Browse the repository at this point in the history
  • Loading branch information
oandregal committed Dec 17, 2024
1 parent ffceac5 commit 096976a
Show file tree
Hide file tree
Showing 5 changed files with 408 additions and 2 deletions.
189 changes: 189 additions & 0 deletions src/wp-includes/class-hierarchical-sort.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
<?php

/**
* Implements sorting posts by parent-child relationship.
*
* @package WordPress
* @since 6.8.0
*/

/**
* Sort post by hierarchy (parent-child relationship).
*
* @since 6.8.0
*/
class Hierarchical_Sort {

private static $post_ids = array();
private static $levels = array();
private static $instance;

public static function get_instance() {
if ( null === self::$instance ) {
self::$instance = new self();
}

return self::$instance;
}

public function run( $args ) {
$new_args = array_merge(
$args,
array(
'fields' => 'id=>parent',
'posts_per_page' => -1,
)
);
$query = new WP_Query( $new_args );
$posts = $query->posts;
$result = self::sort( $posts );

self::$post_ids = $result['post_ids'];
self::$levels = $result['levels'];
}

/**
* Check if the request is eligible for hierarchical sorting.
*
* @param array $request The request data.
*
* @return bool Return true if the request is eligible for hierarchical sorting.
*/
public static function is_eligible( $request ) {
if ( ! isset( $request['orderby_hierarchy'] ) || true !== $request['orderby_hierarchy'] ) {
return false;
}

return true;
}

public static function get_ancestor( $post_id ) {
return get_post( $post_id )->post_parent ?? 0;
}

/**
* Sort posts by hierarchy.
*
* Takes an array of posts and sorts them based on their parent-child relationships.
* It also tracks the level depth of each post in the hierarchy.
*
* Example input:
* ```
* [
* ['ID' => 4, 'post_parent' => 2],
* ['ID' => 2, 'post_parent' => 0],
* ['ID' => 3, 'post_parent' => 2],
* ]
* ```
*
* Example output:
* ```
* [
* 'post_ids' => [2, 4, 3],
* 'levels' => [0, 1, 1]
* ]
* ```
*
* @param array $posts Array of post objects containing ID and post_parent properties.
*
* @return array {
* Sorted post IDs and their hierarchical levels
*
* @type array $post_ids Array of post IDs
* @type array $levels Array of levels for the corresponding post ID in the same index
* }
*/
public static function sort( $posts ) {
/*
* Arrange pages in two arrays:
*
* - $top_level: posts whose parent is 0
* - $children: post ID as the key and an array of children post IDs as the value.
* Example: $children[10][] contains all sub-pages whose parent is 10.
*
* Additionally, keep track of the levels of each post in $levels.
* Example: $levels[10] = 0 means the post ID is a top-level page.
*
*/
$top_level = array();
$children = array();
foreach ( $posts as $post ) {
if ( empty( $post->post_parent ) ) {
$top_level[] = $post->ID;
} else {
$children[ $post->post_parent ][] = $post->ID;
}
}

$ids = array();
$levels = array();
self::add_hierarchical_ids( $ids, $levels, 0, $top_level, $children );

// Process remaining children.
if ( ! empty( $children ) ) {
foreach ( $children as $parent_id => $child_ids ) {
$level = 0;
$ancestor = $parent_id;
while ( 0 !== $ancestor ) {
++$level;
$ancestor = self::get_ancestor( $ancestor );
}
self::add_hierarchical_ids( $ids, $levels, $level, $child_ids, $children );
}
}

return array(
'post_ids' => $ids,
'levels' => $levels,
);
}

private static function add_hierarchical_ids( &$ids, &$levels, $level, $to_process, $children ) {
foreach ( $to_process as $id ) {
if ( in_array( $id, $ids, true ) ) {
continue;
}
$ids[] = $id;
$levels[ $id ] = $level;

if ( isset( $children[ $id ] ) ) {
self::add_hierarchical_ids( $ids, $levels, $level + 1, $children[ $id ], $children );
unset( $children[ $id ] );
}
}
}

public static function get_post_ids() {
return self::$post_ids;
}

public static function get_levels() {
return self::$levels;
}
}

function rest_page_query_hierarchical_sort_filter_by_post_in( $args, $request ) {
if ( ! Hierarchical_Sort::is_eligible( $request ) ) {
return $args;
}

$hs = Hierarchical_Sort::get_instance();
$hs->run( $args );

// Reconfigure the args to display only the ids in the list.
$args['post__in'] = $hs->get_post_ids();
$args['orderby'] = 'post__in';

return $args;
}

function rest_prepare_page_hierarchical_sort_add_levels( $response, $post, $request ) {
if ( ! Hierarchical_Sort::is_eligible( $request ) ) {
return $response;
}

$hs = Hierarchical_Sort::get_instance();
$response->data['level'] = $hs->get_levels()[ $post->ID ];

return $response;
}
4 changes: 4 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -770,4 +770,8 @@
add_filter( 'rest_prepare_post', 'insert_hooked_blocks_into_rest_response', 10, 2 );
add_filter( 'rest_prepare_wp_navigation', 'insert_hooked_blocks_into_rest_response', 10, 2 );

// Filter the pages endpoint to consider orderby_hierarchy.
add_filter( 'rest_page_query', 'rest_page_query_hierarchical_sort_filter_by_post_in', 10, 2 );
add_filter( 'rest_prepare_page', 'rest_prepare_page_hierarchical_sort_add_levels', 10, 3 );

unset( $filter, $action );
Original file line number Diff line number Diff line change
Expand Up @@ -2991,22 +2991,27 @@ public function get_collection_params() {
$post_type = get_post_type_object( $this->post_type );

if ( $post_type->hierarchical || 'attachment' === $this->post_type ) {
$query_params['parent'] = array(
$query_params['parent'] = array(
'description' => __( 'Limit result set to items with particular parent IDs.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['parent_exclude'] = array(
$query_params['parent_exclude'] = array(
'description' => __( 'Limit result set to all items except those of a particular parent ID.' ),
'type' => 'array',
'items' => array(
'type' => 'integer',
),
'default' => array(),
);
$query_params['orderby_hierarchy'] = array(
'description' => 'Sort pages by hierarchy.',
'type' => 'boolean',
'default' => false,
);
}

$query_params['search_columns'] = array(
Expand Down
1 change: 1 addition & 0 deletions src/wp-settings.php
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,7 @@
require ABSPATH . WPINC . '/interactivity-api/class-wp-interactivity-api-directives-processor.php';
require ABSPATH . WPINC . '/interactivity-api/interactivity-api.php';
require ABSPATH . WPINC . '/class-wp-plugin-dependencies.php';
require ABSPATH . WPINC . '/class-hierarchical-sort.php';

add_action( 'after_setup_theme', array( wp_script_modules(), 'add_hooks' ) );
add_action( 'after_setup_theme', array( wp_interactivity(), 'add_hooks' ) );
Expand Down
Loading

0 comments on commit 096976a

Please sign in to comment.