Make WordPress Core

Changeset 63358


Ignore:
Timestamp:
08/27/2026 09:28:56 AM (7 weeks ago)
Author:
swissspidy
Message:

Code Quality: Narrow post and term query return types by fields.

get_posts(), WP_Query::query() and WP_Term_Query::query() take the query arguments directly, so static analysis can resolve their result from the fields value. Only 'ids' was mapped; 'id=>parent' and every WP_Term_Query field are now covered too.

Two divergences in the cached WP_Query path had to be fixed for the 'id=>parent' type to hold: a query served from the post-queries cache returned parent IDs keyed by their post_parent:{$post_id} cache keys rather than by post ID, and left WP_Query::$posts null when the cached query matched nothing.

Developed in: ​https://github.com/WordPress/wordpress-develop/pull/13216

Props westonruter, swissspidy.
See #65817.

Location:
trunk
Files:
6 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/class-wp-query.php

    r63357 r63358  
    32963296                                                }
    32973297
    3298                                                 /** @var int[] */
    3299                                                 $post_parents = wp_cache_get_multiple( $post_parent_cache_keys, 'posts' );
    3300 
    3301                                                 foreach ( $post_parents as $cache_key => $post_parent ) {
     3298                                                /** @var array<non-falsy-string, int|false> $cached_post_parents */
     3299                                                $cached_post_parents = wp_cache_get_multiple( $post_parent_cache_keys, 'posts' );
     3300
     3301                                                $post_parents = array();
     3302                                                $this->posts  = array();
     3303
     3304                                                foreach ( $cached_post_parents as $cache_key => $post_parent ) {
    33023305                                                        $obj              = new stdClass();
    33033306                                                        $obj->ID          = (int) str_replace( 'post_parent:', '', $cache_key );
    … …  
    33053308
    33063309                                                        $this->posts[] = $obj;
     3310
     3311                                                        $post_parents[ $obj->ID ] = $obj->post_parent;
    33073312                                                }
    33083313
    … …  
    39693974         *
    39703975         * @phpstan-return (
    3971          *     $query is array{ fields: 'ids', ... } ? int[] : WP_Post[]
     3976         *     $query is array{ fields: 'ids', ... }
     3977         *         ? int[]
     3978         *         : ( $query is array{ fields: 'id=>parent', ... }
     3979         *             ? array<int, int>
     3980         *             : WP_Post[] )
    39723981         * )
    39733982         */
  • trunk/src/wp-includes/class-wp-term-query.php

    r63320 r63358  
    301301         *
    302302         * @param string|array $query Array or URL query string of parameters.
    303          * @return WP_Term[]|int[]|string[]|string Array of terms, or number of terms as numeric string
    304          *                                         when 'count' is passed to `$args['fields']`.
     303         * @return WP_Term[]|int[]|string[]|int|string Array of terms, or number of terms as numeric string
     304         *                                             when 'count' is passed to `$args['fields']`, or the
     305         *                                             integer 0 when the queried parent term is not in the
     306         *                                             taxonomy hierarchy.
     307         *
     308         * @phpstan-return (
     309         *     $query is array{ fields: 'count', ... }
     310         *         ? 0|numeric-string
     311         *         : ( $query is array{ fields: 'ids'|'tt_ids', ... }
     312         *             ? int[]
     313         *             : ( $query is array{ fields: 'id=>parent', ... }
     314         *                 ? array<int, int>
     315         *                 : ( $query is array{ fields: 'names'|'slugs', ... }
     316         *                     ? string[]
     317         *                     : ( $query is array{ fields: 'id=>name'|'id=>slug', ... }
     318         *                         ? array<int, string>
     319         *                         : WP_Term[] ) ) ) )
     320         * )
    305321         */
    306322        public function query( $query ) {
    … …  
    319335         *   - 'all_with_object_id'
    320336         *
    321          * The following will result in a numeric string being returned:
     337         * The following will result in a numeric string being returned, or the integer 0
     338         * when the queried parent term is not in the taxonomy hierarchy:
    322339         *
    323340         *   - 'count'
    … …  
    330347         *   - 'slugs'
    331348         *
    332          * The following will result in an array of numeric strings being returned:
     349         * The following will result in an array of integers being returned:
    333350         *
    334351         *   - 'id=>parent'
    335          *
    336          * The following will result in an array of integers being returned:
    337          *
    338352         *   - 'ids'
    339353         *   - 'tt_ids'
    … …  
    343357         * @global wpdb $wpdb WordPress database abstraction object.
    344358         *
    345          * @return WP_Term[]|int[]|string[]|string Array of terms, or number of terms as numeric string
    346          *                                         when 'count' is passed to `$args['fields']`.
     359         * @return WP_Term[]|int[]|string[]|int|string Array of terms, or number of terms as numeric string
     360         *                                             when 'count' is passed to `$args['fields']`, or the
     361         *                                             integer 0 when the queried parent term is not in the
     362         *                                             taxonomy hierarchy.
     363         *
     364         * @phpstan-return 0|numeric-string|int[]|array<int, int>|string[]|array<int, string>|WP_Term[]
    347365         */
    348366        public function get_terms() {
    … …  
    803821
    804822                if ( 'count' === $_fields ) {
     823                        // The request selects a single COUNT column, so it only returns null on a database error.
     824                        /** @var numeric-string $count */
    805825                        $count = $wpdb->get_var( $this->request ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
    806826                        if ( $args['cache_results'] ) {
  • trunk/src/wp-includes/post.php

    r63002 r63358  
    26202620 *
    26212621 * @phpstan-return (
    2622  *     $args is array{ fields: 'ids', ... } ? int[] : WP_Post[]
     2622 *     $args is array{ fields: 'ids', ... }
     2623 *         ? int[]
     2624 *         : ( $args is array{ fields: 'id=>parent', ... }
     2625 *             ? array<int, int>
     2626 *             : WP_Post[] )
    26232627 * )
    26242628 */
  • trunk/tests/phpunit/tests/post/getPosts.php

    r62041 r63358  
    177177                $this->assertSame( array( $second_post_id ), $found_post_ids );
    178178        }
     179
     180        /**
     181         * Verifies what get_posts() returns for the `id=>parent` fields value.
     182         *
     183         * The second call is served from the `post-queries` cache, which previously returned
     184         * the parent IDs keyed by the `post_parent:{$post_id}` cache keys they are stored
     185         * under rather than by post ID.
     186         *
     187         * The cache hit is not specific to an external object cache being in use: the second
     188         * call is served from the cache with the default in-memory implementation as well.
     189         *
     190         * @ticket 65817
     191         */
     192        public function test_should_return_parent_ids_keyed_by_post_id_when_cached(): void {
     193                $parent_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
     194                $this->assertIsInt( $parent_id, 'The parent page was not created.' );
     195
     196                $child_id = self::factory()->post->create(
     197                        array(
     198                                'post_type'   => 'page',
     199                                'post_parent' => $parent_id,
     200                        )
     201                );
     202                $this->assertIsInt( $child_id, 'The child page was not created.' );
     203
     204                $args = array(
     205                        'post_type'   => 'page',
     206                        'fields'      => 'id=>parent',
     207                        'numberposts' => -1,
     208                        'orderby'     => 'ID',
     209                        'order'       => 'ASC',
     210                );
     211
     212                $expected = array(
     213                        $parent_id => 0,
     214                        $child_id  => $parent_id,
     215                );
     216
     217                $this->assertSame( $expected, get_posts( $args ), 'The uncached call is not of the expected form.' );
     218                $this->assertSame( $expected, get_posts( $args ), 'The cached call is not of the expected form.' );
     219        }
    179220}
  • trunk/tests/phpunit/tests/query/fieldsClause.php

    r57653 r63358  
    213213
    214214        /**
     215         * Tests the value returned when the fields are limited to the ID and parent sub-set.
     216         *
     217         * The second query is served from the `post-queries` cache, which previously returned
     218         * the parent IDs keyed by the `post_parent:{$post_id}` cache keys they are stored
     219         * under rather than by post ID.
     220         *
     221         * The cache hit is not specific to an external object cache being in use: the second
     222         * query is served from the cache with the default in-memory implementation as well.
     223         *
     224         * @ticket 65817
     225         *
     226         * @covers WP_Query::query
     227         */
     228        public function test_id_and_parent_subset_should_return_parent_ids_keyed_by_post_id_when_cached() {
     229                $parent_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
     230                $this->assertIsInt( $parent_id, 'The parent page was not created.' );
     231
     232                $child_id = self::factory()->post->create(
     233                        array(
     234                                'post_type'   => 'page',
     235                                'post_parent' => $parent_id,
     236                        )
     237                );
     238                $this->assertIsInt( $child_id, 'The child page was not created.' );
     239
     240                $query_args = array(
     241                        'post_type'      => 'page',
     242                        'fields'         => 'id=>parent',
     243                        'posts_per_page' => -1,
     244                        'orderby'        => 'ID',
     245                        'order'          => 'ASC',
     246                );
     247
     248                $expected = array(
     249                        $parent_id => 0,
     250                        $child_id  => $parent_id,
     251                );
     252
     253                $q1 = new WP_Query();
     254                $this->assertSame( $expected, $q1->query( $query_args ), 'The uncached query is not of the expected form.' );
     255
     256                $q2 = new WP_Query();
     257                $this->assertSame( $expected, $q2->query( $query_args ), 'The cached query is not of the expected form.' );
     258        }
     259
     260        /**
     261         * Tests the value returned when the fields are limited to the ID and parent sub-set
     262         * and the query results are not cached.
     263         *
     264         * With caching disabled every query runs against the database, so the `post-queries`
     265         * cache is never consulted.
     266         *
     267         * @ticket 65817
     268         *
     269         * @covers WP_Query::query
     270         */
     271        public function test_id_and_parent_subset_should_return_parent_ids_keyed_by_post_id_when_not_caching_results() {
     272                $parent_id = self::factory()->post->create( array( 'post_type' => 'page' ) );
     273                $this->assertIsInt( $parent_id, 'The parent page was not created.' );
     274
     275                $child_id = self::factory()->post->create(
     276                        array(
     277                                'post_type'   => 'page',
     278                                'post_parent' => $parent_id,
     279                        )
     280                );
     281                $this->assertIsInt( $child_id, 'The child page was not created.' );
     282
     283                $query_args = array(
     284                        'post_type'      => 'page',
     285                        'fields'         => 'id=>parent',
     286                        'posts_per_page' => -1,
     287                        'orderby'        => 'ID',
     288                        'order'          => 'ASC',
     289                        'cache_results'  => false,
     290                );
     291
     292                $expected = array(
     293                        $parent_id => 0,
     294                        $child_id  => $parent_id,
     295                );
     296
     297                $q1 = new WP_Query();
     298                $this->assertSame( $expected, $q1->query( $query_args ), 'First query is not of the expected form.' );
     299
     300                $q2 = new WP_Query();
     301                $this->assertSame( $expected, $q2->query( $query_args ), 'Second query is not of the expected form.' );
     302        }
     303
     304        /**
     305         * Tests the posts property when the fields are limited to the ID and parent sub-set
     306         * and the query matches nothing.
     307         *
     308         * An empty result set is cached like any other, and the cached branch only ever
     309         * appended to the posts property, so a second identical query left it as null where
     310         * the first left it an empty array.
     311         *
     312         * @ticket 65817
     313         */
     314        public function test_id_and_parent_subset_should_populate_posts_property_when_cached_query_has_no_results() {
     315                $query_args = array(
     316                        'post_type' => 'wptests_pt',
     317                        'fields'    => 'id=>parent',
     318                        'name'      => 'this-slug-does-not-exist',
     319                );
     320
     321                $q1 = new WP_Query();
     322                $this->assertSame( array(), $q1->query( $query_args ), 'The uncached query did not return an empty array.' );
     323                $this->assertSame( array(), $q1->posts, 'The posts property is not an empty array after the uncached query.' );
     324
     325                $q2 = new WP_Query();
     326                $this->assertSame( array(), $q2->query( $query_args ), 'The cached query did not return an empty array.' );
     327                $this->assertSame( array(), $q2->posts, 'The posts property is not an empty array after the cached query.' );
     328        }
     329
     330        /**
    215331         * Filters the posts fields.
    216332         *
  • trunk/tests/phpunit/tests/term/query.php

    r61048 r63358  
    12081208                $this->assertSame( ltrim( $q->request ), $q->request, 'The query has leading whitespace' );
    12091209        }
     1210
     1211        /**
     1212         * Verifies what WP_Term_Query::query() returns for the `id=>parent` fields value.
     1213         *
     1214         * Unlike WP_Query, both the uncached and the cached path run the results through
     1215         * WP_Term_Query::format_terms(), so the parent IDs are keyed by term ID either way.
     1216         *
     1217         * The assertion is repeated for a second query so that the cached path is covered
     1218         * both when an external object cache is in use and when it is not.
     1219         *
     1220         * @ticket 65817
     1221         *
     1222         * @covers WP_Term_Query::query
     1223         */
     1224        public function test_id_and_parent_fields_should_return_parent_ids_keyed_by_term_id() {
     1225                register_taxonomy( 'wptests_tax_hierarchical', 'post', array( 'hierarchical' => true ) );
     1226
     1227                $parent_id = self::factory()->term->create( array( 'taxonomy' => 'wptests_tax_hierarchical' ) );
     1228                $this->assertIsInt( $parent_id, 'The parent term was not created.' );
     1229
     1230                $child_id = self::factory()->term->create(
     1231                        array(
     1232                                'taxonomy' => 'wptests_tax_hierarchical',
     1233                                'parent'   => $parent_id,
     1234                        )
     1235                );
     1236                $this->assertIsInt( $child_id, 'The child term was not created.' );
     1237
     1238                $query_args = array(
     1239                        'taxonomy'   => 'wptests_tax_hierarchical',
     1240                        'fields'     => 'id=>parent',
     1241                        'hide_empty' => false,
     1242                        'orderby'    => 'term_id',
     1243                        'order'      => 'ASC',
     1244                );
     1245
     1246                $expected = array(
     1247                        $parent_id => 0,
     1248                        $child_id  => $parent_id,
     1249                );
     1250
     1251                $q1 = new WP_Term_Query();
     1252                $this->assertSame( $expected, $q1->query( $query_args ), 'First query is not of the expected form.' );
     1253
     1254                $q2 = new WP_Term_Query();
     1255                $this->assertSame( $expected, $q2->query( $query_args ), 'Second query is not of the expected form.' );
     1256        }
    12101257}
Note: See TracChangeset for help on using the changeset viewer.