From 4d8b6090c3e70771a1b7d56b8ac6772735ff1a54 Mon Sep 17 00:00:00 2001 From: Olivier Gorzalka Date: Thu, 15 Oct 2026 15:17:01 +0200 Subject: [PATCH] feat: add CommentQuery for WP_Comment_Query - CommentQuery reusing metaQuery(), dateQuery(), MetaField, limits and paging concerns - OrdersByMap concern shared by UserQuery and CommentQuery - terminals: first, ids, count, exists --- README.md | 27 +++ src/CommentQuery.php | 58 ++++++ src/Traits/Comments/Caching.php | 51 +++++ src/Traits/Comments/Fields.php | 30 +++ src/Traits/Comments/Filters.php | 290 ++++++++++++++++++++++++++++ src/Traits/Comments/Order.php | 53 +++++ src/Traits/Comments/PostFilters.php | 137 +++++++++++++ src/Traits/Comments/Retrieval.php | 86 +++++++++ src/Traits/Concerns/OrdersByMap.php | 41 ++++ src/Traits/User/Order.php | 30 +-- tests/Feature/CommentQueryTest.php | 205 ++++++++++++++++++++ tests/helpers/wp.php | 38 ++++ 12 files changed, 1018 insertions(+), 28 deletions(-) create mode 100644 src/CommentQuery.php create mode 100644 src/Traits/Comments/Caching.php create mode 100644 src/Traits/Comments/Fields.php create mode 100644 src/Traits/Comments/Filters.php create mode 100644 src/Traits/Comments/Order.php create mode 100644 src/Traits/Comments/PostFilters.php create mode 100644 src/Traits/Comments/Retrieval.php create mode 100644 src/Traits/Concerns/OrdersByMap.php create mode 100644 tests/Feature/CommentQueryTest.php diff --git a/README.md b/README.md index 5251763..7af7431 100644 --- a/README.md +++ b/README.md @@ -1868,6 +1868,33 @@ UserQuery::select() The deprecated `who` parameter is not supported: use `capability()`. +## CommentQuery + +`CommentQuery` builds `WP_Comment_Query` arguments. `metaQuery()`, `dateQuery()` and the `meta*()` methods are the same ones as on `PostQuery`. + +```php +use Pollora\Query\CommentQuery; + +CommentQuery::select() + ->postId(42) + ->approved() + ->threaded() + ->latest() + ->get(); // WP_Comment_Query +``` + +- **Comments**: `whereIn()` (`comment__in`), `whereNotIn()`, `authorEmail()`, `authorUrl()`, `authorIn()`, `authorNotIn()`, `userId()`, `karma()`, `search()`, `includeUnapproved()`. `CommentQuery::find(5)` or `find([1, 2])` is `comment__in`. +- **Status and type**: `status()`, `approved()` (`approve`), `pending()` (`hold`), `type()`, `typeIn()`, `typeNotIn()`. +- **Replies**: `parentId()`, `rootOnly()`, `parentIn()`, `parentNotIn()`, `hierarchical('threaded' | 'flat' | false)`, `threaded()`, `flat()`. +- **Post**: `postId()`, `postIn()`, `postNotIn()`, `postAuthor()`, `postAuthorIn()`, `postAuthorNotIn()`, `postName()`, `postParent()`, `postStatus()`, `postType()`. +- **Order**: `orderBy($field, $direction)` can be chained (fields and directions are sent as a map); `orderByDate()`, `orderByAuthor()`, `orderByInclude()`, `latest()`, `oldest()`, `withoutOrder()`, `orderByMeta()`. +- **Limit**: `limit()` / `take()` (`number`), `offset()` / `skip()`, `paged()`. +- **Fields**: `select('ids')` or `fields('ids')`; `'all'` (or `'*'`) is WP's default. +- **Cache**: `noFoundRows()`, `updateMetaCache()`, `updatePostCache()`, `cacheDomain()`. +- **Results**: `get()` returns the `WP_Comment_Query`; `first()`, `ids()`, `count()` and `exists()` run it on a copy of the builder. `count()` ignores `limit()`, `offset()` and `paged()`. + +> WP's default status is `all`, which is `hold` and `approve`: **spam and trash comments are left out** unless you ask for them with `status()`. `CommentQuery` does not change that. WP also skips counting the total rows by default (`no_found_rows`), call `noFoundRows(false)` if you need it. + ## Testing ```bash diff --git a/src/CommentQuery.php b/src/CommentQuery.php new file mode 100644 index 0000000..2f43186 --- /dev/null +++ b/src/CommentQuery.php @@ -0,0 +1,58 @@ +|int|null $commentId A comment ID, or a list of comment IDs. + */ + public function __construct(array|int|null $commentId = null, ?string $fields = null) + { + if (is_int($commentId)) { + $this->whereIn([$commentId]); + } elseif (is_array($commentId)) { + $this->whereIn($commentId); + } + + if ($fields) { + $this->fields($fields); + } + } + + /** + * @param array|int|null $commentId + */ + public static function find(array|int|null $commentId = null): self + { + return new static($commentId); + } + + public static function select(?string $fields = 'all'): self + { + return new static(null, $fields); + } + + public function get(): WP_Comment_Query + { + return new WP_Comment_Query($this->getArguments()); + } +} diff --git a/src/Traits/Comments/Caching.php b/src/Traits/Comments/Caching.php new file mode 100644 index 0000000..7a74893 --- /dev/null +++ b/src/Traits/Comments/Caching.php @@ -0,0 +1,51 @@ +noFoundRows = $value; + + return $this; + } + + public function updateMetaCache(bool $value = true): self + { + $this->updateCommentMetaCache = $value; + + return $this; + } + + /** + * Primes the cache of the posts the comments belong to. + */ + public function updatePostCache(bool $value = true): self + { + $this->updateCommentPostCache = $value; + + return $this; + } + + public function cacheDomain(string $domain): self + { + $this->cacheDomain = $domain; + + return $this; + } +} diff --git a/src/Traits/Comments/Fields.php b/src/Traits/Comments/Fields.php new file mode 100644 index 0000000..14e869b --- /dev/null +++ b/src/Traits/Comments/Fields.php @@ -0,0 +1,30 @@ +assertOneOf($value, ['all', 'ids'], 'fields value'); + + $this->fields = $value === 'all' ? null : $value; + + return $this; + } +} diff --git a/src/Traits/Comments/Filters.php b/src/Traits/Comments/Filters.php new file mode 100644 index 0000000..64183da --- /dev/null +++ b/src/Traits/Comments/Filters.php @@ -0,0 +1,290 @@ +|null + */ + protected ?array $author_In = null; + + /** + * @var array|null + */ + protected ?array $author_NotIn = null; + + /** + * @var array|null + */ + protected ?array $comment_In = null; + + /** + * @var array|null + */ + protected ?array $comment_NotIn = null; + + /** + * @var array|null + */ + protected ?array $includeUnapproved = null; + + protected ?int $karma = null; + + protected ?int $parent = null; + + /** + * @var array|null + */ + protected ?array $parent_In = null; + + /** + * @var array|null + */ + protected ?array $parent_NotIn = null; + + protected ?int $userId = null; + + protected ?string $search = null; + + /** + * @var string|array|null + */ + protected string|array|null $status = null; + + /** + * @var string|array|null + */ + protected string|array|null $type = null; + + /** + * @var array|null + */ + protected ?array $type_In = null; + + /** + * @var array|null + */ + protected ?array $type_NotIn = null; + + protected string|bool|null $hierarchical = null; + + public function authorEmail(string $email): self + { + $this->authorEmail = $email; + + return $this; + } + + public function authorUrl(string $url): self + { + $this->authorUrl = $url; + + return $this; + } + + /** + * Comments written by these registered users. + * + * @param array $userIds + */ + public function authorIn(array $userIds): self + { + $this->author_In = $userIds; + + return $this; + } + + /** + * @param array $userIds + */ + public function authorNotIn(array $userIds): self + { + $this->author_NotIn = $userIds; + + return $this; + } + + /** + * @param array $commentIds + */ + public function whereIn(array $commentIds): self + { + $this->comment_In = $commentIds; + + return $this; + } + + /** + * @param array $commentIds + */ + public function whereNotIn(array $commentIds): self + { + $this->comment_NotIn = $commentIds; + + return $this; + } + + /** + * IDs or emails of users whose unapproved comments are returned whatever the status. + * + * @param array $users + */ + public function includeUnapproved(array $users): self + { + $this->includeUnapproved = $users; + + return $this; + } + + public function karma(int $karma): self + { + $this->karma = $karma; + + return $this; + } + + /** + * The direct replies to a comment. + */ + public function parentId(int $commentId): self + { + $this->parent = $commentId; + + return $this; + } + + /** + * Top-level comments only. + */ + public function rootOnly(): self + { + return $this->parentId(0); + } + + /** + * @param array $commentIds + */ + public function parentIn(array $commentIds): self + { + $this->parent_In = $commentIds; + + return $this; + } + + /** + * @param array $commentIds + */ + public function parentNotIn(array $commentIds): self + { + $this->parent_NotIn = $commentIds; + + return $this; + } + + public function userId(int $userId): self + { + $this->userId = $userId; + + return $this; + } + + public function search(string $keyword): self + { + $this->search = $keyword; + + return $this; + } + + /** + * 'hold', 'approve', 'all' (hold and approve only: spam and trash are left out), or a custom status + * such as 'spam' or 'trash'. WP defaults to 'all'. + * + * @param string|array $status + */ + public function status(string|array $status): self + { + $this->status = $status; + + return $this; + } + + public function approved(): self + { + return $this->status('approve'); + } + + /** + * Comments awaiting moderation. + */ + public function pending(): self + { + return $this->status('hold'); + } + + /** + * 'comment', 'pings' (pingbacks and trackbacks) or any custom type. + * + * @param string|array $type + */ + public function type(string|array $type): self + { + $this->type = $type; + + return $this; + } + + /** + * @param array $types + */ + public function typeIn(array $types): self + { + $this->type_In = $types; + + return $this; + } + + /** + * @param array $types + */ + public function typeNotIn(array $types): self + { + $this->type_NotIn = $types; + + return $this; + } + + /** + * Includes the descendants of the matched comments: 'threaded' nests them in a `children` property, + * 'flat' appends them to the list, false leaves them out. Ignored by WP with `fields('ids')` or count(). + * + * @throws QueryException + */ + public function hierarchical(string|bool $mode = 'threaded'): self + { + $this->hierarchical = $mode === false ? false : $this->assertOneOf((string) $mode, ['threaded', 'flat'], 'hierarchical mode'); + + return $this; + } + + public function threaded(): self + { + return $this->hierarchical('threaded'); + } + + public function flat(): self + { + return $this->hierarchical('flat'); + } +} diff --git a/src/Traits/Comments/Order.php b/src/Traits/Comments/Order.php new file mode 100644 index 0000000..56749c4 --- /dev/null +++ b/src/Traits/Comments/Order.php @@ -0,0 +1,53 @@ +orderBy('comment_date_gmt', $order); + } + + public function orderByAuthor(string $order = 'ASC'): self + { + return $this->orderBy('comment_author', $order); + } + + /** + * Keeps the order of the IDs given to whereIn(). + */ + public function orderByInclude(): self + { + return $this->orderBy('comment__in'); + } + + /** + * Most recent first. + */ + public function latest(): self + { + return $this->orderByDate('DESC'); + } + + public function oldest(): self + { + return $this->orderByDate('ASC'); + } + + /** + * Disables the ORDER BY clause. + */ + public function withoutOrder(): self + { + $this->orderby = 'none'; + + return $this; + } +} diff --git a/src/Traits/Comments/PostFilters.php b/src/Traits/Comments/PostFilters.php new file mode 100644 index 0000000..fd08ee2 --- /dev/null +++ b/src/Traits/Comments/PostFilters.php @@ -0,0 +1,137 @@ +|null + */ + protected ?array $post_In = null; + + /** + * @var array|null + */ + protected ?array $post_NotIn = null; + + protected ?int $postAuthor = null; + + /** + * @var array|null + */ + protected ?array $postAuthor_In = null; + + /** + * @var array|null + */ + protected ?array $postAuthor_NotIn = null; + + protected ?string $postName = null; + + protected ?int $postParent = null; + + /** + * @var string|array|null + */ + protected string|array|null $postStatus = null; + + /** + * @var string|array|null + */ + protected string|array|null $postType = null; + + public function postId(int $postId): self + { + $this->postId = $postId; + + return $this; + } + + /** + * @param array $postIds + */ + public function postIn(array $postIds): self + { + $this->post_In = $postIds; + + return $this; + } + + /** + * @param array $postIds + */ + public function postNotIn(array $postIds): self + { + $this->post_NotIn = $postIds; + + return $this; + } + + public function postAuthor(int $userId): self + { + $this->postAuthor = $userId; + + return $this; + } + + /** + * @param array $userIds + */ + public function postAuthorIn(array $userIds): self + { + $this->postAuthor_In = $userIds; + + return $this; + } + + /** + * @param array $userIds + */ + public function postAuthorNotIn(array $userIds): self + { + $this->postAuthor_NotIn = $userIds; + + return $this; + } + + public function postName(string $slug): self + { + $this->postName = $slug; + + return $this; + } + + public function postParent(int $postId): self + { + $this->postParent = $postId; + + return $this; + } + + /** + * @param string|array $status Post status, or 'any'. + */ + public function postStatus(string|array $status): self + { + $this->postStatus = $status; + + return $this; + } + + /** + * @param string|array $type Post type, or 'any'. + */ + public function postType(string|array $type): self + { + $this->postType = $type; + + return $this; + } +} diff --git a/src/Traits/Comments/Retrieval.php b/src/Traits/Comments/Retrieval.php new file mode 100644 index 0000000..f1e7e70 --- /dev/null +++ b/src/Traits/Comments/Retrieval.php @@ -0,0 +1,86 @@ +runOnCopy(function (self $copy): void { + $copy->number = 1; + $copy->offset = null; + $copy->paged = null; + $copy->hierarchical = null; + }); + + return $comments[0] ?? null; + } + + /** + * @return array + */ + public function ids(): array + { + return array_map('intval', $this->runOnCopy(function (self $copy): void { + $copy->fields = 'ids'; + $copy->hierarchical = null; + })); + } + + /** + * The total number of matching comments, whatever the limit, offset or page set on the builder. + */ + public function count(): int + { + $copyArgs = $this->copyWith(function (self $copy): void { + $copy->count = true; + $copy->number = null; + $copy->offset = null; + $copy->paged = null; + $copy->hierarchical = null; + }); + + return (int) (new WP_Comment_Query)->query($copyArgs); + } + + public function exists(): bool + { + return $this->runOnCopy(function (self $copy): void { + $copy->fields = 'ids'; + $copy->number = 1; + $copy->offset = null; + $copy->paged = null; + $copy->hierarchical = null; + }) !== []; + } + + /** + * @return array + */ + private function runOnCopy(callable $configure): array + { + $comments = (new WP_Comment_Query)->query($this->copyWith($configure)); + + return is_array($comments) ? array_values($comments) : []; + } +} diff --git a/src/Traits/Concerns/OrdersByMap.php b/src/Traits/Concerns/OrdersByMap.php new file mode 100644 index 0000000..bd8cb01 --- /dev/null +++ b/src/Traits/Concerns/OrdersByMap.php @@ -0,0 +1,41 @@ + direction, which WP_User_Query and WP_Comment_Query accept. + */ +trait OrdersByMap +{ + use ValidatesChoices; + + /** + * @var array|string|null + */ + protected array|string|null $orderby = null; + + /** + * Adds fields to sort by, in call order. Meta keys and meta query clause names are valid fields too. + * + * @param array|string $field A field, or a map of field => direction. + * + * @throws QueryException If a direction is not ASC or DESC. + */ + public function orderBy(array|string $field, string $order = 'ASC'): self + { + $columns = is_array($field) ? $field : [$field => $order]; + $current = is_array($this->orderby) ? $this->orderby : []; + + foreach ($columns as $column => $direction) { + $current[$column] = $this->assertDirection($direction); + } + + $this->orderby = $current; + + return $this; + } +} diff --git a/src/Traits/User/Order.php b/src/Traits/User/Order.php index 53094b0..1eaeba9 100644 --- a/src/Traits/User/Order.php +++ b/src/Traits/User/Order.php @@ -4,37 +4,11 @@ namespace Pollora\Query\Traits\User; -use Pollora\Query\QueryException; -use Pollora\Query\Traits\Concerns\ValidatesChoices; +use Pollora\Query\Traits\Concerns\OrdersByMap; trait Order { - use ValidatesChoices; - - /** - * @var array|null - */ - protected ?array $orderby = null; - - /** - * Fields to sort by, in call order: a user field (ID, display_name, user_login, user_nicename, - * user_email, user_url, user_registered, post_count...), 'include', 'login__in', 'nicename__in', - * 'meta_value', 'meta_value_num', a meta key or a meta query clause name. - * - * @param array|string $field A field, or a map of field => direction. - * - * @throws QueryException If a direction is not ASC or DESC. - */ - public function orderBy(array|string $field, string $order = 'ASC'): self - { - $columns = is_array($field) ? $field : [$field => $order]; - - foreach ($columns as $column => $direction) { - $this->orderby[$column] = $this->assertDirection($direction); - } - - return $this; - } + use OrdersByMap; public function orderByName(string $order = 'ASC'): self { diff --git a/tests/Feature/CommentQueryTest.php b/tests/Feature/CommentQueryTest.php new file mode 100644 index 0000000..0336f02 --- /dev/null +++ b/tests/Feature/CommentQueryTest.php @@ -0,0 +1,205 @@ +getArguments())->toBe([]); +}); + +test('CommentQuery: find maps to comment__in', function () { + expect(CommentQuery::find(5)->getArguments())->toBe(['comment__in' => [5]]) + ->and(CommentQuery::find([1, 2])->getArguments())->toBe(['comment__in' => [1, 2]]); +}); + +test('CommentQuery: author and identity filters', function () { + $args = CommentQuery::select() + ->authorEmail('a@x.test')->authorUrl('https://x.test')->authorIn([1])->authorNotIn([2]) + ->whereIn([3])->whereNotIn([4])->userId(9)->karma(2)->search('hello') + ->getArguments(); + + expect($args)->toMatchArray([ + 'author_email' => 'a@x.test', 'author_url' => 'https://x.test', 'author__in' => [1], 'author__not_in' => [2], + 'comment__in' => [3], 'comment__not_in' => [4], 'user_id' => 9, 'karma' => 2, 'search' => 'hello', + ]); +}); + +test('CommentQuery: unapproved comments', function () { + expect(CommentQuery::select()->includeUnapproved([3, 'me@x.test'])->getArguments()) + ->toMatchArray(['include_unapproved' => [3, 'me@x.test']]); +}); + +test('CommentQuery: status shortcuts', function () { + expect(CommentQuery::select()->approved()->getArguments()['status'])->toBe('approve') + ->and(CommentQuery::select()->pending()->getArguments()['status'])->toBe('hold') + ->and(CommentQuery::select()->status(['approve', 'spam'])->getArguments()['status'])->toBe(['approve', 'spam']); +}); + +test('CommentQuery: types', function () { + $args = CommentQuery::select()->type('pings')->typeIn(['a'])->typeNotIn(['b'])->getArguments(); + + expect($args)->toMatchArray(['type' => 'pings', 'type__in' => ['a'], 'type__not_in' => ['b']]); +}); + +test('CommentQuery: replies', function () { + $args = CommentQuery::select()->parentId(4)->parentIn([1])->parentNotIn([2])->getArguments(); + + expect($args)->toMatchArray(['parent' => 4, 'parent__in' => [1], 'parent__not_in' => [2]]) + ->and(CommentQuery::select()->rootOnly()->getArguments())->toHaveKey('parent', 0); +}); + +test('CommentQuery: filters on the post', function () { + $args = CommentQuery::select() + ->postId(7)->postIn([1])->postNotIn([2])->postAuthor(3)->postAuthorIn([4])->postAuthorNotIn([5]) + ->postName('hello')->postParent(6)->postStatus(['publish', 'private'])->postType('page') + ->getArguments(); + + expect($args)->toMatchArray([ + 'post_id' => 7, 'post__in' => [1], 'post__not_in' => [2], 'post_author' => 3, + 'post_author__in' => [4], 'post_author__not_in' => [5], 'post_name' => 'hello', 'post_parent' => 6, + 'post_status' => ['publish', 'private'], 'post_type' => 'page', + ]); +}); + +test('CommentQuery: hierarchical', function () { + expect(CommentQuery::select()->hierarchical()->getArguments()['hierarchical'])->toBe('threaded') + ->and(CommentQuery::select()->threaded()->getArguments()['hierarchical'])->toBe('threaded') + ->and(CommentQuery::select()->flat()->getArguments()['hierarchical'])->toBe('flat') + ->and(CommentQuery::select()->hierarchical(false)->getArguments()['hierarchical'])->toBeFalse(); +}); + +test('CommentQuery: invalid hierarchical mode throws', function () { + CommentQuery::select()->hierarchical('tree'); +})->throws(QueryException::class, 'Invalid hierarchical mode supplied: tree'); + +test('CommentQuery: fields', function () { + expect(CommentQuery::select('ids')->getArguments()['fields'])->toBe('ids') + ->and(CommentQuery::select('*')->getArguments())->not->toHaveKey('fields') + ->and(CommentQuery::select('all')->getArguments())->not->toHaveKey('fields'); +}); + +test('CommentQuery: invalid fields throws', function () { + CommentQuery::select('bogus'); +})->throws(QueryException::class, 'Invalid fields value supplied: bogus'); + +test('CommentQuery: orderby is a map of fields and directions', function () { + $args = CommentQuery::select()->orderBy('comment_author', 'asc')->orderByDate()->getArguments(); + + expect($args['orderby'])->toBe(['comment_author' => 'ASC', 'comment_date_gmt' => 'DESC']); +}); + +test('CommentQuery: order shortcuts', function () { + expect(CommentQuery::select()->latest()->getArguments()['orderby'])->toBe(['comment_date_gmt' => 'DESC']) + ->and(CommentQuery::select()->oldest()->getArguments()['orderby'])->toBe(['comment_date_gmt' => 'ASC']) + ->and(CommentQuery::select()->orderByInclude()->getArguments()['orderby'])->toBe(['comment__in' => 'ASC']) + ->and(CommentQuery::select()->withoutOrder()->getArguments()['orderby'])->toBe('none'); +}); + +test('CommentQuery: ordering again after withoutOrder starts over', function () { + expect(CommentQuery::select()->withoutOrder()->latest()->getArguments()['orderby'])->toBe(['comment_date_gmt' => 'DESC']); +}); + +test('CommentQuery: invalid order direction throws', function () { + CommentQuery::select()->orderBy('comment_author', 'sideways'); +})->throws(QueryException::class, 'Invalid order direction supplied: SIDEWAYS'); + +test('CommentQuery: limit, offset and paged', function () { + expect(CommentQuery::select()->limit(5)->offset(10)->paged(2)->getArguments())->toMatchArray(['number' => 5, 'offset' => 10, 'paged' => 2]); +}); + +test('CommentQuery: cache and found rows', function () { + $args = CommentQuery::select()->noFoundRows(false)->updateMetaCache(false)->updatePostCache()->cacheDomain('d')->getArguments(); + + expect($args)->toMatchArray([ + 'no_found_rows' => false, 'update_comment_meta_cache' => false, 'update_comment_post_cache' => true, 'cache_domain' => 'd', + ]); +}); + +test('CommentQuery: meta arguments and orderByMeta', function () { + $args = CommentQuery::select()->metaKey('rating')->metaValue(5)->orderByMeta('rating', 'DESC', true)->getArguments(); + + expect($args)->toMatchArray(['meta_key' => 'rating', 'meta_value' => 5]) + ->and($args['orderby'])->toBe(['meta_value_num' => 'DESC']); +}); + +test('CommentQuery: meta_query reuses the meta builder', function () { + $args = CommentQuery::select()->metaQuery(function (MetaQuery $q) { + $q->where($q->meta('rating')->greaterThan(3))->orWhere($q->meta('verified')->equalTo('1')); + })->getArguments(); + + expect($args['meta_query']['relation'])->toBe('OR') + ->and(array_column(array_filter($args['meta_query'], 'is_array'), 'key'))->toBe(['rating', 'verified']); +}); + +test('CommentQuery: date_query leaves the column to WP (comment_date)', function () { + $args = CommentQuery::select()->dateQuery(function (DateQuery $q) { + $q->where($q->date()->after('2024-01-01')); + })->getArguments(); + + expect($args['date_query'][0])->not->toHaveKey('column'); +}); + +test('CommentQuery: get runs WP_Comment_Query with the arguments', function () { + $query = CommentQuery::select()->postId(3)->get(); + + expect(WP_Comment_Query::$log[0])->toMatchArray(['post_id' => 3]) + ->and($query)->toBeInstanceOf(WP_Comment_Query::class); +}); + +test('CommentQuery: first', function () { + $comment = new stdClass; + WP_Comment_Query::$fakeComments = [$comment, new stdClass]; + + expect(CommentQuery::select()->postId(3)->first())->toBe($comment) + ->and(WP_Comment_Query::$log[0])->toMatchArray(['number' => 1, 'post_id' => 3]); +}); + +test('CommentQuery: first returns null when nothing matches and ignores paging and threading', function () { + expect(CommentQuery::select()->offset(5)->paged(3)->threaded()->first())->toBeNull() + ->and(WP_Comment_Query::$log[0])->not->toHaveKeys(['offset', 'paged', 'hierarchical']); +}); + +test('CommentQuery: ids', function () { + WP_Comment_Query::$fakeComments = ['3', 5]; + + expect(CommentQuery::select()->limit(20)->ids())->toBe([3, 5]) + ->and(WP_Comment_Query::$log[0])->toMatchArray(['fields' => 'ids', 'number' => 20]); +}); + +test('CommentQuery: count ignores limit, offset and page', function () { + WP_Comment_Query::$fakeCount = 42; + + expect(CommentQuery::select()->postId(3)->limit(10)->offset(5)->paged(3)->count())->toBe(42) + ->and(WP_Comment_Query::$log[0])->toMatchArray(['count' => true, 'post_id' => 3]) + ->and(WP_Comment_Query::$log[0])->not->toHaveKeys(['number', 'offset', 'paged']); +}); + +test('CommentQuery: exists', function () { + expect(CommentQuery::select()->exists())->toBeFalse(); + + WP_Comment_Query::$fakeComments = [1]; + + expect(CommentQuery::select()->exists())->toBeTrue() + ->and(WP_Comment_Query::$log[1])->toMatchArray(['fields' => 'ids', 'number' => 1]); +}); + +test('CommentQuery: terminals do not alter the builder', function () { + $query = CommentQuery::select()->postId(3)->limit(10)->offset(5)->paged(2)->threaded(); + $before = $query->getArguments(); + + $query->first(); + $query->ids(); + $query->count(); + $query->exists(); + + expect($query->getArguments())->toBe($before) + ->and($before)->not->toHaveKey('count'); +}); diff --git a/tests/helpers/wp.php b/tests/helpers/wp.php index e066093..1e2fccb 100644 --- a/tests/helpers/wp.php +++ b/tests/helpers/wp.php @@ -128,3 +128,41 @@ public function get_total(): int } } } + +if (! class_exists('WP_Comment_Query')) { + /** + * Minimal WP_Comment_Query double: records the arguments and returns the comments set up by the test. + */ + class WP_Comment_Query + { + /** @var array */ + public static array $log = []; + + public static mixed $fakeComments = []; + + public static int $fakeCount = 0; + + public array $query_vars = []; + + public array $comments = []; + + public function __construct(array $args = []) + { + if ($args !== []) { + $this->query($args); + } + } + + public function query(array $args): array|int + { + self::$log[] = $args; + $this->query_vars = $args; + + if (! empty($args['count'])) { + return self::$fakeCount; + } + + return $this->comments = self::$fakeComments; + } + } +}