Make WordPress Core

Changeset 27285


Ignore:
Timestamp:
02/26/2014 05:09:54 PM (12 years ago)
Author:
nacin
Message:

Make get_adjacent_post() wrap a new WP_Get_Adjacent_Post object that uses WP_Query.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/link-template.php

    r27265 r27285  
    1 <?php
     1 <?php
    22/**
    33 * WordPress Link Template Functions
     
    11201120 * @param bool         $previous       Optional. Whether to retrieve previous post.
    11211121 * @param string       $taxonomy       Optional. Taxonomy, if $in_same_term is true. Default 'category'.
    1122  * @return mixed       Post object if successful. Null if global $post is not set. Empty string if no corresponding post exists.
     1122 * @return mixed       Post object if successful. Null if current post doesn't exist. Empty string if no corresponding adjacent post exists.
    11231123 */
    11241124function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previous = true, $taxonomy = 'category' ) {
    1125         global $wpdb;
    1126 
    1127         if ( ( ! $post = get_post() ) || ! taxonomy_exists( $taxonomy ) )
    1128                 return null;
    1129 
    1130         $current_post_date = $post->post_date;
    1131 
    1132         $join = '';
    1133         $posts_in_ex_terms_sql = '';
    1134         if ( $in_same_term || ! empty( $excluded_terms ) ) {
    1135                 $join = " INNER JOIN $wpdb->term_relationships AS tr ON p.ID = tr.object_id INNER JOIN $wpdb->term_taxonomy tt ON tr.term_taxonomy_id = tt.term_taxonomy_id";
    1136 
    1137                 if ( $in_same_term ) {
    1138                         if ( ! is_object_in_taxonomy( $post->post_type, $taxonomy ) )
    1139                                 return '';
    1140                         $term_array = wp_get_object_terms( $post->ID, $taxonomy, array( 'fields' => 'ids' ) );
    1141                         if ( ! $term_array || is_wp_error( $term_array ) )
    1142                                 return '';
    1143                         $join .= $wpdb->prepare( " AND tt.taxonomy = %s AND tt.term_id IN (" . implode( ',', array_map( 'intval', $term_array ) ) . ")", $taxonomy );
     1125        if ( is_string( $excluded_terms ) && false !== strpos( $excluded_terms, ' and ' ) ) {
     1126                // back-compat: $excluded_terms used to be IDs separated by " and "
     1127                _deprecated_argument( __FUNCTION__, '3.3', sprintf( __( 'Use commas instead of %s to separate excluded terms.' ), "'and'" ) );
     1128                $excluded_terms = explode( ' and ', $excluded_terms );
     1129        }
     1130        if ( $excluded_terms ) {
     1131                $excluded_terms = wp_parse_id_list( $excluded_terms );
     1132        } else {
     1133                $excluded_terms = array();
     1134        }
     1135
     1136        $adjacent = new WP_Get_Adjacent_Post( array(
     1137                'post'           => get_post(),
     1138                'previous'       => $previous,
     1139                'taxonomy'       => $taxonomy,
     1140                'in_same_term'   => $in_same_term,
     1141                'excluded_terms' => $excluded_terms,
     1142        ) );
     1143
     1144        return $adjacent->adjacent_post;
     1145}
     1146
     1147/**
     1148 * WordPress Adjacent Post API
     1149 *
     1150 * Based on the current or specified post, determines either the previous or
     1151 * next post based on the criteria specified. Supports retrieving posts with the
     1152 * same taxonomy terms and posts that lack specific terms.
     1153 */
     1154class WP_Get_Adjacent_Post {
     1155        public $adjacent_post = null;
     1156
     1157        protected $current_post   = false;
     1158        protected $adjacent       = 'previous';
     1159        protected $taxonomy       = 'category';
     1160        protected $in_same_term   = false;
     1161        protected $excluded_terms = '';
     1162
     1163        /**
     1164         * Class constructor.
     1165         *
     1166         * The post is queried is run if arguments are passed to the constructor.
     1167         * Otherwise, the get_post() method will need to be called.
     1168         *
     1169         * @param array $args Optional. See the get_post() method for $args.
     1170         */
     1171        public function __construct( $args = array() ) {
     1172                if ( empty( $args ) ) {
     1173                        return;
    11441174                }
    11451175
    1146                 $posts_in_ex_terms_sql = $wpdb->prepare( "AND tt.taxonomy = %s", $taxonomy );
    1147                 if ( ! empty( $excluded_terms ) ) {
    1148                         if ( ! is_array( $excluded_terms ) ) {
    1149                                 // back-compat, $excluded_terms used to be $excluded_terms with IDs separated by " and "
    1150                                 if ( false !== strpos( $excluded_terms, ' and ' ) ) {
    1151                                         _deprecated_argument( __FUNCTION__, '3.3', sprintf( __( 'Use commas instead of %s to separate excluded terms.' ), "'and'" ) );
    1152                                         $excluded_terms = explode( ' and ', $excluded_terms );
    1153                                 } else {
    1154                                         $excluded_terms = explode( ',', $excluded_terms );
    1155                                 }
    1156                         }
    1157 
    1158                         $excluded_terms = array_map( 'intval', $excluded_terms );
    1159 
    1160                         if ( ! empty( $term_array ) ) {
    1161                                 $excluded_terms = array_diff( $excluded_terms, $term_array );
    1162                                 $posts_in_ex_terms_sql = '';
    1163                         }
    1164 
    1165                         if ( ! empty( $excluded_terms ) ) {
    1166                                 $posts_in_ex_terms_sql = $wpdb->prepare( " AND tt.taxonomy = %s AND tt.term_id NOT IN (" . implode( $excluded_terms, ',' ) . ')', $taxonomy );
     1176                $this->get_post( $args );
     1177        }
     1178
     1179        /**
     1180         * Allow direct access to adjacent post from the class instance itself
     1181         *
     1182         * @param string $property
     1183         * @return mixed String when adjacent post is found and post property exists. Null when no adjacent post is found.
     1184         */
     1185        public function __get( $property ) {
     1186                if ( is_object( $this->adjacent_post ) && property_exists( $this->adjacent_post, $property ) ) {
     1187                        return $this->adjacent_post->{$property};
     1188                } else {
     1189                        return null;
     1190                }
     1191        }
     1192
     1193        /**
     1194         * Determine adjacent post for specified post and adjacency.
     1195         *
     1196         * @since 3.9.0
     1197         *
     1198         * @param array $args {
     1199         *     Arguments for querying the adjacent post.
     1200         *
     1201         *     @type mixed  $post           Optional. Post object or ID to find adjacent post for.
     1202         *     @type bool   $previous       Optional. Whether to retrieve previous post.
     1203         *     @type string $taxonomy       Optional. Taxonomy, if $in_same_term is true. Default 'category'.
     1204         *     @type bool   $in_same_term   Optional. Whether post should be in a same taxonomy term.
     1205         *     @type array  $excluded_terms Optional. Array of excluded term IDs.
     1206         * }
     1207         * @return mixed Post object on success. False if no adjacent post exists. Null on failure.
     1208         */
     1209        protected function get_post( $args ) {
     1210                $this->current_post = get_post( $args['post'] );
     1211                $this->excluded_terms = array_map( 'intval', $args['excluded_terms'] );
     1212                $this->adjacent       = $args['previous'] ? 'previous' : 'next';
     1213                $this->in_same_term   = (bool) $args['in_same_term'];
     1214
     1215                // Return null when either the post or taxonomy doesn't exist.
     1216                if ( ! $this->current_post ) {
     1217                        return;
     1218                }
     1219                if ( $this->in_same_term || $this->excluded_terms ) {
     1220                        if ( ! taxonomy_exists( $args['taxonomy'] ) ) {
     1221                                return;
    11671222                        }
    11681223                }
    1169         }
    1170 
    1171         $adjacent = $previous ? 'previous' : 'next';
    1172         $op = $previous ? '<' : '>';
    1173         $order = $previous ? 'DESC' : 'ASC';
    1174 
    1175         $join  = apply_filters( "get_{$adjacent}_post_join", $join, $in_same_term, $excluded_terms );
    1176         $where = apply_filters( "get_{$adjacent}_post_where", $wpdb->prepare( "WHERE p.post_date $op %s AND p.post_type = %s AND p.post_status = 'publish' $posts_in_ex_terms_sql", $current_post_date, $post->post_type), $in_same_term, $excluded_terms );
    1177         $sort  = apply_filters( "get_{$adjacent}_post_sort", "ORDER BY p.post_date $order LIMIT 1" );
    1178 
    1179         $query = "SELECT p.ID FROM $wpdb->posts AS p $join $where $sort";
    1180         $query_key = 'adjacent_post_' . md5( $query );
    1181         $result = wp_cache_get( $query_key, 'counts' );
    1182         if ( false !== $result ) {
    1183                 if ( $result )
    1184                         $result = get_post( $result );
    1185                 return $result;
    1186         }
    1187 
    1188         $result = $wpdb->get_var( $query );
    1189         if ( null === $result )
    1190                 $result = '';
    1191 
    1192         wp_cache_set( $query_key, $result, 'counts' );
    1193 
    1194         if ( $result )
    1195                 $result = get_post( $result );
    1196 
    1197         return $result;
     1224
     1225                // Build our arguments for WP_Query.
     1226                $query_args = array(
     1227                        'posts_per_page'   => 1,
     1228                        'post_status'      => 'publish',
     1229                        'post_type'        => 'post',
     1230                        'orderby'          => 'date',
     1231                        'order'            => 'previous' === $this->adjacent ? 'DESC' : 'ASC',
     1232                        'no_found_rows'    => true,
     1233                        'cache_results'    => true,
     1234                        'date_query'       => array(),
     1235                );
     1236
     1237                $tax_query = array();
     1238
     1239                // Set up for requests limited to posts that share terms.
     1240                if ( $this->in_same_term ) {
     1241                        $terms = get_the_terms( $this->current_post->ID, $args['taxonomy'] );
     1242
     1243                        if ( is_array( $terms ) && ! empty( $terms ) ) {
     1244                                $terms = wp_list_pluck( $terms, 'term_id' );
     1245                                $terms = array_values( $terms );
     1246                                $terms = array_map( 'intval', $terms );
     1247                        } else {
     1248                                unset( $terms );
     1249                        }
     1250                }
     1251
     1252                // Handle excluded terms.
     1253                if ( $this->excluded_terms ) {
     1254                        $tax_query[] = array(
     1255                                'taxonomy' => $args['taxonomy'],
     1256                                'slugs'    => $this->excluded_terms,
     1257                                'compare'  => 'NOT IN',
     1258                        );
     1259                }
     1260
     1261                // If requesting same term, ensure excluded terms don't appear in term list.
     1262                if ( isset( $terms ) ) {
     1263                        if ( isset( $this->excluded_terms ) && is_array( $this->excluded_terms ) ) {
     1264                                $terms = array_diff( $terms, $this->excluded_terms );
     1265                        }
     1266
     1267                        if ( ! empty( $terms ) ) {
     1268                                $tax_query[] = array(
     1269                                        'taxonomy' => $args['taxonomy'],
     1270                                        'terms'    => $terms,
     1271                                );
     1272                        }
     1273                }
     1274
     1275                // If we have a tax query, add it to our query args.
     1276                if ( $tax_query ) {
     1277                        $query_args['tax_query'] = $tax_query;
     1278                }
     1279
     1280                // And now, the date constraint.
     1281                $date_query_key = 'previous' === $this->adjacent ? 'before' : 'after';
     1282
     1283                $query_args['date_query'][] = array(
     1284                        $date_query_key => $this->current_post->post_date,
     1285                        'inclusive'     => true,
     1286                );
     1287
     1288                // Ensure the current post isn't returned, since we're using an inclusive date query.
     1289                $query_args['post__not_in'] = array( $this->current_post->ID );
     1290
     1291                /**
     1292                 * Filter the arguments passed to WP_Query when finding an adjacent post.
     1293                 *
     1294                 * @since 3.9.0
     1295                 *
     1296                 * @param array $query_args WP_Query arguments.
     1297                 * @param array $args       Arguments passed to WP_Get_Adjacent_Post.
     1298                 */
     1299                $query_args = apply_filters( 'get_adjacent_post_query_args', $query_args, $args );
     1300
     1301                add_filter( 'posts_clauses', array( $this, 'filter' ) );
     1302                $query = new WP_Query( $query_args );
     1303
     1304                if ( $query->posts ) {
     1305                        $this->adjacent_post = $query->post;
     1306                } else {
     1307                        $this->adjacent_post = false;
     1308                }
     1309        }
     1310
     1311        /**
     1312         * Apply the deprecated filters to WP_Query's clauses.
     1313         *
     1314         * @param array $clauses
     1315         * @uses $this->filter_join_and_where()
     1316         * @uses $this->filter_sort()
     1317         * @filter post_clauses
     1318         * @return array
     1319         */
     1320        public function filter( $clauses ) {
     1321                // Immediately deregister these legacy filters to avoid modifying
     1322                // any calls to WP_Query from filter callbacks hooked to WP_Query filters.
     1323                remove_filter( 'posts_clauses', array( $this, 'filter' ) );
     1324
     1325                // The `join` and `where` filters are identical in their parameters,
     1326                // so we can use the same approach for both.
     1327                foreach ( array( 'join', 'where' ) as $clause ) {
     1328                        if ( has_filter( 'get_' . $this->adjacent . '_post_' . $clause ) ) {
     1329                                $clauses[ $clause ] = $this->filter_join_and_where( $clauses[ $clause ], $clause );
     1330                        }
     1331                }
     1332
     1333                // The legacy `sort` filter combined the ORDER BY and LIMIT clauses,
     1334                // while `WP_Query` does not, which requires special handling.
     1335                if ( has_filter( 'get_' . $this->adjacent . '_post_sort' ) ) {
     1336                        $sort_clauses = $this->filter_sort( $clauses['orderby'], $clauses['limits'] );
     1337                        $clauses      = array_merge( $clauses, $sort_clauses );
     1338                }
     1339
     1340                return $clauses;
     1341        }
     1342
     1343        /**
     1344         * Apply the deprecated `join` or `where` clause filter to the clauses built by WP_Query.
     1345         *
     1346         * @param string $value
     1347         * @param string $clause
     1348         * @return string
     1349         */
     1350        protected function filter_join_and_where( $value, $clause ) {
     1351                /**
     1352                 * @deprecated 3.9.0
     1353                 */
     1354                return apply_filters( 'get_' . $this->adjacent . '_post_' . $clause, $value, $this->in_same_term, $this->excluded_terms );
     1355        }
     1356
     1357        /**
     1358         * Apply deprecated `sort` filter, which applies to both the ORDER BY and LIMIT clauses.
     1359         *
     1360         * @param string $orderby
     1361         * @param string $limits
     1362         * @return array
     1363         */
     1364        protected function filter_sort( $orderby, $limits ) {
     1365                /**
     1366                 * @deprecated 3.9.0
     1367                 */
     1368                $sort = apply_filters( 'get_' . $this->adjacent . '_post_sort', 'ORDER BY ' . $orderby . ' ' . $limits );
     1369
     1370                if ( empty( $sort ) ) {
     1371                        return compact( 'orderby', 'limits' );
     1372                }
     1373
     1374                // The legacy filter could allow either clause to be removed, or their order inverted, so we need to know what we have and where.
     1375                $has_order_by = stripos( $sort, 'order by' );
     1376                $has_limit    = stripos( $sort, 'limit' );
     1377
     1378                // Split the string of one or two clauses into their respective array keys
     1379                if ( false !== $has_order_by && false !== $has_limit ) {
     1380                        // The LIMIT clause cannot appear before the ORDER BY clause in a valid query
     1381                        // However, since the legacy filter would allow a user to invert the order, we maintain that handling so the same errors are triggered.
     1382                        if ( $has_order_by < $has_limit ) {
     1383                                $orderby = trim( str_ireplace( 'order by', '', substr( $sort, 0, $has_limit ) ) );
     1384                                $limits  = trim( substr( $sort, $has_limit ) );
     1385                        } else {
     1386                                $orderby = trim( str_ireplace( 'order by', '', substr( $sort, $has_order_by ) ) );
     1387                                $limits  = trim( substr( $sort, 0, $has_order_by ) );
     1388                        }
     1389                } elseif ( false !== $has_order_by ) {
     1390                        $orderby = trim( str_ireplace( 'order by', '', $sort ) );
     1391                        $limits  = '';
     1392                } elseif ( false !== $has_limit ) {
     1393                        $orderby = '';
     1394                        $limits  = trim( $sort );
     1395                }
     1396
     1397                return compact( 'orderby', 'limits' );
     1398        }
    11981399}
    11991400
  • trunk/tests/phpunit/tests/link.php

    r25959 r27285  
    168168                $this->assertEquals( array( $post_four ), get_boundary_post( true, '', false, 'post_tag' ) );
    169169        }
     170
     171        /**
     172         * @ticket 26937
     173         */
     174        function test_legacy_get_adjacent_post_filters() {
     175                // Need some sample posts to test adjacency
     176                $post_one = $this->factory->post->create_and_get( array(
     177                        'post_title' => 'First',
     178                        'post_date' => '2012-01-01 12:00:00'
     179                ) );
     180
     181                $post_two = $this->factory->post->create_and_get( array(
     182                        'post_title' => 'Second',
     183                        'post_date' => '2012-02-01 12:00:00'
     184                ) );
     185
     186                $post_three = $this->factory->post->create_and_get( array(
     187                        'post_title' => 'Third',
     188                        'post_date' => '2012-03-01 12:00:00'
     189                ) );
     190
     191                $post_four = $this->factory->post->create_and_get( array(
     192                        'post_title' => 'Fourth',
     193                        'post_date' => '2012-04-01 12:00:00'
     194                ) );
     195
     196                // Add some meta so we can join the postmeta table and query
     197                add_post_meta( $post_three->ID, 'unit_test_meta', 'waffle' );
     198
     199                // Test "where" filter for a previous post
     200                add_filter( 'get_previous_post_where', array( $this, 'filter_previous_post_where' ) );
     201                $this->go_to( get_permalink( $post_three->ID ) );
     202                $this->assertEquals( $post_one, get_adjacent_post( false, null, true ) );
     203                remove_filter( 'get_previous_post_where', array( $this, 'filter_previous_post_where' ) );
     204
     205                // Test "where" filter for a next post
     206                add_filter( 'get_next_post_where', array( $this, 'filter_next_post_where' ) );
     207                $this->go_to( get_permalink( $post_two->ID ) );
     208                $this->assertEquals( $post_four, get_adjacent_post( false, null, false ) );
     209                remove_filter( 'get_next_post_where', array( $this, 'filter_next_post_where' ) );
     210
     211                // Test "join" filter by joining the postmeta table and restricting by meta key
     212                add_filter( 'get_next_post_join', array( $this, 'filter_next_post_join' ) );
     213                add_filter( 'get_next_post_where', array( $this, 'filter_next_post_where_with_join' ) );
     214                $this->go_to( get_permalink( $post_one->ID ) );
     215                $this->assertEquals( $post_three, get_adjacent_post( false, null, false ) );
     216                remove_filter( 'get_next_post_join', array( $this, 'filter_next_post_join' ) );
     217                remove_filter( 'get_next_post_where', array( $this, 'filter_next_post_where_with_join' ) );
     218
     219                // Test "sort" filter when modifying ORDER BY clause
     220                add_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort' ) );
     221                $this->go_to( get_permalink( $post_one->ID ) );
     222                $this->assertEquals( $post_four, get_adjacent_post( false, null, false ) );
     223                remove_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort' ) );
     224
     225                // Test "sort" filter when modifying LIMIT clause
     226                add_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort_limit' ) );
     227                $this->go_to( get_permalink( $post_one->ID ) );
     228                $this->assertEquals( $post_three, get_adjacent_post( false, null, false ) );
     229                remove_filter( 'get_next_post_sort', array( $this, 'filter_next_post_sort_limit' ) );
     230        }
     231
     232        /**
     233         * Filter callback for `test_legacy_get_adjacent_post_filters()`
     234         */
     235        function filter_previous_post_where( $where ) {
     236                $where .= " AND post_title !='Second'";
     237                return $where;
     238        }
     239
     240        /**
     241         * Filter callback for `test_legacy_get_adjacent_post_filters()`
     242         */
     243        function filter_next_post_where( $where ) {
     244                $where .= " AND post_title !='Third'";
     245                return $where;
     246        }
     247
     248        /**
     249         * Filter callback for `test_legacy_get_adjacent_post_filters()`
     250         */
     251        function filter_next_post_join( $join ) {
     252                global $wpdb;
     253
     254                $join .= " INNER JOIN {$wpdb->postmeta} ON {$wpdb->posts}.ID = {$wpdb->postmeta}.post_id";
     255                return $join;
     256        }
     257
     258        /**
     259         * Filter callback for `test_legacy_get_adjacent_post_filters()`
     260         */
     261        function filter_next_post_where_with_join( $where ) {
     262                global $wpdb;
     263
     264                $where .= " AND {$wpdb->postmeta}.meta_key = 'unit_test_meta'";
     265                return $where;
     266        }
     267
     268        /**
     269         * Filter callback for `test_legacy_get_adjacent_post_filters()`
     270         */
     271        function filter_next_post_sort( $sort ) {
     272                global $wpdb;
     273
     274                $sort = str_replace( $wpdb->posts . '.post_date', $wpdb->posts . '.post_title', $sort );
     275                return $sort;
     276        }
     277
     278        /**
     279         * Filter callback for `test_legacy_get_adjacent_post_filters()`
     280         */
     281        function filter_next_post_sort_limit( $sort ) {
     282                $sort = str_replace( 'LIMIT 0, 1', 'LIMIT 1, 2', $sort );
     283                return $sort;
     284        }
    170285}
Note: See TracChangeset for help on using the changeset viewer.