Skip to content

Commit 05375dd

Browse files
committed
Code Quality: Improve comment API type coverage.
Add a `Data_Array` array-shape type describing the keys returned by `WP_Comment::to_array()`, and narrow the properties it covers: `comment_approved` and the two datetime fields become `non-empty-string`, and the values core uses for it and for `comment_type` are documented. `comment_type` itself stays a plain `string`, because comments created before 5.5.0 may store an empty string rather than 'comment', which is why `get_comment_type()` normalizes that case on read. Declare the 21 post fields that `WP_Comment::__get()` proxies to the comment's post as `@property-read`, typed to match the corresponding `WP_Post` property. These were previously invisible to static analysis, IDE completion, and the generated documentation. Also correct `WP_Comment::$children`, previously a bare `array`, which is `null` until `get_children()` populates it, and type the comment arrays keyed by comment ID as `array<int, WP_Comment>`, since PHP coerces the numeric string `comment_ID` to an integer key on assignment. Add conditional return types to `WP_Comment::get_children()`, `get_comment()`, `get_comments()`, and `get_approved_comments()`, and document every argument `::get_children()` actually accepts, several of which were already in use but undocumented. Comment ID arrays are narrowed to `non-negative-int[]`, and `WP_Comment_Query::$comments` is typed as `null` until a query is run. Several latent issues surfaced by the analysis are fixed: * `WP_Comment::get_children()` now returns a `count` or `fields` query directly rather than storing it in the children cache. That cache holds `WP_Comment` objects and is read back by `add_child()`, `get_child()`, and the 'flat' format, so writing an integer or a list of IDs into it left the object returning the wrong thing on a subsequent call. * `get_comment()` now hands only numeric values to `WP_Comment::get_instance()`. Previously anything that was not a `WP_Comment` or some other object fell through to be cast to an integer ID, even if it wasn't numeric. Now `null` is returned in such cases. * `WP_Comment::get_instance()` ignores a non-object read from the comment cache rather than passing it to the `WP_Comment` constructor, where `get_object_vars()` would raise a `TypeError`. * `WP_Comment::__isset()` returns `false`, and `WP_Comment::__get()` returns `null`, when the comment's post no longer exists, instead of raising a `TypeError` and a warning respectively. `__get()` also returns `null` when the comment is not attached to a post at all; previously `get_post( 0 )` fell back to the global `$post`, so the getter returned an unrelated post's field even though `__isset()` reported that same property as unset. Developed in WordPress#12606. Follow-up to r34583, r62648, r62694, r62717. Props westonruter, adamsilverstein. See #64898. git-svn-id: https://develop.svn.wordpress.org/trunk@62822 602fd350-edb4-49c9-b593-d223f7449a82
1 parent c45d184 commit 05375dd

5 files changed

Lines changed: 320 additions & 19 deletions

File tree

