WordPress.org

Make WordPress Core

Changeset 27285


Ignore:
Timestamp:
02/26/14 17:09:54 (4 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.