From 19f0be50939461108db0a725f4b94282afd9f21b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 11 Jul 2026 10:46:44 -0700 Subject: [PATCH] Posts, Post Types: Improve static analysis types for post retrieval functions. Narrow the conditional `@phpstan-return` types of `get_post()`, `get_page()`, `get_page_by_path()`, `get_children()`, `wp_get_recent_posts()`, and `wp_get_post_revision()` so that the `ARRAY_A` and `ARRAY_N` outputs are typed as `non-empty-array`, consistent with `WP_Post::to_array()`, which always returns at least the object's properties. Prior to this, `wp_get_post_revision()` had no conditional return type at all, so its return value was seen as `WP_Post|array|null` regardless of the requested `$output`. Describing it precisely resolves a number of pre-existing `Cannot access property $ID on array|WP_Post` errors in its callers, including `wp_restore_post_revision()`, `wp_delete_post_revision()`, `wp_xmlrpc_server::wp_restoreRevision()`, and `wp-admin/revision.php`. Additionally: * Guard against `get_post()` returning `null` in `trackback_url_list()`, which would otherwise emit "Trying to access array offset on value of type null" warnings when called with an invalid post ID. * Collect the array output of `wp_get_recent_posts()` in a separate `$posts` variable rather than overwriting the `WP_Post` objects in `$results` in place. Reassigning array values over the elements of `$results` left the variable holding a mix of `WP_Post` objects and arrays partway through the loop, so static analysis could only infer `array>` for it. Building up a dedicated variable guarantees that everything it contains is an array, allowing the `ARRAY_A` branch of the return type to be expressed precisely. This does not change the returned value. Note that `non-empty-array` is a PHPStan-specific type which phpDocumentor does not understand, so the plain `@return` tag for `WP_Post::to_array()` continues to use `array` and the narrower type is supplied via `@phpstan-return`. Co-Authored-By: Claude Opus 4.8 --- src/wp-includes/class-wp-post.php | 4 +++- src/wp-includes/post.php | 39 +++++++++++++++++++------------ src/wp-includes/revision.php | 16 ++++++++++++- 3 files changed, 42 insertions(+), 17 deletions(-) diff --git a/src/wp-includes/class-wp-post.php b/src/wp-includes/class-wp-post.php index d420c93fdce94..e1ed87885b41e 100644 --- a/src/wp-includes/class-wp-post.php +++ b/src/wp-includes/class-wp-post.php @@ -388,9 +388,11 @@ public function filter( $filter ) { * @since 3.5.0 * * @return array Object as array. + * + * @phpstan-return non-empty-array */ public function to_array() { - /** @var array $post */ + /** @var non-empty-array $post */ $post = get_object_vars( $this ); foreach ( array( 'ancestors', 'page_template', 'post_category', 'tags_input' ) as $key ) { diff --git a/src/wp-includes/post.php b/src/wp-includes/post.php index a1d887b45381f..e26a3a6276e54 100644 --- a/src/wp-includes/post.php +++ b/src/wp-includes/post.php @@ -990,8 +990,8 @@ function _wp_relative_upload_path( $path ) { * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output * @phpstan-return ( * $args is array{ fields: 'ids', ... } ? int[] : ( - * $output is 'ARRAY_A' ? array> : ( - * $output is 'ARRAY_N' ? array> : WP_Post[] + * $output is 'ARRAY_A' ? array> : ( + * $output is 'ARRAY_N' ? array> : WP_Post[] * ) * ) * ) @@ -1040,13 +1040,17 @@ function get_children( $args = '', $output = OBJECT ) { } elseif ( ARRAY_A === $output ) { $weeuns = array(); foreach ( (array) $kids as $kid ) { - $weeuns[ $kid->ID ] = get_object_vars( $kids[ $kid->ID ] ); + /** @var non-empty-array $vars */ + $vars = get_object_vars( $kids[ $kid->ID ] ); + $weeuns[ $kid->ID ] = $vars; } return $weeuns; } elseif ( ARRAY_N === $output ) { $babes = array(); foreach ( (array) $kids as $kid ) { - $babes[ $kid->ID ] = array_values( get_object_vars( $kids[ $kid->ID ] ) ); + /** @var non-empty-array $vars */ + $vars = get_object_vars( $kids[ $kid->ID ] ); + $babes[ $kid->ID ] = array_values( $vars ); } return $babes; } else { @@ -1124,8 +1128,8 @@ function get_extended( $post ) { * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output * @phpstan-param 'raw'|'edit'|'db'|'display' $filter * @phpstan-return ( - * $output is 'ARRAY_A' ? array|null : ( - * $output is 'ARRAY_N' ? array|null : ( + * $output is 'ARRAY_A' ? non-empty-array|null : ( + * $output is 'ARRAY_N' ? non-empty-array|null : ( * WP_Post|null * ) * ) @@ -4452,7 +4456,7 @@ function wp_get_post_terms( $post_id = 0, $taxonomy = 'post_tag', $args = array( * * @phpstan-param 'OBJECT'|'ARRAY_A' $output * @phpstan-return ( - * $output is 'ARRAY_A' ? array> : WP_Post[]|false + * $output is 'ARRAY_A' ? array> : WP_Post[]|false * ) */ function wp_get_recent_posts( $args = array(), $output = ARRAY_A ) { @@ -4485,12 +4489,13 @@ function wp_get_recent_posts( $args = array(), $output = ARRAY_A ) { // Backward compatibility. Prior to 3.1 expected posts to be returned in array. if ( ARRAY_A === $output ) { + $posts = array(); foreach ( $results as $key => $result ) { - /** @var array $object_vars */ - $object_vars = get_object_vars( $result ); - $results[ $key ] = $object_vars; + /** @var non-empty-array $object_vars */ + $object_vars = get_object_vars( $result ); + $posts[ $key ] = $object_vars; } - return $results ? $results : array(); + return $posts; } return $results ? $results : false; @@ -6146,6 +6151,10 @@ function trackback_url_list( $tb_list, $post_id ) { // Get post data. $postdata = get_post( $post_id, ARRAY_A ); + if ( ! $postdata ) { + return; + } + // Form an excerpt. $excerpt = strip_tags( $postdata['post_excerpt'] ? $postdata['post_excerpt'] : $postdata['post_content'] ); @@ -6206,8 +6215,8 @@ function get_all_page_ids() { * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output * @phpstan-param 'raw'|'edit'|'db'|'display' $filter * @phpstan-return ( - * $output is 'ARRAY_A' ? array|null : ( - * $output is 'ARRAY_N' ? array|null : ( + * $output is 'ARRAY_A' ? non-empty-array|null : ( + * $output is 'ARRAY_N' ? non-empty-array|null : ( * WP_Post|null * ) * ) @@ -6234,8 +6243,8 @@ function get_page( $page, $output = OBJECT, $filter = 'raw' ) { * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output * @phpstan-param string|string[] $post_type * @phpstan-return ( - * $output is 'ARRAY_A' ? array|null : ( - * $output is 'ARRAY_N' ? array|null : ( + * $output is 'ARRAY_A' ? non-empty-array|null : ( + * $output is 'ARRAY_N' ? non-empty-array|null : ( * WP_Post|null * ) * ) diff --git a/src/wp-includes/revision.php b/src/wp-includes/revision.php index 11f2bba9d97ff..6e27fad4fa0a4 100644 --- a/src/wp-includes/revision.php +++ b/src/wp-includes/revision.php @@ -423,6 +423,17 @@ function wp_save_revisioned_meta_fields( $revision_id, $post_id ) { * respectively. Default OBJECT. * @param string $filter Optional sanitization filter. See sanitize_post(). Default 'raw'. * @return WP_Post|array|null WP_Post (or array) on success, or null on failure. + * + * @phpstan-param int|WP_Post $post + * @phpstan-param 'OBJECT'|'ARRAY_A'|'ARRAY_N' $output + * @phpstan-param 'raw'|'edit'|'db'|'display' $filter + * @phpstan-return ( + * $output is 'ARRAY_A' ? non-empty-array|null : ( + * $output is 'ARRAY_N' ? non-empty-array|null : ( + * WP_Post|null + * ) + * ) + * ) */ function wp_get_post_revision( &$post, $output = OBJECT, $filter = 'raw' ) { $revision = get_post( $post, OBJECT, $filter ); @@ -438,10 +449,13 @@ function wp_get_post_revision( &$post, $output = OBJECT, $filter = 'raw' ) { if ( OBJECT === $output ) { return $revision; } elseif ( ARRAY_A === $output ) { + /** @var non-empty-array $_revision */ $_revision = get_object_vars( $revision ); return $_revision; } elseif ( ARRAY_N === $output ) { - $_revision = array_values( get_object_vars( $revision ) ); + /** @var non-empty-array $vars */ + $vars = get_object_vars( $revision ); + $_revision = array_values( $vars ); return $_revision; }