WordPress Version: 6.3
/**
* Retrieves the adjacent post.
*
* Can either be next or previous post.
*
* @since 2.5.0
*
* @global wpdb $wpdb WordPress database abstraction object.
*
* @param bool $in_same_term Optional. Whether post should be in the same taxonomy term.
* Default false.
* @param int[]|string $excluded_terms Optional. Array or comma-separated list of excluded term IDs.
* Default empty string.
* @param bool $previous Optional. Whether to retrieve previous post.
* Default true.
* @param string $taxonomy Optional. Taxonomy, if `$in_same_term` is true. Default 'category'.
* @return WP_Post|null|string Post object if successful. Null if global `$post` is not set.
* Empty string if no corresponding post exists.
*/
function get_adjacent_post($in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category')
{
global $wpdb;
$post = get_post();
if (!$post || !taxonomy_exists($taxonomy)) {
return null;
}
$current_post_date = $post->post_date;
$join = '';
$where = '';
$adjacent = $previous ? 'previous' : 'next';
if (!empty($excluded_terms) && !is_array($excluded_terms)) {
// Back-compat, $excluded_terms used to be $excluded_categories with IDs separated by " and ".
if (str_contains($excluded_terms, ' and ')) {
_deprecated_argument(__FUNCTION__, '3.3.0', sprintf(
/* translators: %s: The word 'and'. */
__('Use commas instead of %s to separate excluded terms.'),
"'and'"
));
$excluded_terms = explode(' and ', $excluded_terms);
} else {
$excluded_terms = explode(',', $excluded_terms);
}
$excluded_terms = array_map('intval', $excluded_terms);
}
/**
* Filters the IDs of terms excluded from adjacent post queries.
*
* The dynamic portion of the hook name, `$adjacent`, refers to the type
* of adjacency, 'next' or 'previous'.
*
* Possible hook names include:
*
* - `get_next_post_excluded_terms`
* - `get_previous_post_excluded_terms`
*
* @since 4.4.0
*
* @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided.
*/
$excluded_terms = apply_filters("get_{$adjacent}_post_excluded_terms", $excluded_terms);
if ($in_same_term || !empty($excluded_terms)) {
if ($in_same_term) {
$join .= " INNER JOIN {$wpdb->term_relationships} AS tr ON p.ID = tr.object_id INNER JOIN {$wpdb->term_taxonomy} AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id";
$where .= $wpdb->prepare('AND tt.taxonomy = %s', $taxonomy);
if (!is_object_in_taxonomy($post->post_type, $taxonomy)) {
return '';
}
$term_array = wp_get_object_terms($post->ID, $taxonomy, array('fields' => 'ids'));
// Remove any exclusions from the term array to include.
$term_array = array_diff($term_array, (array) $excluded_terms);
$term_array = array_map('intval', $term_array);
if (!$term_array || is_wp_error($term_array)) {
return '';
}
$where .= ' AND tt.term_id IN (' . implode(',', $term_array) . ')';
}
if (!empty($excluded_terms)) {
$where .= " AND p.ID NOT IN ( SELECT tr.object_id FROM {$wpdb->term_relationships} tr LEFT JOIN {$wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id) WHERE tt.term_id IN (" . implode(',', array_map('intval', $excluded_terms)) . ') )';
}
}
// 'post_status' clause depends on the current user.
if (is_user_logged_in()) {
$user_id = get_current_user_id();
$post_type_object = get_post_type_object($post->post_type);
if (empty($post_type_object)) {
$post_type_cap = $post->post_type;
$read_private_cap = 'read_private_' . $post_type_cap . 's';
} else {
$read_private_cap = $post_type_object->cap->read_private_posts;
}
/*
* Results should include private posts belonging to the current user, or private posts where the
* current user has the 'read_private_posts' cap.
*/
$private_states = get_post_stati(array('private' => true));
$where .= " AND ( p.post_status = 'publish'";
foreach ($private_states as $state) {
if (current_user_can($read_private_cap)) {
$where .= $wpdb->prepare(' OR p.post_status = %s', $state);
} else {
$where .= $wpdb->prepare(' OR (p.post_author = %d AND p.post_status = %s)', $user_id, $state);
}
}
$where .= ' )';
} else {
$where .= " AND p.post_status = 'publish'";
}
$op = $previous ? '<' : '>';
$order = $previous ? 'DESC' : 'ASC';
/**
* Filters the JOIN clause in the SQL for an adjacent post query.
*
* The dynamic portion of the hook name, `$adjacent`, refers to the type
* of adjacency, 'next' or 'previous'.
*
* Possible hook names include:
*
* - `get_next_post_join`
* - `get_previous_post_join`
*
* @since 2.5.0
* @since 4.4.0 Added the `$taxonomy` and `$post` parameters.
*
* @param string $join The JOIN clause in the SQL.
* @param bool $in_same_term Whether post should be in the same taxonomy term.
* @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided.
* @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
* @param WP_Post $post WP_Post object.
*/
$join = apply_filters("get_{$adjacent}_post_join", $join, $in_same_term, $excluded_terms, $taxonomy, $post);
/**
* Filters the WHERE clause in the SQL for an adjacent post query.
*
* The dynamic portion of the hook name, `$adjacent`, refers to the type
* of adjacency, 'next' or 'previous'.
*
* Possible hook names include:
*
* - `get_next_post_where`
* - `get_previous_post_where`
*
* @since 2.5.0
* @since 4.4.0 Added the `$taxonomy` and `$post` parameters.
*
* @param string $where The `WHERE` clause in the SQL.
* @param bool $in_same_term Whether post should be in the same taxonomy term.
* @param int[]|string $excluded_terms Array of excluded term IDs. Empty string if none were provided.
* @param string $taxonomy Taxonomy. Used to identify the term used when `$in_same_term` is true.
* @param WP_Post $post WP_Post object.
*/
$where = apply_filters("get_{$adjacent}_post_where", $wpdb->prepare("WHERE p.post_date {$op} %s AND p.post_type = %s {$where}", $current_post_date, $post->post_type), $in_same_term, $excluded_terms, $taxonomy, $post);
/**
* Filters the ORDER BY clause in the SQL for an adjacent post query.
*
* The dynamic portion of the hook name, `$adjacent`, refers to the type
* of adjacency, 'next' or 'previous'.
*
* Possible hook names include:
*
* - `get_next_post_sort`
* - `get_previous_post_sort`
*
* @since 2.5.0
* @since 4.4.0 Added the `$post` parameter.
* @since 4.9.0 Added the `$order` parameter.
*
* @param string $order_by The `ORDER BY` clause in the SQL.
* @param WP_Post $post WP_Post object.
* @param string $order Sort order. 'DESC' for previous post, 'ASC' for next.
*/
$sort = apply_filters("get_{$adjacent}_post_sort", "ORDER BY p.post_date {$order} LIMIT 1", $post, $order);
$query = "SELECT p.ID FROM {$wpdb->posts} AS p {$join} {$where} {$sort}";
$key = md5($query);
$last_changed = wp_cache_get_last_changed('posts');
if ($in_same_term || !empty($excluded_terms)) {
$last_changed .= wp_cache_get_last_changed('terms');
}
$cache_key = "adjacent_post:{$key}:{$last_changed}";
$result = wp_cache_get($cache_key, 'post-queries');
if (false !== $result) {
if ($result) {
$result = get_post($result);
}
return $result;
}
$result = $wpdb->get_var($query);
if (null === $result) {
$result = '';
}
wp_cache_set($cache_key, $result, 'post-queries');
if ($result) {
$result = get_post($result);
}
return $result;
}