src/wp-includes/class-wp-comment-query.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,11 @@ class WP_Comment_Query {
9393
/**
9494
* List of comments located by the query.
9595
*
96+
* Null until a query has been run.
97+
*
9698
* @since 4.0.0
97-
* @var int[]|WP_Comment[]
99+
* @var int[]|WP_Comment[]|null
100+
* @phpstan-var non-negative-int[]|array<int, WP_Comment>|null
98101
*/
99102
public $comments;
100103

@@ -103,6 +106,7 @@ class WP_Comment_Query {
103106
*
104107
* @since 4.4.0
105108
* @var int
109+
* @phpstan-var non-negative-int
106110
*/
107111
public $found_comments = 0;
108112

@@ -111,6 +115,7 @@ class WP_Comment_Query {
111115
*
112116
* @since 4.4.0
113117
* @var int
118+
* @phpstan-var non-negative-int
114119
*/
115120
public $max_num_pages = 0;
116121

@@ -359,7 +364,8 @@ public function parse_query( $query = '' ) {
359364
* @since 4.2.0 Moved parsing to WP_Comment_Query::parse_query().
360365
*
361366
* @param string|array $query Array or URL query string of parameters.
362-
* @return array|int List of comments, or number of comments when 'count' is passed as a query var.
367+
* @return WP_Comment[]|int[]|int List of comments, or number of comments when 'count' is passed as a query var.
368+
* @phpstan-return array<int, WP_Comment>|non-negative-int[]|non-negative-int
363369
*/
364370
public function query( $query ) {
365371
$this->query_vars = wp_parse_args( $query );
@@ -374,6 +380,7 @@ public function query( $query ) {
374380
* @global wpdb $wpdb WordPress database abstraction object.
375381
*
376382
* @return int|int[]|WP_Comment[] List of comments or number of found comments if `$count` argument is true.
383+
* @phpstan-return array<int, WP_Comment>|non-negative-int[]|non-negative-int
377384
*/
378385
public function get_comments() {
379386
global $wpdb;
@@ -541,6 +548,7 @@ public function get_comments() {
541548
* @global wpdb $wpdb WordPress database abstraction object.
542549
*
543550
* @return int|array A single count of comment IDs if a count query. An array of comment IDs if a full query.
551+
* @phpstan-return non-negative-int|list<non-negative-int>
544552
*/
545553
protected function get_comment_ids() {
546554
global $wpdb;

src/wp-includes/class-wp-comment.php

Lines changed: 154 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,52 @@
1010
/**
1111
* Core class used to organize comments as instantiated objects with defined members.
1212
*
13+
* The `@property-read` fields below are not stored on the comment. They are proxied to the
14+
* comment's post by {@see WP_Comment::__get()}, and are null when the comment is not attached
15+
* to a post or when that post no longer exists.
16+
*
1317
* @since 4.4.0
18+
*
19+
* @property-read numeric-string|''|null $post_author
20+
* @property-read string|null $post_date
21+
* @property-read string|null $post_date_gmt
22+
* @property-read string|null $post_content
23+
* @property-read string|null $post_title
24+
* @property-read string|null $post_excerpt
25+
* @property-read non-empty-string|null $post_status
26+
* @property-read non-empty-string|null $comment_status
27+
* @property-read non-empty-string|null $ping_status
28+
* @property-read string|null $post_name
29+
* @property-read string|null $to_ping
30+
* @property-read string|null $pinged
31+
* @property-read string|null $post_modified
32+
* @property-read string|null $post_modified_gmt
33+
* @property-read string|null $post_content_filtered
34+
* @property-read non-negative-int|null $post_parent
35+
* @property-read string|null $guid
36+
* @property-read int|null $menu_order
37+
* @property-read non-empty-string|null $post_type
38+
* @property-read string|null $post_mime_type
39+
* @property-read numeric-string|null $comment_count
40+
*
41+
* @phpstan-type Data_Array array{
42+
* comment_ID: numeric-string,
43+
* comment_post_ID: numeric-string,
44+
* comment_author: string,
45+
* comment_author_email: string,
46+
* comment_author_url: string,
47+
* comment_author_IP: string,
48+
* comment_date: non-empty-string,
49+
* comment_date_gmt: non-empty-string,
50+
* comment_content: string,
51+
* comment_karma: numeric-string,
52+
* comment_approved: non-empty-string,
53+
* comment_agent: string,
54+
* comment_type: string,
55+
* comment_parent: numeric-string,
56+
* user_id: numeric-string,
57+
* ...
58+
* }
1459
*/
1560
#[AllowDynamicProperties]
1661
final class WP_Comment {
@@ -74,6 +119,7 @@ final class WP_Comment {
74119
*
75120
* @since 4.4.0
76121
* @var string
122+
* @phpstan-var non-empty-string
77123
*/
78124
public $comment_date = '0000-00-00 00:00:00';
79125

@@ -82,6 +128,7 @@ final class WP_Comment {
82128
*
83129
* @since 4.4.0
84130
* @var string
131+
* @phpstan-var non-empty-string
85132
*/
86133
public $comment_date_gmt = '0000-00-00 00:00:00';
87134

@@ -107,8 +154,12 @@ final class WP_Comment {
107154
/**
108155
* Comment approval status.
109156
*
157+
* The values used in core are '0' (unapproved), '1' (approved), 'spam', 'trash',
158+
* and 'post-trashed' (set for every comment on a post that is moved to the trash).
159+
*
110160
* @since 4.4.0
111161
* @var string
162+
* @phpstan-var non-empty-string
112163
*/
113164
public $comment_approved = '1';
114165

@@ -123,6 +174,13 @@ final class WP_Comment {
123174
/**
124175
* Comment type.
125176
*
177+
* The values used in core are 'comment', 'pingback', 'trackback', and 'note'. Custom
178+
* comment types are possible.
179+
*
180+
* Comments created before 5.5.0 may store an empty string rather than 'comment', so this
181+
* cannot be relied upon to be non-empty. {@see get_comment_type()} normalizes that case
182+
* when reading.
183+
*
126184
* @since 4.4.0
127185
* @since 5.5.0 Default value changed to `comment`.
128186
* @var string
@@ -154,8 +212,16 @@ final class WP_Comment {
154212
/**
155213
* Comment children.
156214
*
215+
* Mapping of comment ID to WP_Comment object, as populated by the default
216+
* `hierarchical => 'threaded'` argument of get_children(). Note that if a
217+
* caller passes a `hierarchical` value of 'flat' or `false` to
218+
* get_children(), a sequentially-keyed array of WP_Comment objects (also
219+
* including all descendants, in the 'flat' case) is stored here instead.
220+
*
221+
* Null until populated by {@see WP_Comment::get_children()}.
222+
*
157223
* @since 4.4.0
158-
* @var array
224+
* @var array<int, WP_Comment>|null
159225
*/
160226
protected $children;
161227

@@ -171,7 +237,8 @@ final class WP_Comment {
171237
* Post fields.
172238
*
173239
* @since 4.4.0
174-
* @var array
240+
* @var string[]
241+
* @phpstan-var list<non-empty-string>
175242
*/
176243
protected $post_fields = array( 'post_author', 'post_date', 'post_date_gmt', 'post_content', 'post_title', 'post_excerpt', 'post_status', 'comment_status', 'ping_status', 'post_name', 'to_ping', 'pinged', 'post_modified', 'post_modified_gmt', 'post_content_filtered', 'post_parent', 'guid', 'menu_order', 'post_type', 'post_mime_type', 'comment_count' );
177244

@@ -183,6 +250,7 @@ final class WP_Comment {
183250
* @global wpdb $wpdb WordPress database abstraction object.
184251
*
185252
* @param int $id Comment ID.
253+
* @phpstan-param int|numeric-string $id
186254
* @return WP_Comment|false Comment object, otherwise false.
187255
*/
188256
public static function get_instance( $id ) {
@@ -195,7 +263,8 @@ public static function get_instance( $id ) {
195263

196264
$_comment = wp_cache_get( $comment_id, 'comment' );
197265

198-
if ( ! $_comment ) {
266+
if ( ! is_object( $_comment ) ) {
267+
/** @var object{ comment_ID: string, comment_post_ID: string, comment_author: string, comment_author_email: string, comment_author_url: string, comment_author_IP: string, comment_date: string, comment_date_gmt: string, comment_content: string, comment_karma: string, comment_approved: string, comment_agent: string, comment_type: string, comment_parent: string, user_id: string }|null $_comment */
199268
$_comment = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $wpdb->comments WHERE comment_ID = %d LIMIT 1", $comment_id ) );
200269

201270
if ( ! $_comment ) {
@@ -228,19 +297,28 @@ public function __construct( $comment ) {
228297
*
229298
* @since 4.4.0
230299
*
231-
* @return array Object as array.
300+
* @return array<string, mixed> Object as array.
301+
* @phpstan-return Data_Array
232302
*/
233-
public function to_array() {
234-
return get_object_vars( $this );
303+
public function to_array(): array {
304+
/** @var Data_Array $comment */
305+
$comment = get_object_vars( $this );
306+
return $comment;
235307
}
236308

237309
/**
238310
* Gets the children of a comment.
239311
*
240312
* @since 4.4.0
313+
* @since 7.1.0 A `count` or `fields` query now returns its result directly rather than
314+
* erroneously storing it in the comment's children cache.
241315
*
242316
* @param array $args {
243-
* Array of arguments used to pass to get_comments() and determine format.
317+
* Array of arguments used to pass to {@see get_comments()} and determine format.
318+
* Any other argument accepted by {@see WP_Comment_Query::__construct()} may also be passed, and is
319+
* forwarded to `get_comments()`. Note that `parent` is always overridden with this comment's ID.
320+
* A `$count` or `$fields` query returns the direct children only, and does not populate the
321+
* comment's cached children, since that cache holds `WP_Comment` objects.
244322
*
245323
* @type string $format Return value format. 'tree' for a hierarchical tree, 'flat' for a flattened array.
246324
* Default 'tree'.
@@ -267,8 +345,42 @@ public function to_array() {
267345
* the value of $meta_key, and the array keys of
268346
* `$meta_query`. Also accepts false, an empty array, or
269347
* 'none' to disable `ORDER BY` clause.
348+
* @type string $fields Which fields to return. Accepts 'ids' for comment IDs, or an
349+
* empty string for full `WP_Comment` objects. Default empty.
350+
* @type bool $count Whether to return a comment count rather than comments.
351+
* Default false.
352+
* @type string $type Limit results to comments of a given type, such as 'comment',
353+
* 'pingback', 'trackback', or 'note'. Accepts 'all' for every
354+
* type. Default empty.
355+
* @type int $number Maximum number of comments to retrieve. Default empty (no limit).
356+
* @type int $post_id Limit results to comments on a given post. Default 0.
357+
* @type string $order How to order retrieved comments. Accepts 'ASC' or 'DESC'.
358+
* Default 'DESC'.
270359
* }
271-
* @return WP_Comment[] Array of `WP_Comment` objects.
360+
* @return WP_Comment[]|int[]|int Array of `WP_Comment` objects, an array of comment IDs when
361+
* `$fields` is 'ids', or the number of children when `$count`
362+
* is true.
363+
*
364+
* @phpstan-param array{
365+
* format?: 'tree'|'flat',
366+
* status?: 'hold'|'approve'|'all'|string,
367+
* hierarchical?: 'threaded'|'flat'|false,
368+
* orderby?: string|string[]|false,
369+
* fields?: 'ids'|'',
370+
* count?: bool,
371+
* type?: string,
372+
* number?: int,
373+
* post_id?: int,
374+
* order?: 'ASC'|'DESC',
375+
* ...
376+
* } $args
377+
* @phpstan-return (
378+
* $args is array{ count: true, ... } ? non-negative-int : (
379+
* $args is array{ fields: 'ids', ... } ? non-negative-int[] : (
380+
* $args is array{ format: 'flat', ... } ? list<WP_Comment> : array<int, WP_Comment>
381+
* )
382+
* )
383+
* )
272384
*/
273385
public function get_children( $args = array() ) {
274386
$defaults = array(
@@ -278,9 +390,29 @@ public function get_children( $args = array() ) {
278390
'orderby' => '',
279391
);
280392

393+
/** @var array{ format: 'tree'|'flat', status: string, hierarchical: 'threaded'|'flat'|false, orderby: string|string[]|false, fields?: 'ids'|'', count?: bool, type?: string, number?: int, post_id?: int, order?: 'ASC'|'DESC', ... } $_args */
281394
$_args = wp_parse_args( $args, $defaults );
282395
$_args['parent'] = $this->comment_ID;
283396

397+
/*
398+
* A 'count' or 'ids' query returns an integer or a list of comment IDs rather than
399+
* WP_Comment objects. Neither may be written to the children cache, which holds
400+
* WP_Comment objects and is read back by add_child(), get_child(), and the 'flat'
401+
* format below. Return the result directly and leave the cache untouched. The two
402+
* branches must stay separate: each is narrowed independently, and `count` is only
403+
* safe to overwrite in the 'ids' branch.
404+
*/
405+
if ( ! empty( $_args['count'] ) ) {
406+
return get_comments( $_args );
407+
} elseif ( isset( $_args['fields'] ) && 'ids' === $_args['fields'] ) {
408+
$_args['count'] = false; // For static analysis of the conditional return type.
409+
return get_comments( $_args );
410+
}
411+
412+
// Only WP_Comment objects are returned past this point. Stated positively for static analysis.
413+
$_args['count'] = false;
414+
$_args['fields'] = '';
415+
284416
if ( is_null( $this->children ) ) {
285417
if ( $this->populated_children ) {
286418
$this->children = array();
@@ -315,8 +447,8 @@ public function get_children( $args = array() ) {
315447
*
316448
* @param WP_Comment $child Child comment.
317449
*/
318-
public function add_child( WP_Comment $child ) {
319-
$this->children[ $child->comment_ID ] = $child;
450+
public function add_child( WP_Comment $child ): void {
451+
$this->children[ (int) $child->comment_ID ] = $child;
320452
}
321453

322454
/**
@@ -341,7 +473,7 @@ public function get_child( $child_id ) {
341473
*
342474
* @param bool $set Whether the comment's children have already been populated.
343475
*/
344-
public function populated_children( $set ) {
476+
public function populated_children( $set ): void {
345477
$this->populated_children = (bool) $set;
346478
}
347479

@@ -351,14 +483,15 @@ public function populated_children( $set ) {
351483
* If `$name` matches a post field, the comment post will be loaded and the post's value checked.
352484
*
353485
* @since 4.4.0
486+
* @since 7.1.0 Returns false instead of causing a fatal error when the comment's post cannot be found.
354487
*
355488
* @param string $name Property to check if set.
356489
* @return bool Whether the property is set.
357490
*/
358491
public function __isset( $name ) {
359492
if ( in_array( $name, $this->post_fields, true ) && 0 !== (int) $this->comment_post_ID ) {
360-
$post = get_post( $this->comment_post_ID );
361-
return property_exists( $post, $name );
493+
$post = get_post( (int) $this->comment_post_ID );
494+
return $post && property_exists( $post, $name );
362495
}
363496

364497
return false;
@@ -370,14 +503,20 @@ public function __isset( $name ) {
370503
* If `$name` matches a post field, the comment post will be loaded and the post's value returned.
371504
*
372505
* @since 4.4.0
506+
* @since 7.1.0 Returns null instead of the global post's field when the comment is not attached to
507+
* a post, and no longer raises a warning when the comment's post cannot be found.
373508
*
374509
* @param string $name Property name.
375510
* @return mixed
376511
*/
377512
public function __get( $name ) {
378-
if ( in_array( $name, $this->post_fields, true ) ) {
379-
$post = get_post( $this->comment_post_ID );
513+
if ( in_array( $name, $this->post_fields, true ) && 0 !== (int) $this->comment_post_ID ) {
514+
$post = get_post( (int) $this->comment_post_ID );
515+
if ( ! $post ) {
516+
return null;
517+
}
380518
return $post->$name;
381519
}
520+
return null;
382521
}
383522
}

0 commit comments

Comments
 (0)