From 1a01c9767d9e68191dd03a366081f41a81180176 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Tue, 26 Sep 2023 17:05:49 +0100 Subject: [PATCH] refactor: merge filterer and searcher concepts (#3892) * chore: drop remaining backend regex gambits * refactor: merge filterer & searcher concept * refactor: adapt extenders * refactor: no longer need to push gambits to `q` * refactor: filters to gambits * refactor: drop shred `Query` namespace * Apply fixes from StyleCI * chore: cleanup * Apply fixes from StyleCI --- extensions/likes/extend.php | 8 +- extensions/likes/src/Query/LikedByFilter.php | 12 +- extensions/likes/src/Query/LikedFilter.php | 12 +- extensions/lock/extend.php | 8 +- .../common/query/discussions/LockedGambit.ts | 8 + extensions/lock/src/Filter/LockedFilter.php | 32 +++ .../lock/src/Query/LockedFilterGambit.php | 44 ---- extensions/mentions/extend.php | 4 +- .../mentions/src/Filter/MentionedFilter.php | 12 +- .../src/Filter/MentionedPostFilter.php | 10 +- extensions/nicknames/extend.php | 2 +- .../nicknames/src/NicknameFullTextGambit.php | 19 +- extensions/sticky/extend.php | 10 +- .../common/query/discussions/StickyGambit.ts | 8 + .../src/PinStickiedDiscussionsToTop.php | 20 +- extensions/sticky/src/Query/StickyFilter.php | 32 +++ .../sticky/src/Query/StickyFilterGambit.php | 44 ---- extensions/subscriptions/extend.php | 10 +- .../query/discussions/SubscriptionGambit.ts | 8 + .../js/src/forum/addSubscriptionFilter.js | 7 +- .../SubscriptionFilter.php} | 28 +-- .../src/HideIgnoredFromAllDiscussionsPage.php | 12 +- extensions/suspend/extend.php | 8 +- .../src/common/query/users/SuspendedGambit.ts | 8 + ...edFilterGambit.php => SuspendedFilter.php} | 31 +-- extensions/tags/extend.php | 16 +- .../src/common/query/discussions/TagGambit.ts | 8 + extensions/tags/js/src/forum/addTagFilter.tsx | 10 +- .../src/Api/Controller/ListTagsController.php | 4 +- .../HideHiddenTagsFromAllDiscussionsPage.php | 10 +- extensions/tags/src/Filter/PostTagFilter.php | 12 +- .../TagFilter.php} | 24 +-- .../tags/src/Search/Gambit/FulltextGambit.php | 12 +- extensions/tags/src/Search/TagSearcher.php | 13 +- framework/core/js/src/common/GambitManager.ts | 21 ++ framework/core/js/src/common/query/IGambit.ts | 2 + .../common/query/discussions/AuthorGambit.ts | 8 + .../common/query/discussions/CreatedGambit.ts | 17 +- .../common/query/discussions/HiddenGambit.ts | 8 + .../common/query/discussions/UnreadGambit.ts | 8 + .../js/src/common/query/users/EmailGambit.ts | 8 + .../js/src/common/query/users/GroupGambit.ts | 8 + .../components/DiscussionsSearchSource.tsx | 7 +- .../js/src/forum/states/GlobalSearchState.ts | 16 +- .../Controller/ListAccessTokensController.php | 8 +- .../Controller/ListDiscussionsController.php | 12 +- .../Api/Controller/ListGroupsController.php | 10 +- .../Api/Controller/ListPostsController.php | 8 +- .../Api/Controller/ListUsersController.php | 12 +- .../src/Database/ScopeVisibilityTrait.php | 3 + .../src/Discussion/Filter/AuthorFilter.php | 10 +- .../src/Discussion/Filter/CreatedFilter.php | 25 +-- .../Discussion/Filter/DiscussionFilterer.php | 28 --- .../src/Discussion/Filter/HiddenFilter.php | 8 +- .../src/Discussion/Filter/UnreadFilter.php | 8 +- .../Discussion/Search/DiscussionSearcher.php | 15 +- .../Search/Gambit/FulltextGambit.php | 16 +- framework/core/src/Extend/Filter.php | 82 -------- .../core/src/Extend/SimpleFlarumSearch.php | 55 ++--- .../core/src/Filter/AbstractFilterer.php | 76 ------- .../core/src/Filter/FilterServiceProvider.php | 96 --------- framework/core/src/Filter/FilterState.php | 41 ---- .../core/src/Foundation/InstalledSite.php | 2 - .../core/src/Group/Filter/GroupFilterer.php | 31 --- .../core/src/Group/Filter/GroupSearcher.php | 23 ++ .../core/src/Group/Filter/HiddenFilter.php | 12 +- ...enFilterer.php => AccessTokenSearcher.php} | 4 +- framework/core/src/Http/Filter/UserFilter.php | 12 +- .../core/src/Post/Filter/AuthorFilter.php | 12 +- .../core/src/Post/Filter/DiscussionFilter.php | 12 +- framework/core/src/Post/Filter/IdFilter.php | 12 +- .../core/src/Post/Filter/NumberFilter.php | 12 +- .../core/src/Post/Filter/PostFilterer.php | 31 --- .../core/src/Post/Filter/PostSearcher.php | 23 ++ framework/core/src/Post/Filter/TypeFilter.php | 12 +- .../core/src/Query/AbstractQueryState.php | 59 ------ .../src/Query/ApplyQueryParametersTrait.php | 56 ----- .../src/Search/AbstractFulltextFilter.php | 25 +++ .../core/src/Search/AbstractRegexGambit.php | 49 ----- .../core/src/Search/AbstractSearcher.php | 57 +++-- .../{Filter => Search}/FilterInterface.php | 4 +- framework/core/src/Search/FilterManager.php | 59 ++++++ framework/core/src/Search/GambitInterface.php | 21 -- framework/core/src/Search/GambitManager.php | 83 -------- .../SearchCriteria.php} | 6 +- .../SearchResults.php} | 4 +- .../core/src/Search/SearchServiceProvider.php | 70 +++++-- framework/core/src/Search/SearchState.php | 72 +++++-- .../ValidateFilterTrait.php | 2 +- .../core/src/User/Filter/EmailFilter.php | 12 +- .../core/src/User/Filter/GroupFilter.php | 10 +- .../core/src/User/Filter/UserFilterer.php | 31 --- .../src/User/Search/Gambit/FulltextGambit.php | 12 +- .../core/src/User/Search/UserSearcher.php | 14 +- .../api/AbstractSerializeControllerTest.php | 4 +- .../api/access_tokens/DeleteTest.php | 2 +- .../api/access_tokens/ListTest.php | 8 +- .../integration/api/discussions/ListTest.php | 196 ------------------ .../tests/integration/api/posts/ListTest.php | 2 +- .../integration/api/users/GroupSearchTest.php | 2 +- .../tests/integration/api/users/ListTest.php | 90 -------- .../integration/extenders/FilterTest.php | 131 ------------ .../extenders/SimpleFlarumSearchTest.php | 53 +++-- 103 files changed, 777 insertions(+), 1677 deletions(-) create mode 100644 extensions/lock/src/Filter/LockedFilter.php delete mode 100644 extensions/lock/src/Query/LockedFilterGambit.php create mode 100644 extensions/sticky/src/Query/StickyFilter.php delete mode 100644 extensions/sticky/src/Query/StickyFilterGambit.php rename extensions/subscriptions/src/{Query/SubscriptionFilterGambit.php => Filter/SubscriptionFilter.php} (52%) rename extensions/suspend/src/Query/{SuspendedFilterGambit.php => SuspendedFilter.php} (51%) rename extensions/tags/src/{Query/TagFilterGambit.php => Filter/TagFilter.php} (71%) delete mode 100644 framework/core/src/Discussion/Filter/DiscussionFilterer.php delete mode 100644 framework/core/src/Extend/Filter.php delete mode 100644 framework/core/src/Filter/AbstractFilterer.php delete mode 100644 framework/core/src/Filter/FilterServiceProvider.php delete mode 100644 framework/core/src/Filter/FilterState.php delete mode 100644 framework/core/src/Group/Filter/GroupFilterer.php create mode 100644 framework/core/src/Group/Filter/GroupSearcher.php rename framework/core/src/Http/Filter/{AccessTokenFilterer.php => AccessTokenSearcher.php} (83%) delete mode 100644 framework/core/src/Post/Filter/PostFilterer.php create mode 100644 framework/core/src/Post/Filter/PostSearcher.php delete mode 100644 framework/core/src/Query/AbstractQueryState.php delete mode 100644 framework/core/src/Query/ApplyQueryParametersTrait.php create mode 100644 framework/core/src/Search/AbstractFulltextFilter.php delete mode 100644 framework/core/src/Search/AbstractRegexGambit.php rename framework/core/src/{Filter => Search}/FilterInterface.php (75%) create mode 100644 framework/core/src/Search/FilterManager.php delete mode 100644 framework/core/src/Search/GambitInterface.php delete mode 100644 framework/core/src/Search/GambitManager.php rename framework/core/src/{Query/QueryCriteria.php => Search/SearchCriteria.php} (92%) rename framework/core/src/{Query/QueryResults.php => Search/SearchResults.php} (92%) rename framework/core/src/{Filter => Search}/ValidateFilterTrait.php (99%) delete mode 100644 framework/core/src/User/Filter/UserFilterer.php delete mode 100644 framework/core/tests/integration/extenders/FilterTest.php diff --git a/extensions/likes/extend.php b/extensions/likes/extend.php index c59a497e58..434df72cdc 100644 --- a/extensions/likes/extend.php +++ b/extensions/likes/extend.php @@ -19,9 +19,9 @@ use Flarum\Likes\Notification\PostLikedBlueprint; use Flarum\Likes\Query\LikedByFilter; use Flarum\Likes\Query\LikedFilter; -use Flarum\Post\Filter\PostFilterer; +use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; -use Flarum\User\Filter\UserFilterer; +use Flarum\User\Search\UserSearcher; use Flarum\User\User; return [ @@ -76,10 +76,10 @@ ->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class) ->subscribe(Listener\SaveLikesToDatabase::class), - (new Extend\Filter(PostFilterer::class)) + (new Extend\SimpleFlarumSearch(PostSearcher::class)) ->addFilter(LikedByFilter::class), - (new Extend\Filter(UserFilterer::class)) + (new Extend\SimpleFlarumSearch(UserSearcher::class)) ->addFilter(LikedFilter::class), (new Extend\Settings()) diff --git a/extensions/likes/src/Query/LikedByFilter.php b/extensions/likes/src/Query/LikedByFilter.php index 13e09ace0c..e31acf507a 100644 --- a/extensions/likes/src/Query/LikedByFilter.php +++ b/extensions/likes/src/Query/LikedByFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Likes\Query; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class LikedByFilter implements FilterInterface { @@ -22,11 +22,11 @@ public function getFilterKey(): string return 'likedBy'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $likedId = $this->asInt($filterValue); + $likedId = $this->asInt($value); - $filterState + $state ->getQuery() ->whereIn('id', function ($query) use ($likedId, $negate) { $query->select('post_id') diff --git a/extensions/likes/src/Query/LikedFilter.php b/extensions/likes/src/Query/LikedFilter.php index 155b4c4441..17524eb668 100644 --- a/extensions/likes/src/Query/LikedFilter.php +++ b/extensions/likes/src/Query/LikedFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Likes\Query; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class LikedFilter implements FilterInterface { @@ -22,11 +22,11 @@ public function getFilterKey(): string return 'liked'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $likedId = $this->asString($filterValue); + $likedId = $this->asString($value); - $filterState + $state ->getQuery() ->whereIn('id', function ($query) use ($likedId) { $query->select('user_id') diff --git a/extensions/lock/extend.php b/extensions/lock/extend.php index 9cf4429c6e..6d0ab38a0b 100644 --- a/extensions/lock/extend.php +++ b/extensions/lock/extend.php @@ -11,16 +11,15 @@ use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving; -use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; use Flarum\Lock\Access; use Flarum\Lock\Event\DiscussionWasLocked; use Flarum\Lock\Event\DiscussionWasUnlocked; +use Flarum\Lock\Filter\LockedFilter; use Flarum\Lock\Listener; use Flarum\Lock\Notification\DiscussionLockedBlueprint; use Flarum\Lock\Post\DiscussionLockedPost; -use Flarum\Lock\Query\LockedFilterGambit; return [ (new Extend\Frontend('forum')) @@ -57,9 +56,6 @@ (new Extend\Policy()) ->modelPolicy(Discussion::class, Access\DiscussionPolicy::class), - (new Extend\Filter(DiscussionFilterer::class)) - ->addFilter(LockedFilterGambit::class), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addGambit(LockedFilterGambit::class), + ->addFilter(LockedFilter::class), ]; diff --git a/extensions/lock/js/src/common/query/discussions/LockedGambit.ts b/extensions/lock/js/src/common/query/discussions/LockedGambit.ts index aa2f8d08ce..b3639fd35d 100644 --- a/extensions/lock/js/src/common/query/discussions/LockedGambit.ts +++ b/extensions/lock/js/src/common/query/discussions/LockedGambit.ts @@ -12,4 +12,12 @@ export default class LockedGambit implements IGambit { [key]: true, }; } + + filterKey(): string { + return 'locked'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}is:locked`; + } } diff --git a/extensions/lock/src/Filter/LockedFilter.php b/extensions/lock/src/Filter/LockedFilter.php new file mode 100644 index 0000000000..02cdf411ef --- /dev/null +++ b/extensions/lock/src/Filter/LockedFilter.php @@ -0,0 +1,32 @@ +constrain($state->getQuery(), $negate); + } + + protected function constrain(Builder $query, bool $negate): void + { + $query->where('is_locked', ! $negate); + } +} diff --git a/extensions/lock/src/Query/LockedFilterGambit.php b/extensions/lock/src/Query/LockedFilterGambit.php deleted file mode 100644 index 2c3d14ad98..0000000000 --- a/extensions/lock/src/Query/LockedFilterGambit.php +++ /dev/null @@ -1,44 +0,0 @@ -constrain($search->getQuery(), $negate); - } - - public function getFilterKey(): string - { - return 'locked'; - } - - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void - { - $this->constrain($filterState->getQuery(), $negate); - } - - protected function constrain(Builder $query, bool $negate): void - { - $query->where('is_locked', ! $negate); - } -} diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 5f9c568b8c..20c7798eff 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -24,7 +24,7 @@ use Flarum\Post\Event\Posted; use Flarum\Post\Event\Restored; use Flarum\Post\Event\Revised; -use Flarum\Post\Filter\PostFilterer; +use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; use Flarum\Tags\Api\Serializer\TagSerializer; use Flarum\Tags\Tag; @@ -115,7 +115,7 @@ ->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class) ->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class), - (new Extend\Filter(PostFilterer::class)) + (new Extend\SimpleFlarumSearch(PostSearcher::class)) ->addFilter(Filter\MentionedFilter::class) ->addFilter(Filter\MentionedPostFilter::class), diff --git a/extensions/mentions/src/Filter/MentionedFilter.php b/extensions/mentions/src/Filter/MentionedFilter.php index a0b470d932..af0d96ba33 100644 --- a/extensions/mentions/src/Filter/MentionedFilter.php +++ b/extensions/mentions/src/Filter/MentionedFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Mentions\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class MentionedFilter implements FilterInterface { @@ -22,11 +22,11 @@ public function getFilterKey(): string return 'mentioned'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $mentionedId = $this->asInt($filterValue); + $mentionedId = $this->asInt($value); - $filterState + $state ->getQuery() ->join('post_mentions_user', 'posts.id', '=', 'post_mentions_user.post_id') ->where('post_mentions_user.mentions_user_id', $negate ? '!=' : '=', $mentionedId); diff --git a/extensions/mentions/src/Filter/MentionedPostFilter.php b/extensions/mentions/src/Filter/MentionedPostFilter.php index da46bbe2c9..65bf8d2e3a 100644 --- a/extensions/mentions/src/Filter/MentionedPostFilter.php +++ b/extensions/mentions/src/Filter/MentionedPostFilter.php @@ -9,8 +9,8 @@ namespace Flarum\Mentions\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; class MentionedPostFilter implements FilterInterface { @@ -19,11 +19,11 @@ public function getFilterKey(): string return 'mentionedPost'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $mentionedId = trim($filterValue, '"'); + $mentionedId = trim($value, '"'); - $filterState + $state ->getQuery() ->join('post_mentions_post', 'posts.id', '=', 'post_mentions_post.post_id') ->where('post_mentions_post.mentions_post_id', $negate ? '!=' : '=', $mentionedId); diff --git a/extensions/nicknames/extend.php b/extensions/nicknames/extend.php index 99fb7ab7bf..853499e099 100644 --- a/extensions/nicknames/extend.php +++ b/extensions/nicknames/extend.php @@ -52,7 +52,7 @@ ->configure(AddNicknameValidation::class), (new Extend\SimpleFlarumSearch(UserSearcher::class)) - ->setFullTextGambit(NicknameFullTextGambit::class), + ->setFullTextFilter(NicknameFullTextGambit::class), (new Extend\Policy()) ->modelPolicy(User::class, UserPolicy::class), diff --git a/extensions/nicknames/src/NicknameFullTextGambit.php b/extensions/nicknames/src/NicknameFullTextGambit.php index 1079e865f0..2dc8e3ccc4 100644 --- a/extensions/nicknames/src/NicknameFullTextGambit.php +++ b/extensions/nicknames/src/NicknameFullTextGambit.php @@ -9,19 +9,12 @@ namespace Flarum\Nicknames; -/* - * This file is part of Flarum. - * - * For detailed copyright and license information, please view the - * LICENSE file that was distributed with this source code. - */ - -use Flarum\Search\GambitInterface; +use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\SearchState; use Flarum\User\UserRepository; use Illuminate\Database\Eloquent\Builder; -class NicknameFullTextGambit implements GambitInterface +class NicknameFullTextGambit extends AbstractFulltextFilter { public function __construct( protected UserRepository $users @@ -37,14 +30,12 @@ private function getUserSearchSubQuery(string $searchValue): Builder ->orWhere('nickname', 'like', "{$searchValue}%"); } - public function apply(SearchState $search, string $bit): bool + public function search(SearchState $state, string $query): void { - $search->getQuery() + $state->getQuery() ->whereIn( 'id', - $this->getUserSearchSubQuery($bit) + $this->getUserSearchSubQuery($query) ); - - return true; } } diff --git a/extensions/sticky/extend.php b/extensions/sticky/extend.php index 3a9ff0bc22..61c55c9123 100644 --- a/extensions/sticky/extend.php +++ b/extensions/sticky/extend.php @@ -11,7 +11,6 @@ use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving; -use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; use Flarum\Sticky\Event\DiscussionWasStickied; @@ -20,7 +19,7 @@ use Flarum\Sticky\Listener\SaveStickyToDatabase; use Flarum\Sticky\PinStickiedDiscussionsToTop; use Flarum\Sticky\Post\DiscussionStickiedPost; -use Flarum\Sticky\Query\StickyFilterGambit; +use Flarum\Sticky\Query\StickyFilter; return [ (new Extend\Frontend('forum')) @@ -54,10 +53,7 @@ ->listen(DiscussionWasStickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasStickied']) ->listen(DiscussionWasUnstickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasUnstickied']), - (new Extend\Filter(DiscussionFilterer::class)) - ->addFilter(StickyFilterGambit::class) - ->addFilterMutator(PinStickiedDiscussionsToTop::class), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addGambit(StickyFilterGambit::class), + ->addFilter(StickyFilter::class) + ->addSearchMutator(PinStickiedDiscussionsToTop::class), ]; diff --git a/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts b/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts index 1c9ed860ad..d1ab9f688c 100644 --- a/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts +++ b/extensions/sticky/js/src/common/query/discussions/StickyGambit.ts @@ -12,4 +12,12 @@ export default class StickyGambit implements IGambit { [key]: true, }; } + + filterKey(): string { + return 'sticky'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}is:sticky`; + } } diff --git a/extensions/sticky/src/PinStickiedDiscussionsToTop.php b/extensions/sticky/src/PinStickiedDiscussionsToTop.php index b16eaefa8b..3b4e3a625a 100755 --- a/extensions/sticky/src/PinStickiedDiscussionsToTop.php +++ b/extensions/sticky/src/PinStickiedDiscussionsToTop.php @@ -9,23 +9,23 @@ namespace Flarum\Sticky; -use Flarum\Filter\FilterState; -use Flarum\Query\QueryCriteria; -use Flarum\Tags\Query\TagFilterGambit; +use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchState; +use Flarum\Tags\Filter\TagFilter; class PinStickiedDiscussionsToTop { - public function __invoke(FilterState $filterState, QueryCriteria $criteria): void + public function __invoke(SearchState $state, SearchCriteria $criteria): void { - if ($criteria->sortIsDefault) { - $query = $filterState->getQuery(); + if ($criteria->sortIsDefault && ! $state->isFulltextSearch()) { + $query = $state->getQuery(); // If we are viewing a specific tag, then pin all stickied // discussions to the top no matter what. - $filters = $filterState->getActiveFilters(); + $filters = $state->getActiveFilters(); if ($count = count($filters)) { - if ($count === 1 && $filters[0] instanceof TagFilterGambit) { + if ($count === 1 && $filters[0] instanceof TagFilter) { if (! is_array($query->orders)) { $query->orders = []; } @@ -51,14 +51,14 @@ public function __invoke(FilterState $filterState, QueryCriteria $criteria): voi ->selectRaw('1') ->from('discussion_user as sticky') ->whereColumn('sticky.discussion_id', 'id') - ->where('sticky.user_id', '=', $filterState->getActor()->id) + ->where('sticky.user_id', '=', $state->getActor()->id) ->whereColumn('sticky.last_read_post_number', '>=', 'last_post_number'); // Add the bindings manually (rather than as the second // argument in orderByRaw) for now due to a bug in Laravel which // would add the bindings in the wrong order. $query->orderByRaw('is_sticky and not exists ('.$read->toSql().') and last_posted_at > ? desc') - ->addBinding(array_merge($read->getBindings(), [$filterState->getActor()->marked_all_as_read_at ?: 0]), 'union'); + ->addBinding(array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]), 'union'); $query->unionOrders = array_merge($query->unionOrders, $query->orders); $query->unionLimit = $query->limit; diff --git a/extensions/sticky/src/Query/StickyFilter.php b/extensions/sticky/src/Query/StickyFilter.php new file mode 100644 index 0000000000..8a498e3e78 --- /dev/null +++ b/extensions/sticky/src/Query/StickyFilter.php @@ -0,0 +1,32 @@ +constrain($state->getQuery(), $negate); + } + + protected function constrain(Builder $query, bool $negate): void + { + $query->where('is_sticky', ! $negate); + } +} diff --git a/extensions/sticky/src/Query/StickyFilterGambit.php b/extensions/sticky/src/Query/StickyFilterGambit.php deleted file mode 100644 index e95ddd0507..0000000000 --- a/extensions/sticky/src/Query/StickyFilterGambit.php +++ /dev/null @@ -1,44 +0,0 @@ -constrain($search->getQuery(), $negate); - } - - public function getFilterKey(): string - { - return 'sticky'; - } - - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void - { - $this->constrain($filterState->getQuery(), $negate); - } - - protected function constrain(Builder $query, bool $negate): void - { - $query->where('is_sticky', ! $negate); - } -} diff --git a/extensions/subscriptions/extend.php b/extensions/subscriptions/extend.php index 5a8974e588..36942714ef 100644 --- a/extensions/subscriptions/extend.php +++ b/extensions/subscriptions/extend.php @@ -12,7 +12,6 @@ use Flarum\Approval\Event\PostWasApproved; use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving; -use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Discussion\UserState; use Flarum\Extend; @@ -20,11 +19,11 @@ use Flarum\Post\Event\Hidden; use Flarum\Post\Event\Posted; use Flarum\Post\Event\Restored; +use Flarum\Subscriptions\Filter\SubscriptionFilter; use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage; use Flarum\Subscriptions\Listener; use Flarum\Subscriptions\Notification\FilterVisiblePostsBeforeSending; use Flarum\Subscriptions\Notification\NewPostBlueprint; -use Flarum\Subscriptions\Query\SubscriptionFilterGambit; use Flarum\User\User; return [ @@ -70,12 +69,9 @@ ->listen(Deleted::class, Listener\DeleteNotificationWhenPostIsHiddenOrDeleted::class) ->listen(Posted::class, Listener\FollowAfterReply::class), - (new Extend\Filter(DiscussionFilterer::class)) - ->addFilter(SubscriptionFilterGambit::class) - ->addFilterMutator(HideIgnoredFromAllDiscussionsPage::class), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addGambit(SubscriptionFilterGambit::class), + ->addFilter(SubscriptionFilter::class) + ->addSearchMutator(HideIgnoredFromAllDiscussionsPage::class), (new Extend\User()) ->registerPreference('flarum-subscriptions.notify_for_all_posts', 'boolval', false), diff --git a/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts b/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts index a702a43eaf..6d1a137078 100644 --- a/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts +++ b/extensions/subscriptions/js/src/common/query/discussions/SubscriptionGambit.ts @@ -12,4 +12,12 @@ export default class SubscriptionGambit implements IGambit { subscription: type, }; } + + filterKey(): string { + return 'subscription'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}is:${value}`; + } } diff --git a/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js b/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js index ed950fff1e..d9a29d23b0 100644 --- a/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js +++ b/extensions/subscriptions/js/src/forum/addSubscriptionFilter.js @@ -35,12 +35,7 @@ export default function addSubscriptionFilter() { extend(DiscussionListState.prototype, 'requestParams', function (params) { if (this.params.onFollowing) { params.filter ||= {}; - - if (params.filter.q) { - params.filter.q += ' is:following'; - } else { - params.filter.subscription = 'following'; - } + params.filter.subscription = 'following'; } }); } diff --git a/extensions/subscriptions/src/Query/SubscriptionFilterGambit.php b/extensions/subscriptions/src/Filter/SubscriptionFilter.php similarity index 52% rename from extensions/subscriptions/src/Query/SubscriptionFilterGambit.php rename to extensions/subscriptions/src/Filter/SubscriptionFilter.php index fc3814c08f..2d3224a9fc 100644 --- a/extensions/subscriptions/src/Query/SubscriptionFilterGambit.php +++ b/extensions/subscriptions/src/Filter/SubscriptionFilter.php @@ -7,42 +7,30 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Subscriptions\Query; +namespace Flarum\Subscriptions\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; -use Flarum\Search\AbstractRegexGambit; +use Flarum\Search\FilterInterface; use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Flarum\User\User; use Illuminate\Database\Query\Builder; -class SubscriptionFilterGambit extends AbstractRegexGambit implements FilterInterface +class SubscriptionFilter implements FilterInterface { use ValidateFilterTrait; - protected function getGambitPattern(): string - { - return 'is:(follow|ignor)(?:ing|ed)'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $search->getActor(), $matches[1], $negate); - } - public function getFilterKey(): string { return 'subscription'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $filterValue = $this->asString($filterValue); + $value = $this->asString($value); - preg_match('/^'.$this->getGambitPattern().'$/i', 'is:'.$filterValue, $matches); + preg_match('/^(follow|ignor)(?:ing|ed)$/i', $value, $matches); - $this->constrain($filterState->getQuery(), $filterState->getActor(), $matches[1], $negate); + $this->constrain($state->getQuery(), $state->getActor(), $matches[1], $negate); } protected function constrain(Builder $query, User $actor, string $subscriptionType, bool $negate): void diff --git a/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php b/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php index 37b38e7778..63c2eeffd2 100644 --- a/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php +++ b/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php @@ -9,18 +9,18 @@ namespace Flarum\Subscriptions; -use Flarum\Filter\FilterState; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchState; class HideIgnoredFromAllDiscussionsPage { - public function __invoke(FilterState $filterState, QueryCriteria $criteria): void + public function __invoke(SearchState $state, SearchCriteria $criteria): void { // We only want to hide on the "all discussions" page. - if (count($filterState->getActiveFilters()) === 0) { + if (count($state->getActiveFilters()) === 0 && ! $state->isFulltextSearch()) { // TODO: might be better as `id IN (subquery)`? - $actor = $filterState->getActor(); - $filterState->getQuery()->whereNotExists(function ($query) use ($actor) { + $actor = $state->getActor(); + $state->getQuery()->whereNotExists(function ($query) use ($actor) { $query->selectRaw(1) ->from('discussion_user') ->whereColumn('discussions.id', 'discussion_id') diff --git a/extensions/suspend/extend.php b/extensions/suspend/extend.php index fe924d00b7..935ce1983b 100644 --- a/extensions/suspend/extend.php +++ b/extensions/suspend/extend.php @@ -17,10 +17,9 @@ use Flarum\Suspend\Listener; use Flarum\Suspend\Notification\UserSuspendedBlueprint; use Flarum\Suspend\Notification\UserUnsuspendedBlueprint; -use Flarum\Suspend\Query\SuspendedFilterGambit; +use Flarum\Suspend\Query\SuspendedFilter; use Flarum\Suspend\RevokeAccessFromSuspendedUsers; use Flarum\User\Event\Saving; -use Flarum\User\Filter\UserFilterer; use Flarum\User\Search\UserSearcher; use Flarum\User\User; @@ -58,11 +57,8 @@ (new Extend\User()) ->permissionGroups(RevokeAccessFromSuspendedUsers::class), - (new Extend\Filter(UserFilterer::class)) - ->addFilter(SuspendedFilterGambit::class), - (new Extend\SimpleFlarumSearch(UserSearcher::class)) - ->addGambit(SuspendedFilterGambit::class), + ->addFilter(SuspendedFilter::class), (new Extend\View()) ->namespace('flarum-suspend', __DIR__.'/views'), diff --git a/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts b/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts index 2ef338f590..f8c2c4c6c0 100644 --- a/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts +++ b/extensions/suspend/js/src/common/query/users/SuspendedGambit.ts @@ -12,4 +12,12 @@ export default class SuspendedGambit implements IGambit { [key]: true, }; } + + filterKey(): string { + return 'suspended'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}is:suspended`; + } } diff --git a/extensions/suspend/src/Query/SuspendedFilterGambit.php b/extensions/suspend/src/Query/SuspendedFilter.php similarity index 51% rename from extensions/suspend/src/Query/SuspendedFilterGambit.php rename to extensions/suspend/src/Query/SuspendedFilter.php index 24b192c4d0..b55c1d66aa 100644 --- a/extensions/suspend/src/Query/SuspendedFilterGambit.php +++ b/extensions/suspend/src/Query/SuspendedFilter.php @@ -10,52 +10,31 @@ namespace Flarum\Suspend\Query; use Carbon\Carbon; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Search\AbstractRegexGambit; +use Flarum\Search\FilterInterface; use Flarum\Search\SearchState; use Flarum\User\Guest; use Flarum\User\UserRepository; use Illuminate\Database\Query\Builder; -class SuspendedFilterGambit extends AbstractRegexGambit implements FilterInterface +class SuspendedFilter implements FilterInterface { public function __construct( protected UserRepository $users ) { } - protected function getGambitPattern(): string - { - return 'is:suspended'; - } - - public function apply(SearchState $search, string $bit): bool - { - if (! $search->getActor()->can('suspend', new Guest())) { - return false; - } - - return parent::apply($search, $bit); - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $negate); - } - public function getFilterKey(): string { return 'suspended'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - if (! $filterState->getActor()->can('suspend', new Guest())) { + if (! $state->getActor()->can('suspend', new Guest())) { return; } - $this->constrain($filterState->getQuery(), $negate); + $this->constrain($state->getQuery(), $negate); } protected function constrain(Builder $query, bool $negate): void diff --git a/extensions/tags/extend.php b/extensions/tags/extend.php index a216c68d9e..c119b98610 100644 --- a/extensions/tags/extend.php +++ b/extensions/tags/extend.php @@ -13,12 +13,11 @@ use Flarum\Api\Serializer\ForumSerializer; use Flarum\Discussion\Discussion; use Flarum\Discussion\Event\Saving; -use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; use Flarum\Flags\Api\Controller\ListFlagsController; use Flarum\Http\RequestUtil; -use Flarum\Post\Filter\PostFilterer; +use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; use Flarum\Tags\Access; use Flarum\Tags\Api\Controller; @@ -27,10 +26,10 @@ use Flarum\Tags\Event\DiscussionWasTagged; use Flarum\Tags\Filter\HideHiddenTagsFromAllDiscussionsPage; use Flarum\Tags\Filter\PostTagFilter; +use Flarum\Tags\Filter\TagFilter; use Flarum\Tags\Listener; use Flarum\Tags\LoadForumTagsRelationship; use Flarum\Tags\Post\DiscussionTaggedPost; -use Flarum\Tags\Query\TagFilterGambit; use Flarum\Tags\Search\Gambit\FulltextGambit; use Flarum\Tags\Search\TagSearcher; use Flarum\Tags\Tag; @@ -135,18 +134,15 @@ ->listen(DiscussionWasTagged::class, Listener\CreatePostWhenTagsAreChanged::class) ->subscribe(Listener\UpdateTagMetadata::class), - (new Extend\Filter(PostFilterer::class)) + (new Extend\SimpleFlarumSearch(PostSearcher::class)) ->addFilter(PostTagFilter::class), - (new Extend\Filter(DiscussionFilterer::class)) - ->addFilter(TagFilterGambit::class) - ->addFilterMutator(HideHiddenTagsFromAllDiscussionsPage::class), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addGambit(TagFilterGambit::class), + ->addFilter(TagFilter::class) + ->addSearchMutator(HideHiddenTagsFromAllDiscussionsPage::class), (new Extend\SimpleFlarumSearch(TagSearcher::class)) - ->setFullTextGambit(FullTextGambit::class), + ->setFullTextFilter(FullTextGambit::class), (new Extend\ModelUrl(Tag::class)) ->addSlugDriver('default', Utf8SlugDriver::class), diff --git a/extensions/tags/js/src/common/query/discussions/TagGambit.ts b/extensions/tags/js/src/common/query/discussions/TagGambit.ts index fae0333354..0cae0b17bc 100644 --- a/extensions/tags/js/src/common/query/discussions/TagGambit.ts +++ b/extensions/tags/js/src/common/query/discussions/TagGambit.ts @@ -12,4 +12,12 @@ export default class TagGambit implements IGambit { [key]: matches[1].split(','), }; } + + filterKey(): string { + return 'tag'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}tag:${value}`; + } } diff --git a/extensions/tags/js/src/forum/addTagFilter.tsx b/extensions/tags/js/src/forum/addTagFilter.tsx index b2742bdb72..55dc73577c 100644 --- a/extensions/tags/js/src/forum/addTagFilter.tsx +++ b/extensions/tags/js/src/forum/addTagFilter.tsx @@ -117,14 +117,8 @@ export default function addTagFilter() { } if (this.params.tags) { - const filter = params.filter ?? {}; - filter.tag = this.params.tags; - // TODO: replace this with a more robust system. - const q = filter.q; - if (q) { - filter.q = `${q} tag:${this.params.tags}`; - } - params.filter = filter; + params.filter ||= {}; + params.filter.tag = this.params.tags; } }); } diff --git a/extensions/tags/src/Api/Controller/ListTagsController.php b/extensions/tags/src/Api/Controller/ListTagsController.php index 85400f3c5e..511c887dd4 100644 --- a/extensions/tags/src/Api/Controller/ListTagsController.php +++ b/extensions/tags/src/Api/Controller/ListTagsController.php @@ -12,7 +12,7 @@ use Flarum\Api\Controller\AbstractListController; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; use Flarum\Tags\Api\Serializer\TagSerializer; use Flarum\Tags\Search\TagSearcher; use Flarum\Tags\TagRepository; @@ -53,7 +53,7 @@ protected function data(ServerRequestInterface $request, Document $document): it } if (array_key_exists('q', $filters)) { - $results = $this->searcher->search(new QueryCriteria($actor, $filters), $limit, $offset); + $results = $this->searcher->search(new SearchCriteria($actor, $filters), $limit, $offset); $tags = $results->getResults(); $document->addPaginationLinks( diff --git a/extensions/tags/src/Filter/HideHiddenTagsFromAllDiscussionsPage.php b/extensions/tags/src/Filter/HideHiddenTagsFromAllDiscussionsPage.php index b4ae373a08..bce8df1710 100644 --- a/extensions/tags/src/Filter/HideHiddenTagsFromAllDiscussionsPage.php +++ b/extensions/tags/src/Filter/HideHiddenTagsFromAllDiscussionsPage.php @@ -9,19 +9,19 @@ namespace Flarum\Tags\Filter; -use Flarum\Filter\FilterState; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchState; use Flarum\Tags\Tag; class HideHiddenTagsFromAllDiscussionsPage { - public function __invoke(FilterState $filter, QueryCriteria $queryCriteria): void + public function __invoke(SearchState $state, SearchCriteria $queryCriteria): void { - if (count($filter->getActiveFilters()) > 0) { + if (count($state->getActiveFilters()) > 0 || $state->isFulltextSearch()) { return; } - $filter->getQuery()->whereNotIn('discussions.id', function ($query) { + $state->getQuery()->whereNotIn('discussions.id', function ($query) { return $query->select('discussion_id') ->from('discussion_tag') ->whereIn('tag_id', Tag::where('is_hidden', 1)->pluck('id')); diff --git a/extensions/tags/src/Filter/PostTagFilter.php b/extensions/tags/src/Filter/PostTagFilter.php index 39709c2e12..edba1ca29f 100644 --- a/extensions/tags/src/Filter/PostTagFilter.php +++ b/extensions/tags/src/Filter/PostTagFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Tags\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class PostTagFilter implements FilterInterface { @@ -22,11 +22,11 @@ public function getFilterKey(): string return 'tag'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $ids = $this->asIntArray($filterValue); + $ids = $this->asIntArray($value); - $filterState->getQuery() + $state->getQuery() ->join('discussion_tag', 'discussion_tag.discussion_id', '=', 'posts.discussion_id') ->whereIn('discussion_tag.tag_id', $ids, 'and', $negate); } diff --git a/extensions/tags/src/Query/TagFilterGambit.php b/extensions/tags/src/Filter/TagFilter.php similarity index 71% rename from extensions/tags/src/Query/TagFilterGambit.php rename to extensions/tags/src/Filter/TagFilter.php index 159bb26810..4e6d15c374 100644 --- a/extensions/tags/src/Query/TagFilterGambit.php +++ b/extensions/tags/src/Filter/TagFilter.php @@ -7,20 +7,18 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Tags\Query; +namespace Flarum\Tags\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; use Flarum\Http\SlugManager; -use Flarum\Search\AbstractRegexGambit; +use Flarum\Search\FilterInterface; use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Flarum\Tags\Tag; use Flarum\User\User; use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Query\Builder; -class TagFilterGambit extends AbstractRegexGambit implements FilterInterface +class TagFilter implements FilterInterface { use ValidateFilterTrait; @@ -29,24 +27,14 @@ public function __construct( ) { } - protected function getGambitPattern(): string - { - return 'tag:(.+)'; - } - - protected function conditions(SearchState $search, array $matches, bool $negate): void - { - $this->constrain($search->getQuery(), $matches[1], $negate, $search->getActor()); - } - public function getFilterKey(): string { return 'tag'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $this->constrain($filterState->getQuery(), $filterValue, $negate, $filterState->getActor()); + $this->constrain($state->getQuery(), $value, $negate, $state->getActor()); } protected function constrain(Builder $query, string|array $rawSlugs, bool $negate, User $actor): void diff --git a/extensions/tags/src/Search/Gambit/FulltextGambit.php b/extensions/tags/src/Search/Gambit/FulltextGambit.php index ad8d59ca34..903f648c2d 100644 --- a/extensions/tags/src/Search/Gambit/FulltextGambit.php +++ b/extensions/tags/src/Search/Gambit/FulltextGambit.php @@ -9,12 +9,12 @@ namespace Flarum\Tags\Search\Gambit; -use Flarum\Search\GambitInterface; +use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\SearchState; use Flarum\Tags\TagRepository; use Illuminate\Database\Eloquent\Builder; -class FulltextGambit implements GambitInterface +class FulltextGambit extends AbstractFulltextFilter { public function __construct( protected TagRepository $tags @@ -30,14 +30,12 @@ private function getTagSearchSubQuery(string $searchValue): Builder ->orWhere('slug', 'like', "$searchValue%"); } - public function apply(SearchState $search, string $bit): bool + public function search(SearchState $state, string $query): void { - $search->getQuery() + $state->getQuery() ->whereIn( 'id', - $this->getTagSearchSubQuery($bit) + $this->getTagSearchSubQuery($query) ); - - return true; } } diff --git a/extensions/tags/src/Search/TagSearcher.php b/extensions/tags/src/Search/TagSearcher.php index b92ef5960e..bfa9b1f8e8 100644 --- a/extensions/tags/src/Search/TagSearcher.php +++ b/extensions/tags/src/Search/TagSearcher.php @@ -10,23 +10,14 @@ namespace Flarum\Tags\Search; use Flarum\Search\AbstractSearcher; -use Flarum\Search\GambitManager; -use Flarum\Tags\TagRepository; +use Flarum\Tags\Tag; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; class TagSearcher extends AbstractSearcher { - public function __construct( - protected TagRepository $tags, - GambitManager $gambits, - array $searchMutators - ) { - parent::__construct($gambits, $searchMutators); - } - protected function getQuery(User $actor): Builder { - return $this->tags->query()->whereVisibleTo($actor); + return Tag::whereVisibleTo($actor)->select('tags.*'); } } diff --git a/framework/core/js/src/common/GambitManager.ts b/framework/core/js/src/common/GambitManager.ts index 11cc0569ec..10774c0920 100644 --- a/framework/core/js/src/common/GambitManager.ts +++ b/framework/core/js/src/common/GambitManager.ts @@ -48,4 +48,25 @@ export default class GambitManager { return filter; } + + public from(type: string, q: string, filter: Record): string { + const gambits = this.gambits[type] || []; + + if (gambits.length === 0) return q; + + Object.keys(filter).forEach((key) => { + for (const gambitClass of gambits) { + const gambit = new gambitClass(); + const negate = key[0] === '-'; + + if (negate) key = key.substring(1); + + if (gambit.filterKey() !== key) continue; + + q += ` ${gambit.fromFilter(filter[key], negate)}`; + } + }); + + return q; + } } diff --git a/framework/core/js/src/common/query/IGambit.ts b/framework/core/js/src/common/query/IGambit.ts index 719c319689..a10e8bdb11 100644 --- a/framework/core/js/src/common/query/IGambit.ts +++ b/framework/core/js/src/common/query/IGambit.ts @@ -1,4 +1,6 @@ export default interface IGambit { pattern(): string; toFilter(matches: string[], negate: boolean): Record; + filterKey(): string; + fromFilter(value: string, negate: boolean): string; } diff --git a/framework/core/js/src/common/query/discussions/AuthorGambit.ts b/framework/core/js/src/common/query/discussions/AuthorGambit.ts index 367686dff2..f38f71cda6 100644 --- a/framework/core/js/src/common/query/discussions/AuthorGambit.ts +++ b/framework/core/js/src/common/query/discussions/AuthorGambit.ts @@ -12,4 +12,12 @@ export default class AuthorGambit implements IGambit { [key]: matches[1].split(','), }; } + + filterKey(): string { + return 'author'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}author:${value}`; + } } diff --git a/framework/core/js/src/common/query/discussions/CreatedGambit.ts b/framework/core/js/src/common/query/discussions/CreatedGambit.ts index c1eed2809d..1b84ed044d 100644 --- a/framework/core/js/src/common/query/discussions/CreatedGambit.ts +++ b/framework/core/js/src/common/query/discussions/CreatedGambit.ts @@ -2,19 +2,22 @@ import IGambit from '../IGambit'; export default class CreatedGambit implements IGambit { pattern(): string { - return 'created:(\\d{4}\\-\\d\\d\\-\\d\\d)(?:\\.\\.(\\d{4}\\-\\d\\d\\-\\d\\d))?'; + return 'created:(\\d{4}\\-\\d\\d\\-\\d\\d(?:\\.\\.\\d{4}\\-\\d\\d\\-\\d\\d))?'; } toFilter(matches: string[], negate: boolean): Record { const key = (negate ? '-' : '') + 'created'; return { - [key]: matches[2] - ? { - from: matches[1], - to: matches[2], - } - : matches[1], + [key]: matches[1], }; } + + filterKey(): string { + return 'created'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}created:${value}`; + } } diff --git a/framework/core/js/src/common/query/discussions/HiddenGambit.ts b/framework/core/js/src/common/query/discussions/HiddenGambit.ts index 2e2157307b..fb0f448df7 100644 --- a/framework/core/js/src/common/query/discussions/HiddenGambit.ts +++ b/framework/core/js/src/common/query/discussions/HiddenGambit.ts @@ -12,4 +12,12 @@ export default class HiddenGambit implements IGambit { [key]: true, }; } + + filterKey(): string { + return 'hidden'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}is:hidden`; + } } diff --git a/framework/core/js/src/common/query/discussions/UnreadGambit.ts b/framework/core/js/src/common/query/discussions/UnreadGambit.ts index ac3fc920e8..9466b76be4 100644 --- a/framework/core/js/src/common/query/discussions/UnreadGambit.ts +++ b/framework/core/js/src/common/query/discussions/UnreadGambit.ts @@ -12,4 +12,12 @@ export default class UnreadGambit implements IGambit { [key]: true, }; } + + filterKey(): string { + return 'unread'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}is:unread`; + } } diff --git a/framework/core/js/src/common/query/users/EmailGambit.ts b/framework/core/js/src/common/query/users/EmailGambit.ts index ad851290f5..5a9d432abd 100644 --- a/framework/core/js/src/common/query/users/EmailGambit.ts +++ b/framework/core/js/src/common/query/users/EmailGambit.ts @@ -12,4 +12,12 @@ export default class EmailGambit implements IGambit { [key]: matches[1], }; } + + filterKey(): string { + return 'email'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}email:${value}`; + } } diff --git a/framework/core/js/src/common/query/users/GroupGambit.ts b/framework/core/js/src/common/query/users/GroupGambit.ts index 279b895bc2..6293367198 100644 --- a/framework/core/js/src/common/query/users/GroupGambit.ts +++ b/framework/core/js/src/common/query/users/GroupGambit.ts @@ -12,4 +12,12 @@ export default class GroupGambit implements IGambit { [key]: matches[1].split(','), }; } + + filterKey(): string { + return 'group'; + } + + fromFilter(value: string, negate: boolean): string { + return `${negate ? '-' : ''}group:${value}`; + } } diff --git a/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx b/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx index 3a42945c65..84de850396 100644 --- a/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx +++ b/framework/core/js/src/forum/components/DiscussionsSearchSource.tsx @@ -48,10 +48,15 @@ export default class DiscussionsSearchSource implements SearchSource { ); }) as Array; + const filter = app.store.gambits.apply('discussions', { q: query }); + const q = filter.q || null; + + delete filter.q; + return [
  • {app.translator.trans('core.forum.search.discussions_heading')}
  • ,
  • - + {app.translator.trans('core.forum.search.all_discussions_button', { query })}
  • , diff --git a/framework/core/js/src/forum/states/GlobalSearchState.ts b/framework/core/js/src/forum/states/GlobalSearchState.ts index f728dfe22b..b4ca4e5c26 100644 --- a/framework/core/js/src/forum/states/GlobalSearchState.ts +++ b/framework/core/js/src/forum/states/GlobalSearchState.ts @@ -36,7 +36,14 @@ export default class GlobalSearchState extends SearchState { * @inheritdoc */ getInitialSearch(): string { - return this.currPageProvidesSearch() ? this.params().q : ''; + return this.currPageProvidesSearch() ? this.searchToQuery() : ''; + } + + private searchToQuery(): string { + const q = this.params().q || ''; + const filter = this.params().filter || {}; + + return app.store.gambits.from('users', app.store.gambits.from('discussions', q, filter), filter).trim(); } /** @@ -57,7 +64,7 @@ export default class GlobalSearchState extends SearchState { * 'x' is clicked in the search box in the header. */ protected clearInitialSearch() { - const { q, ...params } = this.params(); + const { q, filter, ...params } = this.params(); setRouteWithForcedRefresh(app.route(app.current.get('routeName'), params)); } @@ -71,6 +78,9 @@ export default class GlobalSearchState extends SearchState { return { sort: m.route.param('sort'), q: m.route.param('q'), + // Objects must be copied, otherwise they are passed by reference. + // Which could end up undesirably modifying the mithril route params. + filter: Object.assign({}, m.route.param('filter')), }; } @@ -80,8 +90,6 @@ export default class GlobalSearchState extends SearchState { params(): SearchParams { const params = this.stickyParams(); - params.filter = m.route.param('filter'); - return params; } diff --git a/framework/core/src/Api/Controller/ListAccessTokensController.php b/framework/core/src/Api/Controller/ListAccessTokensController.php index 5b6b425ba0..8adec05452 100644 --- a/framework/core/src/Api/Controller/ListAccessTokensController.php +++ b/framework/core/src/Api/Controller/ListAccessTokensController.php @@ -10,10 +10,10 @@ namespace Flarum\Api\Controller; use Flarum\Api\Serializer\AccessTokenSerializer; -use Flarum\Http\Filter\AccessTokenFilterer; +use Flarum\Http\Filter\AccessTokenSearcher; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -23,7 +23,7 @@ class ListAccessTokensController extends AbstractListController public function __construct( protected UrlGenerator $url, - protected AccessTokenFilterer $filterer + protected AccessTokenSearcher $searcher ) { } @@ -37,7 +37,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $limit = $this->extractLimit($request); $filter = $this->extractFilter($request); - $tokens = $this->filterer->filter(new QueryCriteria($actor, $filter), $limit, $offset); + $tokens = $this->searcher->search(new SearchCriteria($actor, $filter), $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('access-tokens.index'), diff --git a/framework/core/src/Api/Controller/ListDiscussionsController.php b/framework/core/src/Api/Controller/ListDiscussionsController.php index ca34a1881d..b8eda4fa91 100644 --- a/framework/core/src/Api/Controller/ListDiscussionsController.php +++ b/framework/core/src/Api/Controller/ListDiscussionsController.php @@ -11,11 +11,10 @@ use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Discussion\Discussion; -use Flarum\Discussion\Filter\DiscussionFilterer; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -40,7 +39,6 @@ class ListDiscussionsController extends AbstractListController public array $sortFields = ['lastPostedAt', 'commentCount', 'createdAt']; public function __construct( - protected DiscussionFilterer $filterer, protected DiscussionSearcher $searcher, protected UrlGenerator $url ) { @@ -57,12 +55,8 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = array_merge($this->extractInclude($request), ['state']); - $criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault); - if (array_key_exists('q', $filters)) { - $results = $this->searcher->search($criteria, $limit, $offset); - } else { - $results = $this->filterer->filter($criteria, $limit, $offset); - } + $criteria = new SearchCriteria($actor, $filters, $sort, $sortIsDefault); + $results = $this->searcher->search($criteria, $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('discussions.index'), diff --git a/framework/core/src/Api/Controller/ListGroupsController.php b/framework/core/src/Api/Controller/ListGroupsController.php index df5c904678..93c8e9e2d8 100644 --- a/framework/core/src/Api/Controller/ListGroupsController.php +++ b/framework/core/src/Api/Controller/ListGroupsController.php @@ -10,10 +10,10 @@ namespace Flarum\Api\Controller; use Flarum\Api\Serializer\GroupSerializer; -use Flarum\Group\Filter\GroupFilterer; +use Flarum\Group\Filter\GroupSearcher; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -26,7 +26,7 @@ class ListGroupsController extends AbstractListController public int $limit = -1; public function __construct( - protected GroupFilterer $filterer, + protected GroupSearcher $searcher, protected UrlGenerator $url ) { } @@ -42,9 +42,9 @@ protected function data(ServerRequestInterface $request, Document $document): it $limit = $this->extractLimit($request); $offset = $this->extractOffset($request); - $criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault); + $criteria = new SearchCriteria($actor, $filters, $sort, $sortIsDefault); - $queryResults = $this->filterer->filter($criteria, $limit, $offset); + $queryResults = $this->searcher->search($criteria, $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('groups.index'), diff --git a/framework/core/src/Api/Controller/ListPostsController.php b/framework/core/src/Api/Controller/ListPostsController.php index 5a6a0cf1c6..86a8fa5f42 100644 --- a/framework/core/src/Api/Controller/ListPostsController.php +++ b/framework/core/src/Api/Controller/ListPostsController.php @@ -12,9 +12,9 @@ use Flarum\Api\Serializer\PostSerializer; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Post\Filter\PostFilterer; +use Flarum\Post\Filter\PostSearcher; use Flarum\Post\PostRepository; -use Flarum\Query\QueryCriteria; +use Flarum\Search\SearchCriteria; use Illuminate\Support\Arr; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -35,7 +35,7 @@ class ListPostsController extends AbstractListController public array $sortFields = ['number', 'createdAt']; public function __construct( - protected PostFilterer $filterer, + protected PostSearcher $searcher, protected PostRepository $posts, protected UrlGenerator $url ) { @@ -53,7 +53,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = $this->extractInclude($request); - $results = $this->filterer->filter(new QueryCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); + $results = $this->searcher->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('posts.index'), diff --git a/framework/core/src/Api/Controller/ListUsersController.php b/framework/core/src/Api/Controller/ListUsersController.php index 7926353418..f89fa2785f 100644 --- a/framework/core/src/Api/Controller/ListUsersController.php +++ b/framework/core/src/Api/Controller/ListUsersController.php @@ -12,8 +12,7 @@ use Flarum\Api\Serializer\UserSerializer; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Query\QueryCriteria; -use Flarum\User\Filter\UserFilterer; +use Flarum\Search\SearchCriteria; use Flarum\User\Search\UserSearcher; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -33,7 +32,6 @@ class ListUsersController extends AbstractListController ]; public function __construct( - protected UserFilterer $filterer, protected UserSearcher $searcher, protected UrlGenerator $url ) { @@ -60,12 +58,8 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = $this->extractInclude($request); - $criteria = new QueryCriteria($actor, $filters, $sort, $sortIsDefault); - if (array_key_exists('q', $filters)) { - $results = $this->searcher->search($criteria, $limit, $offset); - } else { - $results = $this->filterer->filter($criteria, $limit, $offset); - } + $criteria = new SearchCriteria($actor, $filters, $sort, $sortIsDefault); + $results = $this->searcher->search($criteria, $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('users.index'), diff --git a/framework/core/src/Database/ScopeVisibilityTrait.php b/framework/core/src/Database/ScopeVisibilityTrait.php index a3e3f333bb..d49880de0b 100644 --- a/framework/core/src/Database/ScopeVisibilityTrait.php +++ b/framework/core/src/Database/ScopeVisibilityTrait.php @@ -13,6 +13,9 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Arr; +/** + * @method static Builder whereVisibleTo(User $user) + */ trait ScopeVisibilityTrait { /** diff --git a/framework/core/src/Discussion/Filter/AuthorFilter.php b/framework/core/src/Discussion/Filter/AuthorFilter.php index 27a35ad2cb..66adcfe709 100644 --- a/framework/core/src/Discussion/Filter/AuthorFilter.php +++ b/framework/core/src/Discussion/Filter/AuthorFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Discussion\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Flarum\User\UserRepository; use Illuminate\Database\Query\Builder; @@ -29,9 +29,9 @@ public function getFilterKey(): string return 'author'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $this->constrain($filterState->getQuery(), $filterValue, $negate); + $this->constrain($state->getQuery(), $value, $negate); } protected function constrain(Builder $query, string|array $rawUsernames, bool $negate): void diff --git a/framework/core/src/Discussion/Filter/CreatedFilter.php b/framework/core/src/Discussion/Filter/CreatedFilter.php index 1dd2b25c15..53d242f218 100644 --- a/framework/core/src/Discussion/Filter/CreatedFilter.php +++ b/framework/core/src/Discussion/Filter/CreatedFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Discussion\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Illuminate\Database\Query\Builder; use Illuminate\Support\Arr; @@ -24,21 +24,16 @@ public function getFilterKey(): string return 'created'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $filterValue = is_string($filterValue) - ? $this->asString($filterValue) - : $this->asStringArray($filterValue); + $value = $this->asString($value); - if (is_array($filterValue)) { - $from = Arr::get($filterValue, 'from'); - $to = Arr::get($filterValue, 'to'); - } else { - $from = $filterValue; - $to = null; - } + preg_match('/^(\d{4}-\d{2}-\d{2})(?:\.\.(\d{4}-\d{2}-\d{2}))?$/', $value, $matches); + + $from = Arr::get($matches, 1); + $to = Arr::get($matches, 2); - $this->constrain($filterState->getQuery(), $from, $to, $negate); + $this->constrain($state->getQuery(), $from, $to, $negate); } public function constrain(Builder $query, ?string $from, ?string $to, bool $negate): void diff --git a/framework/core/src/Discussion/Filter/DiscussionFilterer.php b/framework/core/src/Discussion/Filter/DiscussionFilterer.php deleted file mode 100644 index ec4be9aeb8..0000000000 --- a/framework/core/src/Discussion/Filter/DiscussionFilterer.php +++ /dev/null @@ -1,28 +0,0 @@ -discussions->query()->select('discussions.*')->whereVisibleTo($actor); - } -} diff --git a/framework/core/src/Discussion/Filter/HiddenFilter.php b/framework/core/src/Discussion/Filter/HiddenFilter.php index 831b3f1270..85b1092084 100644 --- a/framework/core/src/Discussion/Filter/HiddenFilter.php +++ b/framework/core/src/Discussion/Filter/HiddenFilter.php @@ -9,8 +9,8 @@ namespace Flarum\Discussion\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; use Illuminate\Database\Query\Builder; class HiddenFilter implements FilterInterface @@ -20,9 +20,9 @@ public function getFilterKey(): string return 'hidden'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $this->constrain($filterState->getQuery(), $negate); + $this->constrain($state->getQuery(), $negate); } protected function constrain(Builder $query, bool $negate): void diff --git a/framework/core/src/Discussion/Filter/UnreadFilter.php b/framework/core/src/Discussion/Filter/UnreadFilter.php index b4b84468a8..322b103721 100644 --- a/framework/core/src/Discussion/Filter/UnreadFilter.php +++ b/framework/core/src/Discussion/Filter/UnreadFilter.php @@ -10,8 +10,8 @@ namespace Flarum\Discussion\Filter; use Flarum\Discussion\DiscussionRepository; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; use Flarum\User\User; use Illuminate\Database\Query\Builder; @@ -27,9 +27,9 @@ public function getFilterKey(): string return 'unread'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $this->constrain($filterState->getQuery(), $filterState->getActor(), $negate); + $this->constrain($state->getQuery(), $state->getActor(), $negate); } protected function constrain(Builder $query, User $actor, bool $negate): void diff --git a/framework/core/src/Discussion/Search/DiscussionSearcher.php b/framework/core/src/Discussion/Search/DiscussionSearcher.php index e5c953e2b4..1dfb59e660 100644 --- a/framework/core/src/Discussion/Search/DiscussionSearcher.php +++ b/framework/core/src/Discussion/Search/DiscussionSearcher.php @@ -9,26 +9,15 @@ namespace Flarum\Discussion\Search; -use Flarum\Discussion\DiscussionRepository; +use Flarum\Discussion\Discussion; use Flarum\Search\AbstractSearcher; -use Flarum\Search\GambitManager; use Flarum\User\User; -use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Builder; class DiscussionSearcher extends AbstractSearcher { - public function __construct( - protected DiscussionRepository $discussions, - protected Dispatcher $events, - GambitManager $gambits, - array $searchMutators - ) { - parent::__construct($gambits, $searchMutators); - } - protected function getQuery(User $actor): Builder { - return $this->discussions->query()->select('discussions.*')->whereVisibleTo($actor); + return Discussion::whereVisibleTo($actor)->select('discussions.*'); } } diff --git a/framework/core/src/Discussion/Search/Gambit/FulltextGambit.php b/framework/core/src/Discussion/Search/Gambit/FulltextGambit.php index 5e72febfe4..72a8c4744b 100644 --- a/framework/core/src/Discussion/Search/Gambit/FulltextGambit.php +++ b/framework/core/src/Discussion/Search/Gambit/FulltextGambit.php @@ -11,20 +11,20 @@ use Flarum\Discussion\Discussion; use Flarum\Post\Post; -use Flarum\Search\GambitInterface; +use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\SearchState; use Illuminate\Database\Query\Expression; -class FulltextGambit implements GambitInterface +class FulltextGambit extends AbstractFulltextFilter { - public function apply(SearchState $search, string $bit): bool + public function search(SearchState $state, string $query): void { // Replace all non-word characters with spaces. // We do this to prevent MySQL fulltext search boolean mode from taking // effect: https://dev.mysql.com/doc/refman/5.7/en/fulltext-boolean.html - $bit = preg_replace('/[^\p{L}\p{N}\p{M}_]+/u', ' ', $bit); + $bit = preg_replace('/[^\p{L}\p{N}\p{M}_]+/u', ' ', $query); - $query = $search->getQuery(); + $query = $state->getQuery(); $grammar = $query->getGrammar(); $discussionSubquery = Discussion::select('id') @@ -36,7 +36,7 @@ public function apply(SearchState $search, string $bit): bool // posts. Retrieve the collective relevance of each discussion's posts, // which we will use later in the order by clause, and also retrieve // the ID of the most relevant post. - $subquery = Post::whereVisibleTo($search->getActor()) + $subquery = Post::whereVisibleTo($state->getActor()) ->select('posts.discussion_id') ->selectRaw('SUM(MATCH('.$grammar->wrap('posts.content').') AGAINST (?)) as score', [$bit]) ->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY MATCH('.$grammar->wrap('posts.content').') AGAINST (?) DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id', [$bit]) @@ -58,11 +58,9 @@ public function apply(SearchState $search, string $bit): bool ->groupBy('discussions.id') ->addBinding($subquery->getBindings(), 'join'); - $search->setDefaultSort(function ($query) use ($grammar, $bit) { + $state->setDefaultSort(function ($query) use ($grammar, $bit) { $query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$bit]); $query->orderBy('posts_ft.score', 'desc'); }); - - return true; } } diff --git a/framework/core/src/Extend/Filter.php b/framework/core/src/Extend/Filter.php deleted file mode 100644 index cfc54c4760..0000000000 --- a/framework/core/src/Extend/Filter.php +++ /dev/null @@ -1,82 +0,0 @@ - $filtererClass: The ::class attribute of the filterer to extend. - */ - public function __construct( - private readonly string $filtererClass - ) { - } - - /** - * Add a filter to run when the filtererClass is filtered. - * - * @param string $filterClass: The ::class attribute of the filter you are adding. - * @return self - */ - public function addFilter(string $filterClass): self - { - $this->filters[] = $filterClass; - - return $this; - } - - /** - * Add a callback through which to run all filter queries after filters have been applied. - * - * @param (callable(FilterState $filter, QueryCriteria $criteria): void)|class-string $callback - * - * The callback can be a closure or an invokable class, and should accept: - * - Flarum\Filter\FilterState $filter - * - Flarum\Query\QueryCriteria $criteria - * - * The callable should return void. - * - * @return self - */ - public function addFilterMutator(callable|string $callback): self - { - $this->filterMutators[] = $callback; - - return $this; - } - - public function extend(Container $container, Extension $extension = null): void - { - $container->extend('flarum.filter.filters', function ($originalFilters) { - foreach ($this->filters as $filter) { - $originalFilters[$this->filtererClass][] = $filter; - } - - return $originalFilters; - }); - - $container->extend('flarum.filter.filter_mutators', function ($originalMutators) { - foreach ($this->filterMutators as $mutator) { - $originalMutators[$this->filtererClass][] = $mutator; - } - - return $originalMutators; - }); - } -} diff --git a/framework/core/src/Extend/SimpleFlarumSearch.php b/framework/core/src/Extend/SimpleFlarumSearch.php index 0e1924c37f..1fba81fcef 100644 --- a/framework/core/src/Extend/SimpleFlarumSearch.php +++ b/framework/core/src/Extend/SimpleFlarumSearch.php @@ -10,17 +10,17 @@ namespace Flarum\Extend; use Flarum\Extension\Extension; -use Flarum\Query\QueryCriteria; -use Flarum\Search\AbstractRegexGambit; +use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\AbstractSearcher; -use Flarum\Search\GambitInterface; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchCriteria; use Flarum\Search\SearchState; use Illuminate\Contracts\Container\Container; class SimpleFlarumSearch implements ExtenderInterface { - private ?string $fullTextGambit = null; - private array $gambits = []; + private ?string $fullTextFilter = null; + private array $filters = []; private array $searchMutators = []; /** @@ -33,37 +33,37 @@ public function __construct( } /** - * Add a gambit to this searcher. Gambits are used to filter search queries. + * Add a filter to this searcher. Filters are used to filter search queries. * - * @param class-string $gambitClass: The ::class attribute of the gambit you are adding. - * This gambit must extend \Flarum\Search\AbstractRegexGambit + * @param class-string $filterClass: The ::class attribute of the filter you are adding. + * This filter must implement \Flarum\Search\FilterInterface * @return self */ - public function addGambit(string $gambitClass): self + public function addFilter(string $filterClass): self { - $this->gambits[] = $gambitClass; + $this->filters[] = $filterClass; return $this; } /** - * Set the full text gambit for this searcher. The full text gambit actually executes the search. + * Set the full text filter for this searcher. The full text filter actually executes the search. * - * @param class-string $gambitClass: The ::class attribute of the full test gambit you are adding. - * This gambit must implement \Flarum\Search\GambitInterface + * @param class-string $fulltextClass: The ::class attribute of the full test filter you are adding. + * This filter must implement \Flarum\Search\FilterInterface * @return self */ - public function setFullTextGambit(string $gambitClass): self + public function setFullTextFilter(string $fulltextClass): self { - $this->fullTextGambit = $gambitClass; + $this->fullTextFilter = $fulltextClass; return $this; } /** - * Add a callback through which to run all search queries after gambits have been applied. + * Add a callback through which to run all search queries after filters have been applied. * - * @param (callable(SearchState $search, QueryCriteria $criteria): void)|class-string $callback + * @param (callable(SearchState $search, SearchCriteria $criteria): void)|class-string $callback * * The callback can be a closure or an invokable class, and should accept: * - \Flarum\Search\SearchState $search @@ -82,23 +82,26 @@ public function addSearchMutator(callable|string $callback): self public function extend(Container $container, Extension $extension = null): void { - if (! is_null($this->fullTextGambit)) { - $container->extend('flarum.simple_search.fulltext_gambits', function ($oldFulltextGambits) { - $oldFulltextGambits[$this->searcher] = $this->fullTextGambit; + if (! is_null($this->fullTextFilter)) { + $container->extend('flarum.simple_search.fulltext_filters', function (array $oldFulltextFilters) { + $oldFulltextFilters[$this->searcher] = $this->fullTextFilter; - return $oldFulltextGambits; + return $oldFulltextFilters; }); } - $container->extend('flarum.simple_search.gambits', function ($oldGambits) { - foreach ($this->gambits as $gambit) { - $oldGambits[$this->searcher][] = $gambit; + $container->extend('flarum.simple_search.filters', function (array $oldFilters) { + // We need the key to be set, even if there are no filters, so that the searcher is registered. + $oldFilters[$this->searcher] = $oldFilters[$this->searcher] ?? []; + + foreach ($this->filters as $filter) { + $oldFilters[$this->searcher][] = $filter; } - return $oldGambits; + return $oldFilters; }); - $container->extend('flarum.simple_search.search_mutators', function ($oldMutators) { + $container->extend('flarum.simple_search.search_mutators', function (array $oldMutators) { foreach ($this->searchMutators as $mutator) { $oldMutators[$this->searcher][] = $mutator; } diff --git a/framework/core/src/Filter/AbstractFilterer.php b/framework/core/src/Filter/AbstractFilterer.php deleted file mode 100644 index 7db2e197c2..0000000000 --- a/framework/core/src/Filter/AbstractFilterer.php +++ /dev/null @@ -1,76 +0,0 @@ - */ - protected array $filters, - protected array $filterMutators - ) { - } - - abstract protected function getQuery(User $actor): Builder; - - /** - * @throws InvalidArgumentException - */ - public function filter(QueryCriteria $criteria, int $limit = null, int $offset = 0): QueryResults - { - $actor = $criteria->actor; - - $query = $this->getQuery($actor); - - $filterState = new FilterState($query->getQuery(), $actor); - - foreach ($criteria->query as $filterKey => $filterValue) { - $negate = false; - - if (str_starts_with($filterKey, '-')) { - $negate = true; - $filterKey = substr($filterKey, 1); - } - - foreach (($this->filters[$filterKey] ?? []) as $filter) { - $filterState->addActiveFilter($filter); - $filter->filter($filterState, $filterValue, $negate); - } - } - - $this->applySort($filterState, $criteria->sort, $criteria->sortIsDefault); - $this->applyOffset($filterState, $offset); - $this->applyLimit($filterState, $limit + 1); - - foreach ($this->filterMutators as $mutator) { - $mutator($filterState, $criteria); - } - - // Execute the filter query and retrieve the results. We get one more - // results than the user asked for, so that we can say if there are more - // results. If there are, we will get rid of that extra result. - $results = $query->get(); - - if ($areMoreResults = $limit > 0 && $results->count() > $limit) { - $results->pop(); - } - - return new QueryResults($results, $areMoreResults); - } -} diff --git a/framework/core/src/Filter/FilterServiceProvider.php b/framework/core/src/Filter/FilterServiceProvider.php deleted file mode 100644 index df6e947e4d..0000000000 --- a/framework/core/src/Filter/FilterServiceProvider.php +++ /dev/null @@ -1,96 +0,0 @@ -container->singleton('flarum.filter.filters', function () { - return [ - AccessTokenFilterer::class => [ - HttpFilter\UserFilter::class, - ], - DiscussionFilterer::class => [ - DiscussionFilter\AuthorFilter::class, - DiscussionFilter\CreatedFilter::class, - DiscussionFilter\HiddenFilter::class, - DiscussionFilter\UnreadFilter::class, - ], - UserFilterer::class => [ - UserFilter\EmailFilter::class, - UserFilter\GroupFilter::class, - ], - GroupFilterer::class => [ - GroupFilter\HiddenFilter::class, - ], - PostFilterer::class => [ - PostFilter\AuthorFilter::class, - PostFilter\DiscussionFilter::class, - PostFilter\IdFilter::class, - PostFilter\NumberFilter::class, - PostFilter\TypeFilter::class - ], - ]; - }); - - $this->container->singleton('flarum.filter.filter_mutators', function () { - return []; - }); - } - - public function boot(Container $container): void - { - // We can resolve the filter mutators in the when->needs->give callback, - // but we need to resolve at least one regardless so we know which - // filterers we need to register filters for. - $filters = $this->container->make('flarum.filter.filters'); - - foreach ($filters as $filterer => $filterClasses) { - $container - ->when($filterer) - ->needs('$filters') - ->give(function () use ($filterClasses) { - $compiled = []; - - foreach ($filterClasses as $filterClass) { - $filter = $this->container->make($filterClass); - $compiled[$filter->getFilterKey()][] = $filter; - } - - return $compiled; - }); - - $container - ->when($filterer) - ->needs('$filterMutators') - ->give(function () use ($container, $filterer) { - return array_map(function ($filterMutatorClass) { - return ContainerUtil::wrapCallback($filterMutatorClass, $this->container); - }, Arr::get($container->make('flarum.filter.filter_mutators'), $filterer, [])); - }); - } - } -} diff --git a/framework/core/src/Filter/FilterState.php b/framework/core/src/Filter/FilterState.php deleted file mode 100644 index 9b8b19ed91..0000000000 --- a/framework/core/src/Filter/FilterState.php +++ /dev/null @@ -1,41 +0,0 @@ -activeFilters; - } - - /** - * Add a filter as being active. - * - * @param FilterInterface $filter - * @return void - */ - public function addActiveFilter(FilterInterface $filter): void - { - $this->activeFilters[] = $filter; - } -} diff --git a/framework/core/src/Foundation/InstalledSite.php b/framework/core/src/Foundation/InstalledSite.php index 2d52395f73..b4b41e823b 100644 --- a/framework/core/src/Foundation/InstalledSite.php +++ b/framework/core/src/Foundation/InstalledSite.php @@ -18,7 +18,6 @@ use Flarum\Extend\ExtenderInterface; use Flarum\Extension\ExtensionServiceProvider; use Flarum\Filesystem\FilesystemServiceProvider; -use Flarum\Filter\FilterServiceProvider; use Flarum\Formatter\FormatterServiceProvider; use Flarum\Forum\ForumServiceProvider; use Flarum\Frontend\FrontendServiceProvider; @@ -110,7 +109,6 @@ protected function bootLaravel(): Container $app->register(ExtensionServiceProvider::class); $app->register(ErrorServiceProvider::class); $app->register(FilesystemServiceProvider::class); - $app->register(FilterServiceProvider::class); $app->register(FormatterServiceProvider::class); $app->register(ForumServiceProvider::class); $app->register(FrontendServiceProvider::class); diff --git a/framework/core/src/Group/Filter/GroupFilterer.php b/framework/core/src/Group/Filter/GroupFilterer.php deleted file mode 100644 index 8233b4780f..0000000000 --- a/framework/core/src/Group/Filter/GroupFilterer.php +++ /dev/null @@ -1,31 +0,0 @@ -groups->query()->whereVisibleTo($actor); - } -} diff --git a/framework/core/src/Group/Filter/GroupSearcher.php b/framework/core/src/Group/Filter/GroupSearcher.php new file mode 100644 index 0000000000..170a7d40cc --- /dev/null +++ b/framework/core/src/Group/Filter/GroupSearcher.php @@ -0,0 +1,23 @@ +select('groups.*'); + } +} diff --git a/framework/core/src/Group/Filter/HiddenFilter.php b/framework/core/src/Group/Filter/HiddenFilter.php index 38d7c65673..bc81a0671d 100644 --- a/framework/core/src/Group/Filter/HiddenFilter.php +++ b/framework/core/src/Group/Filter/HiddenFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Group\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class HiddenFilter implements FilterInterface { @@ -22,10 +22,10 @@ public function getFilterKey(): string return 'hidden'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $hidden = $this->asBool($filterValue); + $hidden = $this->asBool($value); - $filterState->getQuery()->where('is_hidden', $negate ? '!=' : '=', $hidden); + $state->getQuery()->where('is_hidden', $negate ? '!=' : '=', $hidden); } } diff --git a/framework/core/src/Http/Filter/AccessTokenFilterer.php b/framework/core/src/Http/Filter/AccessTokenSearcher.php similarity index 83% rename from framework/core/src/Http/Filter/AccessTokenFilterer.php rename to framework/core/src/Http/Filter/AccessTokenSearcher.php index c55df90d8e..ab2ecc6bc5 100644 --- a/framework/core/src/Http/Filter/AccessTokenFilterer.php +++ b/framework/core/src/Http/Filter/AccessTokenSearcher.php @@ -9,12 +9,12 @@ namespace Flarum\Http\Filter; -use Flarum\Filter\AbstractFilterer; use Flarum\Http\AccessToken; +use Flarum\Search\AbstractSearcher; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; -class AccessTokenFilterer extends AbstractFilterer +class AccessTokenSearcher extends AbstractSearcher { protected function getQuery(User $actor): Builder { diff --git a/framework/core/src/Http/Filter/UserFilter.php b/framework/core/src/Http/Filter/UserFilter.php index 5a8e4f68b0..d897b4c97b 100644 --- a/framework/core/src/Http/Filter/UserFilter.php +++ b/framework/core/src/Http/Filter/UserFilter.php @@ -10,9 +10,9 @@ namespace Flarum\Http\Filter; use Flarum\Api\Controller\ListAccessTokensController; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; /** * Filters an access tokens request by the related user. @@ -28,10 +28,10 @@ public function getFilterKey(): string return 'user'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $filterValue = $this->asInt($filterValue); + $value = $this->asInt($value); - $filterState->getQuery()->where('user_id', $negate ? '!=' : '=', $filterValue); + $state->getQuery()->where('user_id', $negate ? '!=' : '=', $value); } } diff --git a/framework/core/src/Post/Filter/AuthorFilter.php b/framework/core/src/Post/Filter/AuthorFilter.php index b9f337db6c..878470d083 100644 --- a/framework/core/src/Post/Filter/AuthorFilter.php +++ b/framework/core/src/Post/Filter/AuthorFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Post\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Flarum\User\UserRepository; class AuthorFilter implements FilterInterface @@ -28,12 +28,12 @@ public function getFilterKey(): string return 'author'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $usernames = $this->asStringArray($filterValue); + $usernames = $this->asStringArray($value); $ids = $this->users->query()->whereIn('username', $usernames)->pluck('id'); - $filterState->getQuery()->whereIn('posts.user_id', $ids, 'and', $negate); + $state->getQuery()->whereIn('posts.user_id', $ids, 'and', $negate); } } diff --git a/framework/core/src/Post/Filter/DiscussionFilter.php b/framework/core/src/Post/Filter/DiscussionFilter.php index 1c7fa566b7..7ce9b59169 100644 --- a/framework/core/src/Post/Filter/DiscussionFilter.php +++ b/framework/core/src/Post/Filter/DiscussionFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Post\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class DiscussionFilter implements FilterInterface { @@ -22,10 +22,10 @@ public function getFilterKey(): string return 'discussion'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $discussionId = $this->asInt($filterValue); + $discussionId = $this->asInt($value); - $filterState->getQuery()->where('posts.discussion_id', $negate ? '!=' : '=', $discussionId); + $state->getQuery()->where('posts.discussion_id', $negate ? '!=' : '=', $discussionId); } } diff --git a/framework/core/src/Post/Filter/IdFilter.php b/framework/core/src/Post/Filter/IdFilter.php index fe908920ba..1680f30b2e 100644 --- a/framework/core/src/Post/Filter/IdFilter.php +++ b/framework/core/src/Post/Filter/IdFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Post\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class IdFilter implements FilterInterface { @@ -22,10 +22,10 @@ public function getFilterKey(): string return 'id'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $ids = $this->asIntArray($filterValue); + $ids = $this->asIntArray($value); - $filterState->getQuery()->whereIn('posts.id', $ids, 'and', $negate); + $state->getQuery()->whereIn('posts.id', $ids, 'and', $negate); } } diff --git a/framework/core/src/Post/Filter/NumberFilter.php b/framework/core/src/Post/Filter/NumberFilter.php index 182ed8d32c..48d06702e9 100644 --- a/framework/core/src/Post/Filter/NumberFilter.php +++ b/framework/core/src/Post/Filter/NumberFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Post\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class NumberFilter implements FilterInterface { @@ -22,10 +22,10 @@ public function getFilterKey(): string return 'number'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $number = $this->asInt($filterValue); + $number = $this->asInt($value); - $filterState->getQuery()->where('posts.number', $negate ? '!=' : '=', $number); + $state->getQuery()->where('posts.number', $negate ? '!=' : '=', $number); } } diff --git a/framework/core/src/Post/Filter/PostFilterer.php b/framework/core/src/Post/Filter/PostFilterer.php deleted file mode 100644 index 1cc559f3d0..0000000000 --- a/framework/core/src/Post/Filter/PostFilterer.php +++ /dev/null @@ -1,31 +0,0 @@ -posts->query()->select('posts.*')->whereVisibleTo($actor); - } -} diff --git a/framework/core/src/Post/Filter/PostSearcher.php b/framework/core/src/Post/Filter/PostSearcher.php new file mode 100644 index 0000000000..4b0d6dc9d9 --- /dev/null +++ b/framework/core/src/Post/Filter/PostSearcher.php @@ -0,0 +1,23 @@ +select('posts.*'); + } +} diff --git a/framework/core/src/Post/Filter/TypeFilter.php b/framework/core/src/Post/Filter/TypeFilter.php index e8fa0debf1..5dd00bb2df 100644 --- a/framework/core/src/Post/Filter/TypeFilter.php +++ b/framework/core/src/Post/Filter/TypeFilter.php @@ -9,9 +9,9 @@ namespace Flarum\Post\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; class TypeFilter implements FilterInterface { @@ -22,10 +22,10 @@ public function getFilterKey(): string return 'type'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $type = $this->asString($filterValue); + $type = $this->asString($value); - $filterState->getQuery()->where('posts.type', $negate ? '!=' : '=', $type); + $state->getQuery()->where('posts.type', $negate ? '!=' : '=', $type); } } diff --git a/framework/core/src/Query/AbstractQueryState.php b/framework/core/src/Query/AbstractQueryState.php deleted file mode 100644 index 0c9400f0f4..0000000000 --- a/framework/core/src/Query/AbstractQueryState.php +++ /dev/null @@ -1,59 +0,0 @@ -|Closure $defaultSort - */ - protected array|Closure $defaultSort = [] - ) { - } - - /** - * Get the query builder for the search results query. - */ - public function getQuery(): Builder - { - return $this->query; - } - - public function getActor(): User - { - return $this->actor; - } - - public function getDefaultSort(): array|Closure - { - return $this->defaultSort; - } - - /** - * Set the default sort order for the search. This will only be applied if - * a sort order has not been specified in the search criteria. - */ - public function setDefaultSort(array|Closure $defaultSort): void - { - $this->defaultSort = $defaultSort; - } -} diff --git a/framework/core/src/Query/ApplyQueryParametersTrait.php b/framework/core/src/Query/ApplyQueryParametersTrait.php deleted file mode 100644 index a6e1f7b884..0000000000 --- a/framework/core/src/Query/ApplyQueryParametersTrait.php +++ /dev/null @@ -1,56 +0,0 @@ -getDefaultSort())) { - $sort = $query->getDefaultSort(); - } - - if (is_callable($sort)) { - $sort($query->getQuery()); - } else { - foreach ((array) $sort as $field => $order) { - if (is_array($order)) { - foreach ($order as $value) { - $query->getQuery()->orderByRaw(Str::snake($field).' != ?', [$value]); - } - } else { - $query->getQuery()->orderBy(Str::snake($field), $order); - } - } - } - } - - protected function applyOffset(AbstractQueryState $query, int $offset): void - { - if ($offset > 0) { - $query->getQuery()->skip($offset); - } - } - - protected function applyLimit(AbstractQueryState $query, ?int $limit): void - { - if ($limit > 0) { - $query->getQuery()->take($limit); - } - } -} diff --git a/framework/core/src/Search/AbstractFulltextFilter.php b/framework/core/src/Search/AbstractFulltextFilter.php new file mode 100644 index 0000000000..b1b6f07a85 --- /dev/null +++ b/framework/core/src/Search/AbstractFulltextFilter.php @@ -0,0 +1,25 @@ +search($state, $value); + } + + abstract public function search(SearchState $state, string $query): void; +} diff --git a/framework/core/src/Search/AbstractRegexGambit.php b/framework/core/src/Search/AbstractRegexGambit.php deleted file mode 100644 index 9372842178..0000000000 --- a/framework/core/src/Search/AbstractRegexGambit.php +++ /dev/null @@ -1,49 +0,0 @@ -match($bit)) { - list($negate) = array_splice($matches, 1, 1); - - $this->conditions($search, $matches, (bool) $negate); - } - - return (bool) $matches; - } - - /** - * Match the bit against this gambit. - */ - protected function match(string $bit): ?array - { - if (! empty($bit) && preg_match('/^(-?)'.$this->getGambitPattern().'$/i', $bit, $matches)) { - return $matches; - } - - return null; - } - - /** - * Apply conditions to the search, given that the gambit was matched. - * - * @param array $matches An array of matches from the search bit. - * @param bool $negate Whether the bit was negated, and thus whether the conditions should be negated. - */ - abstract protected function conditions(SearchState $search, array $matches, bool $negate): void; -} diff --git a/framework/core/src/Search/AbstractSearcher.php b/framework/core/src/Search/AbstractSearcher.php index a59408a91a..83b4b03a41 100644 --- a/framework/core/src/Search/AbstractSearcher.php +++ b/framework/core/src/Search/AbstractSearcher.php @@ -9,38 +9,36 @@ namespace Flarum\Search; -use Flarum\Query\ApplyQueryParametersTrait; -use Flarum\Query\QueryCriteria; -use Flarum\Query\QueryResults; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Str; abstract class AbstractSearcher { - use ApplyQueryParametersTrait; - public function __construct( - protected GambitManager $gambits, - protected array $searchMutators + protected FilterManager $filters, + /** @var array */ + protected array $mutators ) { } abstract protected function getQuery(User $actor): Builder; - public function search(QueryCriteria $criteria, ?int $limit = null, int $offset = 0): QueryResults + public function search(SearchCriteria $criteria, ?int $limit = null, int $offset = 0): SearchResults { $actor = $criteria->actor; $query = $this->getQuery($actor); - $search = new SearchState($query->getQuery(), $actor); + $search = new SearchState($query->getQuery(), $actor, in_array('q', array_keys($criteria->filters), true)); + + $this->filters->apply($search, $criteria->filters); - $this->gambits->apply($search, $criteria->query['q']); $this->applySort($search, $criteria->sort, $criteria->sortIsDefault); $this->applyOffset($search, $offset); $this->applyLimit($search, $limit + 1); - foreach ($this->searchMutators as $mutator) { + foreach ($this->mutators as $mutator) { $mutator($search, $criteria); } @@ -53,6 +51,41 @@ public function search(QueryCriteria $criteria, ?int $limit = null, int $offset $results->pop(); } - return new QueryResults($results, $areMoreResults); + return new SearchResults($results, $areMoreResults); + } + + protected function applySort(SearchState $query, ?array $sort = null, bool $sortIsDefault = false): void + { + if ($sortIsDefault && ! empty($query->getDefaultSort())) { + $sort = $query->getDefaultSort(); + } + + if (is_callable($sort)) { + $sort($query->getQuery()); + } else { + foreach ((array) $sort as $field => $order) { + if (is_array($order)) { + foreach ($order as $value) { + $query->getQuery()->orderByRaw(Str::snake($field).' != ?', [$value]); + } + } else { + $query->getQuery()->orderBy(Str::snake($field), $order); + } + } + } + } + + protected function applyOffset(SearchState $query, int $offset): void + { + if ($offset > 0) { + $query->getQuery()->skip($offset); + } + } + + protected function applyLimit(SearchState $query, ?int $limit): void + { + if ($limit > 0) { + $query->getQuery()->take($limit); + } } } diff --git a/framework/core/src/Filter/FilterInterface.php b/framework/core/src/Search/FilterInterface.php similarity index 75% rename from framework/core/src/Filter/FilterInterface.php rename to framework/core/src/Search/FilterInterface.php index 481978a1e8..0fd9d1e292 100644 --- a/framework/core/src/Filter/FilterInterface.php +++ b/framework/core/src/Search/FilterInterface.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Filter; +namespace Flarum\Search; interface FilterInterface { @@ -19,5 +19,5 @@ public function getFilterKey(): string; /** * Filters a query. */ - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void; + public function filter(SearchState $state, string|array $value, bool $negate): void; } diff --git a/framework/core/src/Search/FilterManager.php b/framework/core/src/Search/FilterManager.php new file mode 100644 index 0000000000..2f21dc2868 --- /dev/null +++ b/framework/core/src/Search/FilterManager.php @@ -0,0 +1,59 @@ + + */ + protected array $filters = []; + + public function __construct( + protected ?AbstractFulltextFilter $fulltextGambit = null + ) { + } + + public function add(FilterInterface $filter): void + { + $this->filters[$filter->getFilterKey()][] = $filter; + } + + public function apply(SearchState $search, array $filters): void + { + $this->applyFulltext($search, $filters['q'] ?? null); + $this->applyFilters($search, $filters); + } + + protected function applyFilters(SearchState $search, array $filters): void + { + foreach ($filters as $filterKey => $filterValue) { + $negate = false; + + if (str_starts_with($filterKey, '-')) { + $negate = true; + $filterKey = substr($filterKey, 1); + } + + foreach (($this->filters[$filterKey] ?? []) as $filter) { + $search->addActiveFilter($filter); + $filter->filter($search, $filterValue, $negate); + } + } + } + + protected function applyFulltext(SearchState $search, ?string $query): void + { + if ($this->fulltextGambit && $query) { + $search->addActiveFilter($this->fulltextGambit); + $this->fulltextGambit->search($search, $query); + } + } +} diff --git a/framework/core/src/Search/GambitInterface.php b/framework/core/src/Search/GambitInterface.php deleted file mode 100644 index 99fb01729b..0000000000 --- a/framework/core/src/Search/GambitInterface.php +++ /dev/null @@ -1,21 +0,0 @@ -gambits[] = $gambit; - } - - /** - * Apply gambits to a search, given a search query. - */ - public function apply(SearchState $search, string $query): void - { - $query = $this->applyGambits($search, $query); - - if ($query) { - $this->applyFulltext($search, $query); - } - } - - /** - * Explode a search query into an array of bits. - */ - protected function explode(string $query): array - { - return str_getcsv($query, ' '); - } - - protected function applyGambits(SearchState $search, string $query): string - { - $bits = array_filter($this->explode($query)); - - if (! $bits) { - return ''; - } - - foreach ($bits as $k => $bit) { - foreach ($this->gambits as $gambit) { - if (! $gambit instanceof GambitInterface) { - throw new LogicException( - 'Gambit '.get_class($gambit).' does not implement '.GambitInterface::class - ); - } - - if ($gambit->apply($search, $bit)) { - $search->addActiveGambit($gambit); - unset($bits[$k]); - break; - } - } - } - - return implode(' ', $bits); - } - - protected function applyFulltext(SearchState $search, string $query): void - { - $search->addActiveGambit($this->fulltextGambit); - $this->fulltextGambit->apply($search, $query); - } -} diff --git a/framework/core/src/Query/QueryCriteria.php b/framework/core/src/Search/SearchCriteria.php similarity index 92% rename from framework/core/src/Query/QueryCriteria.php rename to framework/core/src/Search/SearchCriteria.php index 44d4728211..dfd8fc08a0 100644 --- a/framework/core/src/Query/QueryCriteria.php +++ b/framework/core/src/Search/SearchCriteria.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Query; +namespace Flarum\Search; use Flarum\User\User; @@ -16,11 +16,11 @@ * query. The limit and offset are not included because they only determine * which part of the entire result set will be returned. */ -class QueryCriteria +class SearchCriteria { public function __construct( public User $actor, - public array $query, + public array $filters, /** * An array of sort-order pairs, where the column is the key, and the order * is the value. The order may be 'asc', 'desc', or an array of IDs to diff --git a/framework/core/src/Query/QueryResults.php b/framework/core/src/Search/SearchResults.php similarity index 92% rename from framework/core/src/Query/QueryResults.php rename to framework/core/src/Search/SearchResults.php index a3882dce00..9df3a85336 100644 --- a/framework/core/src/Query/QueryResults.php +++ b/framework/core/src/Search/SearchResults.php @@ -7,11 +7,11 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Query; +namespace Flarum\Search; use Illuminate\Database\Eloquent\Collection; -class QueryResults +class SearchResults { public function __construct( protected Collection $results, diff --git a/framework/core/src/Search/SearchServiceProvider.php b/framework/core/src/Search/SearchServiceProvider.php index e71e60fcbf..295d9e7acd 100644 --- a/framework/core/src/Search/SearchServiceProvider.php +++ b/framework/core/src/Search/SearchServiceProvider.php @@ -9,11 +9,19 @@ namespace Flarum\Search; +use Flarum\Discussion\Filter as DiscussionFilter; use Flarum\Discussion\Search\DiscussionSearcher; -use Flarum\Discussion\Search\Gambit\FulltextGambit as DiscussionFulltextGambit; +use Flarum\Discussion\Search\Gambit\FulltextGambit as DiscussionFulltextFilter; use Flarum\Foundation\AbstractServiceProvider; use Flarum\Foundation\ContainerUtil; -use Flarum\User\Search\Gambit\FulltextGambit as UserFulltextGambit; +use Flarum\Group\Filter as GroupFilter; +use Flarum\Group\Filter\GroupSearcher; +use Flarum\Http\Filter\AccessTokenSearcher; +use Flarum\Http\Filter as HttpFilter; +use Flarum\Post\Filter as PostFilter; +use Flarum\Post\Filter\PostSearcher; +use Flarum\User\Filter as UserFilter; +use Flarum\User\Search\Gambit\FulltextGambit as UserFulltextFilter; use Flarum\User\Search\UserSearcher; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Arr; @@ -22,18 +30,38 @@ class SearchServiceProvider extends AbstractServiceProvider { public function register(): void { - $this->container->singleton('flarum.simple_search.fulltext_gambits', function () { + $this->container->singleton('flarum.simple_search.fulltext_filters', function () { return [ - DiscussionSearcher::class => DiscussionFulltextGambit::class, - UserSearcher::class => UserFulltextGambit::class + DiscussionSearcher::class => DiscussionFulltextFilter::class, + UserSearcher::class => UserFulltextFilter::class ]; }); - $this->container->singleton('flarum.simple_search.gambits', function () { + $this->container->singleton('flarum.simple_search.filters', function () { return [ - // @TODO searcher filters - DiscussionSearcher::class => [], - UserSearcher::class => [] + AccessTokenSearcher::class => [ + HttpFilter\UserFilter::class, + ], + DiscussionSearcher::class => [ + DiscussionFilter\AuthorFilter::class, + DiscussionFilter\CreatedFilter::class, + DiscussionFilter\HiddenFilter::class, + DiscussionFilter\UnreadFilter::class, + ], + UserSearcher::class => [ + UserFilter\EmailFilter::class, + UserFilter\GroupFilter::class, + ], + GroupSearcher::class => [ + GroupFilter\HiddenFilter::class, + ], + PostSearcher::class => [ + PostFilter\AuthorFilter::class, + PostFilter\DiscussionFilter::class, + PostFilter\IdFilter::class, + PostFilter\NumberFilter::class, + PostFilter\TypeFilter::class + ], ]; }); @@ -44,24 +72,28 @@ public function register(): void public function boot(Container $container): void { - $fullTextGambits = $container->make('flarum.simple_search.fulltext_gambits'); - - foreach ($fullTextGambits as $searcher => $fullTextGambitClass) { + foreach ($container->make('flarum.simple_search.filters') as $searcher => $filterClasses) { $container ->when($searcher) - ->needs(GambitManager::class) - ->give(function () use ($container, $searcher, $fullTextGambitClass) { - $gambitManager = new GambitManager($container->make($fullTextGambitClass)); - foreach (Arr::get($container->make('flarum.simple_search.gambits'), $searcher, []) as $gambit) { - $gambitManager->add($container->make($gambit)); + ->needs(FilterManager::class) + ->give(function () use ($container, $searcher) { + $fulltext = $container->make('flarum.simple_search.fulltext_filters'); + $fulltextClass = $fulltext[$searcher] ?? null; + + $manager = new FilterManager( + $fulltextClass ? $container->make($fulltextClass) : null + ); + + foreach (Arr::get($container->make('flarum.simple_search.filters'), $searcher, []) as $filter) { + $manager->add($container->make($filter)); } - return $gambitManager; + return $manager; }); $container ->when($searcher) - ->needs('$searchMutators') + ->needs('$mutators') ->give(function () use ($container, $searcher) { $searchMutators = Arr::get($container->make('flarum.simple_search.search_mutators'), $searcher, []); diff --git a/framework/core/src/Search/SearchState.php b/framework/core/src/Search/SearchState.php index bee14cb8b7..24b4aad42e 100644 --- a/framework/core/src/Search/SearchState.php +++ b/framework/core/src/Search/SearchState.php @@ -9,30 +9,78 @@ namespace Flarum\Search; -use Flarum\Query\AbstractQueryState; +use Closure; +use Flarum\User\User; +use Illuminate\Database\Query\Builder; -class SearchState extends AbstractQueryState +class SearchState { /** - * @var GambitInterface[] + * @var FilterInterface[] */ - protected array $activeGambits = []; + protected array $activeFilters = []; + + public function __construct( + protected Builder $query, + protected User $actor, + /** Whether this is a fulltext search or just filtering. */ + protected bool $fulltextSearch, + /** + * An array of sort-order pairs, where the column + * is the key, and the order is the value. The order may be 'asc', + * 'desc', or an array of IDs to order by. + * Alternatively, a callable may be used. + * + * @var array|Closure $defaultSort + */ + protected array|Closure $defaultSort = [] + ) { + } /** - * Get a list of the gambits that are active in this search. - * - * @return GambitInterface[] + * Get the query builder for the search results query. */ - public function getActiveGambits(): array + public function getQuery(): Builder + { + return $this->query; + } + + public function getActor(): User { - return $this->activeGambits; + return $this->actor; + } + + public function getDefaultSort(): array|Closure + { + return $this->defaultSort; } /** - * Add a gambit as being active in this search. + * Set the default sort order for the search. This will only be applied if + * a sort order has not been specified in the search criteria. */ - public function addActiveGambit(GambitInterface $gambit): void + public function setDefaultSort(array|Closure $defaultSort): void + { + $this->defaultSort = $defaultSort; + } + + public function isFulltextSearch(): bool + { + return $this->fulltextSearch; + } + + /** + * Get a list of the filters that are active. + * + * @return FilterInterface[] + */ + public function getActiveFilters(): array + { + return $this->activeFilters; + } + + public function addActiveFilter(FilterInterface $filter): void { - $this->activeGambits[] = $gambit; + $this->activeFilters[] = $filter; } } diff --git a/framework/core/src/Filter/ValidateFilterTrait.php b/framework/core/src/Search/ValidateFilterTrait.php similarity index 99% rename from framework/core/src/Filter/ValidateFilterTrait.php rename to framework/core/src/Search/ValidateFilterTrait.php index 7c496105eb..9ebcb79d41 100644 --- a/framework/core/src/Filter/ValidateFilterTrait.php +++ b/framework/core/src/Search/ValidateFilterTrait.php @@ -7,7 +7,7 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Filter; +namespace Flarum\Search; use Flarum\Foundation\ValidationException as FlarumValidationException; use Flarum\Locale\Translator; diff --git a/framework/core/src/User/Filter/EmailFilter.php b/framework/core/src/User/Filter/EmailFilter.php index b5b93b0746..732f0a1736 100644 --- a/framework/core/src/User/Filter/EmailFilter.php +++ b/framework/core/src/User/Filter/EmailFilter.php @@ -9,9 +9,9 @@ namespace Flarum\User\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Illuminate\Database\Query\Builder; class EmailFilter implements FilterInterface @@ -23,13 +23,13 @@ public function getFilterKey(): string return 'email'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - if (! $filterState->getActor()->hasPermission('user.edit')) { + if (! $state->getActor()->hasPermission('user.edit')) { return; } - $this->constrain($filterState->getQuery(), $filterValue, $negate); + $this->constrain($state->getQuery(), $value, $negate); } protected function constrain(Builder $query, string|array $rawEmail, bool $negate): void diff --git a/framework/core/src/User/Filter/GroupFilter.php b/framework/core/src/User/Filter/GroupFilter.php index bec0d10bc3..a3dd5a5329 100644 --- a/framework/core/src/User/Filter/GroupFilter.php +++ b/framework/core/src/User/Filter/GroupFilter.php @@ -9,10 +9,10 @@ namespace Flarum\User\Filter; -use Flarum\Filter\FilterInterface; -use Flarum\Filter\FilterState; -use Flarum\Filter\ValidateFilterTrait; use Flarum\Group\Group; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchState; +use Flarum\Search\ValidateFilterTrait; use Flarum\User\User; use Illuminate\Database\Query\Builder; @@ -25,9 +25,9 @@ public function getFilterKey(): string return 'group'; } - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void + public function filter(SearchState $state, string|array $value, bool $negate): void { - $this->constrain($filterState->getQuery(), $filterState->getActor(), $filterValue, $negate); + $this->constrain($state->getQuery(), $state->getActor(), $value, $negate); } protected function constrain(Builder $query, User $actor, string|array $rawQuery, bool $negate): void diff --git a/framework/core/src/User/Filter/UserFilterer.php b/framework/core/src/User/Filter/UserFilterer.php deleted file mode 100644 index 3af54ff765..0000000000 --- a/framework/core/src/User/Filter/UserFilterer.php +++ /dev/null @@ -1,31 +0,0 @@ -users->query()->whereVisibleTo($actor); - } -} diff --git a/framework/core/src/User/Search/Gambit/FulltextGambit.php b/framework/core/src/User/Search/Gambit/FulltextGambit.php index f3eeeebe74..5b00f2efd8 100644 --- a/framework/core/src/User/Search/Gambit/FulltextGambit.php +++ b/framework/core/src/User/Search/Gambit/FulltextGambit.php @@ -9,13 +9,13 @@ namespace Flarum\User\Search\Gambit; -use Flarum\Search\GambitInterface; +use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\SearchState; use Flarum\User\User; use Flarum\User\UserRepository; use Illuminate\Database\Eloquent\Builder; -class FulltextGambit implements GambitInterface +class FulltextGambit extends AbstractFulltextFilter { public function __construct( protected UserRepository $users @@ -33,14 +33,12 @@ private function getUserSearchSubQuery(string $searchValue): Builder ->where('username', 'like', "$searchValue%"); } - public function apply(SearchState $search, string $bit): bool + public function search(SearchState $state, string $query): void { - $search->getQuery() + $state->getQuery() ->whereIn( 'id', - $this->getUserSearchSubQuery($bit) + $this->getUserSearchSubQuery($query) ); - - return true; } } diff --git a/framework/core/src/User/Search/UserSearcher.php b/framework/core/src/User/Search/UserSearcher.php index f6cbeab0bf..c758e2f6bd 100644 --- a/framework/core/src/User/Search/UserSearcher.php +++ b/framework/core/src/User/Search/UserSearcher.php @@ -10,25 +10,13 @@ namespace Flarum\User\Search; use Flarum\Search\AbstractSearcher; -use Flarum\Search\GambitManager; use Flarum\User\User; -use Flarum\User\UserRepository; -use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Eloquent\Builder; class UserSearcher extends AbstractSearcher { - public function __construct( - protected UserRepository $users, - protected Dispatcher $events, - GambitManager $gambits, - array $searchMutators - ) { - parent::__construct($gambits, $searchMutators); - } - protected function getQuery(User $actor): Builder { - return $this->users->query()->whereVisibleTo($actor); + return User::whereVisibleTo($actor)->select('users.*'); } } diff --git a/framework/core/tests/integration/api/AbstractSerializeControllerTest.php b/framework/core/tests/integration/api/AbstractSerializeControllerTest.php index 9b98cc4877..cd5be3d891 100644 --- a/framework/core/tests/integration/api/AbstractSerializeControllerTest.php +++ b/framework/core/tests/integration/api/AbstractSerializeControllerTest.php @@ -30,9 +30,9 @@ public function test_missing_serializer_class_throws_exception() $this->request('GET', '/api/dummy-serialize') ); - $json = json_decode((string) $response->getBody(), true); + $json = json_decode($contents = (string) $response->getBody(), true); - $this->assertEquals(500, $response->getStatusCode()); + $this->assertEquals(500, $response->getStatusCode(), $contents); $this->assertStringStartsWith('InvalidArgumentException: Serializer required for controller: '.DummySerializeController::class, $json['errors'][0]['detail']); } } diff --git a/framework/core/tests/integration/api/access_tokens/DeleteTest.php b/framework/core/tests/integration/api/access_tokens/DeleteTest.php index b12b9dbd1e..e2a9d1a25f 100644 --- a/framework/core/tests/integration/api/access_tokens/DeleteTest.php +++ b/framework/core/tests/integration/api/access_tokens/DeleteTest.php @@ -115,7 +115,7 @@ public function user_cannot_delete_current_session_token() $response = $this->send($request); - $this->assertEquals(403, $response->getStatusCode()); + $this->assertEquals(403, $response->getStatusCode(), $response->getBody()->getContents()); } /** diff --git a/framework/core/tests/integration/api/access_tokens/ListTest.php b/framework/core/tests/integration/api/access_tokens/ListTest.php index 2a077bedbe..1e11766ddb 100644 --- a/framework/core/tests/integration/api/access_tokens/ListTest.php +++ b/framework/core/tests/integration/api/access_tokens/ListTest.php @@ -62,11 +62,11 @@ public function user_can_view_access_tokens(int $authenticatedAs, array $canView $request = $this->request('GET', '/api/access-tokens', compact('authenticatedAs')) ); - $data = Arr::get(json_decode($response->getBody()->getContents(), true), 'data'); + $data = Arr::get(json_decode($contents = $response->getBody()->getContents(), true), 'data'); $testsTokenId = AccessToken::findValid($request->getAttribute('tests_token'))->id; - $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(200, $response->getStatusCode(), $contents); $this->assertEqualsCanonicalizing(array_merge($canViewIds, [$testsTokenId]), Arr::pluck($data, 'id')); } @@ -112,14 +112,14 @@ public function user_needs_permissions_to_use_user_filter(int $authenticatedAs, ]) ); - $data = Arr::get(json_decode($response->getBody()->getContents(), true), 'data'); + $data = Arr::get(json_decode($contents = $response->getBody()->getContents(), true), 'data'); $testsTokenId = AccessToken::findValid($request->getAttribute('tests_token'))->id; if ($authenticatedAs === $userId) { $canViewIds[] = $testsTokenId; } - $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(200, $response->getStatusCode(), $contents); $this->assertEqualsCanonicalizing($canViewIds, Arr::pluck($data, 'id')); } diff --git a/framework/core/tests/integration/api/discussions/ListTest.php b/framework/core/tests/integration/api/discussions/ListTest.php index 63078ddce1..170f9b05da 100644 --- a/framework/core/tests/integration/api/discussions/ListTest.php +++ b/framework/core/tests/integration/api/discussions/ListTest.php @@ -265,200 +265,4 @@ public function unread_filter_works_when_negated() // Order-independent comparison $this->assertEqualsCanonicalizing(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match'); } - - /** - * @test - */ - public function author_gambit_works() - { - $response = $this->send( - $this->request('GET', '/api/discussions') - ->withQueryParams([ - 'filter' => ['q' => 'author:normal'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEqualsCanonicalizing(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function author_gambit_works_negated() - { - $response = $this->send( - $this->request('GET', '/api/discussions') - ->withQueryParams([ - 'filter' => ['q' => '-author:normal'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEquals(['1'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function created_gambit_works_with_date() - { - $response = $this->send( - $this->request('GET', '/api/discussions') - ->withQueryParams([ - 'filter' => ['q' => 'created:1995-05-21'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function created_gambit_works_negated_with_date() - { - $response = $this->send( - $this->request('GET', '/api/discussions') - ->withQueryParams([ - 'filter' => ['q' => '-created:1995-05-21'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEqualsCanonicalizing(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function created_gambit_works_with_range() - { - $response = $this->send( - $this->request('GET', '/api/discussions') - ->withQueryParams([ - 'filter' => ['q' => 'created:1980-05-21..2000-05-21'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEqualsCanonicalizing(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function created_gambit_works_negated_with_range() - { - $response = $this->send( - $this->request('GET', '/api/discussions') - ->withQueryParams([ - 'filter' => ['q' => '-created:1980-05-21..2000-05-21'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEquals(['1'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function hidden_gambit_works() - { - $response = $this->send( - $this->request('GET', '/api/discussions', ['authenticatedAs' => 1]) - ->withQueryParams([ - 'filter' => ['q' => 'is:hidden'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEquals(['4'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function hidden_gambit_works_negated() - { - $response = $this->send( - $this->request('GET', '/api/discussions', ['authenticatedAs' => 1]) - ->withQueryParams([ - 'filter' => ['q' => '-is:hidden'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEqualsCanonicalizing(['1', '2', '3'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function unread_gambit_works() - { - $this->app(); - $this->read(); - - $response = $this->send( - $this->request('GET', '/api/discussions', ['authenticatedAs' => 2]) - ->withQueryParams([ - 'filter' => ['q' => 'is:unread'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match'); - } - - /** - * @test - */ - public function unread_gambit_works_when_negated() - { - $this->app(); - $this->read(); - - $response = $this->send( - $this->request('GET', '/api/discussions', ['authenticatedAs' => 2]) - ->withQueryParams([ - 'filter' => ['q' => '-is:unread'], - 'include' => 'mostRelevantPost', - ]) - ); - - $data = json_decode($response->getBody()->getContents(), true)['data']; - - // Order-independent comparison - $this->assertEqualsCanonicalizing(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match'); - } } diff --git a/framework/core/tests/integration/api/posts/ListTest.php b/framework/core/tests/integration/api/posts/ListTest.php index fad1721722..8cc025999f 100644 --- a/framework/core/tests/integration/api/posts/ListTest.php +++ b/framework/core/tests/integration/api/posts/ListTest.php @@ -14,7 +14,7 @@ use Flarum\Testing\integration\TestCase; use Illuminate\Support\Arr; -class ListTests extends TestCase +class ListTest extends TestCase { use RetrievesAuthorizedUsers; diff --git a/framework/core/tests/integration/api/users/GroupSearchTest.php b/framework/core/tests/integration/api/users/GroupSearchTest.php index 42b8447bbd..803bd263d2 100644 --- a/framework/core/tests/integration/api/users/GroupSearchTest.php +++ b/framework/core/tests/integration/api/users/GroupSearchTest.php @@ -236,7 +236,7 @@ private function createRequest(array $group, int $userId = null) return $this->send( $this->request('GET', '/api/users', $auth) - ->withQueryParams(['filter' => ['q' => 'group:'.implode(',', $group)]]) + ->withQueryParams(['filter' => ['group' => implode(',', $group)]]) ); } diff --git a/framework/core/tests/integration/api/users/ListTest.php b/framework/core/tests/integration/api/users/ListTest.php index 86c4588811..4ff425c98e 100644 --- a/framework/core/tests/integration/api/users/ListTest.php +++ b/framework/core/tests/integration/api/users/ListTest.php @@ -223,94 +223,4 @@ public function email_filter_only_works_for_admin() $data = json_decode($response->getBody()->getContents(), true)['data']; $this->assertEquals(['1', '2'], Arr::pluck($data, 'id')); } - - /** - * @test - */ - public function group_gambit_works() - { - $response = $this->send( - $this->request('GET', '/api/users', [ - 'authenticatedAs' => 1, - ])->withQueryParams([ - 'filter' => ['q' => 'group:1'], - ]) - ); - - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true)['data']; - $this->assertEquals(['1'], Arr::pluck($data, 'id')); - } - - /** - * @test - */ - public function group_gambit_works_negated() - { - $response = $this->send( - $this->request('GET', '/api/users', [ - 'authenticatedAs' => 1, - ])->withQueryParams([ - 'filter' => ['q' => '-group:1'], - ]) - ); - - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true)['data']; - $this->assertEquals(['2'], Arr::pluck($data, 'id')); - } - - /** - * @test - */ - public function email_gambit_works() - { - $response = $this->send( - $this->request('GET', '/api/users', [ - 'authenticatedAs' => 1, - ])->withQueryParams([ - 'filter' => ['q' => 'email:admin@machine.local'], - ]) - ); - - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true)['data']; - $this->assertEquals(['1'], Arr::pluck($data, 'id')); - } - - /** - * @test - */ - public function email_gambit_works_negated() - { - $response = $this->send( - $this->request('GET', '/api/users', [ - 'authenticatedAs' => 1, - ])->withQueryParams([ - 'filter' => ['q' => '-email:admin@machine.local'], - ]) - ); - - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true)['data']; - $this->assertEquals(['2'], Arr::pluck($data, 'id')); - } - - /** - * @test - */ - public function email_gambit_only_works_for_admin() - { - $response = $this->send( - $this->request('GET', '/api/users', [ - 'authenticatedAs' => 2, - ])->withQueryParams([ - 'filter' => ['q' => 'email:admin@machine.local'], - ]) - ); - - $this->assertEquals(200, $response->getStatusCode()); - $data = json_decode($response->getBody()->getContents(), true)['data']; - $this->assertEquals([], Arr::pluck($data, 'id')); - } } diff --git a/framework/core/tests/integration/extenders/FilterTest.php b/framework/core/tests/integration/extenders/FilterTest.php deleted file mode 100644 index b2e790f127..0000000000 --- a/framework/core/tests/integration/extenders/FilterTest.php +++ /dev/null @@ -1,131 +0,0 @@ -prepareDatabase([ - 'discussions' => [ - ['id' => 1, 'title' => 'DISCUSSION 1', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1], - ['id' => 2, 'title' => 'DISCUSSION 2', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 2, 'comment_count' => 1], - ], - 'posts' => [ - ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '

    foo bar

    '], - ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '

    foo bar not the same

    '], - ], - 'users' => [ - $this->normalUser(), - ], - ]); - } - - public function filterDiscussions($filters, $limit = null) - { - $response = $this->send( - $this->request('GET', '/api/discussions', [ - 'authenticatedAs' => 1, - ])->withQueryParams([ - 'filter' => $filters, - 'include' => 'mostRelevantPost', - ]) - ); - - return json_decode($response->getBody()->getContents(), true)['data']; - } - - /** - * @test - */ - public function works_as_expected_with_no_modifications() - { - $this->prepDb(); - - $searchForAll = json_encode($this->filterDiscussions([], 5)); - $this->assertStringContainsString('DISCUSSION 1', $searchForAll); - $this->assertStringContainsString('DISCUSSION 2', $searchForAll); - } - - /** - * @test - */ - public function custom_filter_has_effect_if_added() - { - $this->extend((new Extend\Filter(DiscussionFilterer::class))->addFilter(NoResultFilter::class)); - - $this->prepDb(); - - $withResultSearch = json_encode($this->filterDiscussions(['noResult' => 0], 5)); - $this->assertStringContainsString('DISCUSSION 1', $withResultSearch); - $this->assertStringContainsString('DISCUSSION 2', $withResultSearch); - $this->assertEquals([], $this->filterDiscussions(['noResult' => 1], 5)); - } - - /** - * @test - */ - public function filter_mutator_has_effect_if_added() - { - $this->extend((new Extend\Filter(DiscussionFilterer::class))->addFilterMutator(function ($filterState, $criteria) { - $filterState->getQuery()->whereRaw('1=0'); - })); - - $this->prepDb(); - - $this->assertEquals([], $this->filterDiscussions([], 5)); - } - - /** - * @test - */ - public function filter_mutator_has_effect_if_added_with_invokable_class() - { - $this->extend((new Extend\Filter(DiscussionFilterer::class))->addFilterMutator(CustomFilterMutator::class)); - - $this->prepDb(); - - $this->assertEquals([], $this->filterDiscussions([], 5)); - } -} - -class NoResultFilter implements FilterInterface -{ - public function getFilterKey(): string - { - return 'noResult'; - } - - public function filter(FilterState $filterState, string|array $filterValue, bool $negate): void - { - if ($filterValue) { - $filterState->getQuery() - ->whereRaw('0=1'); - } - } -} - -class CustomFilterMutator -{ - public function __invoke($filterState, $criteria) - { - $filterState->getQuery()->whereRaw('1=0'); - } -} diff --git a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php b/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php index ce4f8062d5..7514a25e1a 100644 --- a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php +++ b/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php @@ -13,10 +13,10 @@ use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; use Flarum\Group\Group; -use Flarum\Query\QueryCriteria; -use Flarum\Search\AbstractRegexGambit; +use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\AbstractSearcher; -use Flarum\Search\GambitInterface; +use Flarum\Search\FilterInterface; +use Flarum\Search\SearchCriteria; use Flarum\Search\SearchState; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; @@ -62,13 +62,15 @@ protected function tearDown(): void $this->database()->table('posts')->whereIn('id', [1, 2])->delete(); } - public function searchDiscussions($query, $limit = null) + public function searchDiscussions($query, $limit = null, array $filters = []) { $this->app(); $actor = User::find(1); - $criteria = new QueryCriteria($actor, ['q' => $query]); + $filters['q'] = $query; + + $criteria = new SearchCriteria($actor, $filters); return $this->app()->getContainer()->make(DiscussionSearcher::class)->search($criteria, $limit)->getResults(); } @@ -94,7 +96,7 @@ public function works_as_expected_with_no_modifications() */ public function custom_full_text_gambit_has_effect_if_added() { - $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->setFullTextGambit(NoResultFullTextGambit::class)); + $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->setFullTextFilter(NoResultFullTextFilter::class)); $this->assertEquals('[]', json_encode($this->searchDiscussions('in text', 5))); } @@ -102,16 +104,16 @@ public function custom_full_text_gambit_has_effect_if_added() /** * @test */ - public function custom_filter_gambit_has_effect_if_added() + public function custom_filter_has_effect_if_added() { - $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->addGambit(NoResultFilterGambit::class)); + $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->addFilter(NoResultFilter::class)); $this->prepDb(); - $withResultSearch = json_encode($this->searchDiscussions('noResult:0', 5)); + $withResultSearch = json_encode($this->searchDiscussions('', 5, ['noResult' => '0'])); $this->assertStringContainsString('DISCUSSION 1', $withResultSearch); $this->assertStringContainsString('DISCUSSION 2', $withResultSearch); - $this->assertEquals('[]', json_encode($this->searchDiscussions('noResult:1', 5))); + $this->assertEquals('[]', json_encode($this->searchDiscussions('', 5, ['noResult' => '1']))); } /** @@ -156,7 +158,7 @@ public function cant_resolve_custom_searcher_without_fulltext_gambit() public function can_resolve_custom_searcher_with_fulltext_gambit() { $this->extend( - (new Extend\SimpleFlarumSearch(CustomSearcher::class))->setFullTextGambit(CustomFullTextGambit::class) + (new Extend\SimpleFlarumSearch(CustomSearcher::class))->setFullTextFilter(CustomFullTextFilter::class) ); $anExceptionWasThrown = false; @@ -171,30 +173,27 @@ public function can_resolve_custom_searcher_with_fulltext_gambit() } } -class NoResultFullTextGambit implements GambitInterface +class NoResultFullTextFilter extends AbstractFulltextFilter { - public function apply(SearchState $search, string $bit): bool + public function search(SearchState $state, string $query): void { - $search->getQuery() - ->whereRaw('0=1'); - - return true; + $state->getQuery()->whereRaw('0=1'); } } -class NoResultFilterGambit extends AbstractRegexGambit +class NoResultFilter implements FilterInterface { - public function getGambitPattern(): string + public function getFilterKey(): string { - return 'noResult:(.+)'; + return 'noResult'; } - public function conditions(SearchState $search, array $matches, bool $negate): void + public function filter(SearchState $state, array|string $value, bool $negate): void { - $noResults = trim($matches[1], ' '); + $noResults = trim($value, ' '); + if ($noResults == '1') { - $search->getQuery() - ->whereRaw('0=1'); + $state->getQuery()->whereRaw('0=1'); } } } @@ -216,10 +215,10 @@ protected function getQuery(User $actor): Builder } } -class CustomFullTextGambit implements GambitInterface +class CustomFullTextFilter extends AbstractFulltextFilter { - public function apply(SearchState $search, string $bit): bool + public function search(SearchState $state, string $query): void { - return true; + // } }