From c2b73ea45996f17ca77bcbf02ca30338cf8352db Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 27 Sep 2023 17:25:22 +0100 Subject: [PATCH 01/15] feat: first iteration of search drivers --- extensions/likes/extend.php | 9 +- extensions/likes/src/Query/LikedByFilter.php | 6 +- extensions/likes/src/Query/LikedFilter.php | 6 +- extensions/lock/extend.php | 5 +- extensions/lock/src/Filter/LockedFilter.php | 6 +- extensions/mentions/extend.php | 7 +- .../mentions/src/Filter/MentionedFilter.php | 6 +- .../src/Filter/MentionedPostFilter.php | 6 +- extensions/nicknames/extend.php | 5 +- .../nicknames/src/NicknameFullTextFilter.php | 4 + extensions/sticky/extend.php | 7 +- .../src/PinStickiedDiscussionsToTop.php | 4 +- extensions/sticky/src/Query/StickyFilter.php | 6 +- extensions/subscriptions/extend.php | 7 +- .../src/Filter/SubscriptionFilter.php | 6 +- .../src/HideIgnoredFromAllDiscussionsPage.php | 4 +- extensions/suspend/extend.php | 5 +- .../suspend/src/Query/SuspendedFilter.php | 6 +- extensions/tags/extend.php | 15 +- .../tags/src/Search/Filter/PostTagFilter.php | 6 +- .../tags/src/Search/Filter/TagFilter.php | 6 +- extensions/tags/src/Search/FulltextFilter.php | 4 + .../HideHiddenTagsFromAllDiscussionsPage.php | 4 +- extensions/tags/src/Search/TagSearcher.php | 2 +- .../Controller/ListAccessTokensController.php | 9 +- .../Controller/ListDiscussionsController.php | 9 +- .../Api/Controller/ListGroupsController.php | 11 +- .../Api/Controller/ListPostsController.php | 9 +- .../Api/Controller/ListUsersController.php | 10 +- .../Discussion/Search/DiscussionSearcher.php | 2 +- .../Discussion/Search/Filter/AuthorFilter.php | 6 +- .../Search/Filter/CreatedFilter.php | 6 +- .../Discussion/Search/Filter/HiddenFilter.php | 6 +- .../Discussion/Search/Filter/UnreadFilter.php | 6 +- .../src/Discussion/Search/FulltextFilter.php | 4 + framework/core/src/Extend/Search.php | 152 ++++++++++++++++++ .../core/src/Extend/SimpleFlarumSearch.php | 112 ------------- .../core/src/Group/Filter/GroupSearcher.php | 2 +- .../core/src/Group/Filter/HiddenFilter.php | 6 +- .../src/Http/Filter/AccessTokenSearcher.php | 2 +- framework/core/src/Http/Filter/UserFilter.php | 4 +- .../core/src/Install/Steps/WriteSettings.php | 2 +- .../core/src/Post/Filter/AuthorFilter.php | 6 +- .../core/src/Post/Filter/DiscussionFilter.php | 6 +- framework/core/src/Post/Filter/IdFilter.php | 6 +- .../core/src/Post/Filter/NumberFilter.php | 6 +- .../core/src/Post/Filter/PostSearcher.php | 2 +- framework/core/src/Post/Filter/TypeFilter.php | 6 +- framework/core/src/Search/AbstractDriver.php | 20 +++ .../src/Search/AbstractFulltextFilter.php | 9 ++ .../{ => Database}/AbstractSearcher.php | 17 +- .../Search/Database/DatabaseSearchDriver.php | 13 ++ .../Search/Database/DatabaseSearchState.php | 28 ++++ .../Search/{ => Filter}/FilterInterface.php | 9 +- .../src/Search/{ => Filter}/FilterManager.php | 5 +- framework/core/src/Search/SearchManager.php | 42 +++++ .../core/src/Search/SearchServiceProvider.php | 52 +++++- framework/core/src/Search/SearchState.php | 20 +-- .../core/src/Search/SearcherInterface.php | 8 + .../src/Settings/SettingsServiceProvider.php | 5 + .../src/User/Search/Filter/EmailFilter.php | 6 +- .../src/User/Search/Filter/GroupFilter.php | 6 +- .../core/src/User/Search/FulltextFilter.php | 4 + .../core/src/User/Search/UserSearcher.php | 2 +- .../extenders/SimpleFlarumSearchTest.php | 84 ++++------ 65 files changed, 576 insertions(+), 286 deletions(-) create mode 100644 framework/core/src/Extend/Search.php delete mode 100644 framework/core/src/Extend/SimpleFlarumSearch.php create mode 100644 framework/core/src/Search/AbstractDriver.php rename framework/core/src/Search/{ => Database}/AbstractSearcher.php (77%) create mode 100644 framework/core/src/Search/Database/DatabaseSearchDriver.php create mode 100644 framework/core/src/Search/Database/DatabaseSearchState.php rename framework/core/src/Search/{ => Filter}/FilterInterface.php (77%) rename framework/core/src/Search/{ => Filter}/FilterManager.php (93%) create mode 100644 framework/core/src/Search/SearchManager.php create mode 100644 framework/core/src/Search/SearcherInterface.php diff --git a/extensions/likes/extend.php b/extensions/likes/extend.php index 434df72cdc..f22f698dfc 100644 --- a/extensions/likes/extend.php +++ b/extensions/likes/extend.php @@ -21,6 +21,7 @@ use Flarum\Likes\Query\LikedFilter; use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\User\Search\UserSearcher; use Flarum\User\User; @@ -76,11 +77,9 @@ ->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class) ->subscribe(Listener\SaveLikesToDatabase::class), - (new Extend\SimpleFlarumSearch(PostSearcher::class)) - ->addFilter(LikedByFilter::class), - - (new Extend\SimpleFlarumSearch(UserSearcher::class)) - ->addFilter(LikedFilter::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(PostSearcher::class, LikedByFilter::class) + ->addFilter(UserSearcher::class, LikedFilter::class), (new Extend\Settings()) ->default('flarum-likes.like_own_post', true), diff --git a/extensions/likes/src/Query/LikedByFilter.php b/extensions/likes/src/Query/LikedByFilter.php index e31acf507a..6fdb89c9a2 100644 --- a/extensions/likes/src/Query/LikedByFilter.php +++ b/extensions/likes/src/Query/LikedByFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Likes\Query; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class LikedByFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/extensions/likes/src/Query/LikedFilter.php b/extensions/likes/src/Query/LikedFilter.php index 17524eb668..b291d97eaf 100644 --- a/extensions/likes/src/Query/LikedFilter.php +++ b/extensions/likes/src/Query/LikedFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Likes\Query; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class LikedFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/extensions/lock/extend.php b/extensions/lock/extend.php index 6d0ab38a0b..11e1e125b0 100644 --- a/extensions/lock/extend.php +++ b/extensions/lock/extend.php @@ -20,6 +20,7 @@ use Flarum\Lock\Listener; use Flarum\Lock\Notification\DiscussionLockedBlueprint; use Flarum\Lock\Post\DiscussionLockedPost; +use Flarum\Search\Database\DatabaseSearchDriver; return [ (new Extend\Frontend('forum')) @@ -56,6 +57,6 @@ (new Extend\Policy()) ->modelPolicy(Discussion::class, Access\DiscussionPolicy::class), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addFilter(LockedFilter::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(DiscussionSearcher::class, LockedFilter::class), ]; diff --git a/extensions/lock/src/Filter/LockedFilter.php b/extensions/lock/src/Filter/LockedFilter.php index 02cdf411ef..6fb2349f59 100644 --- a/extensions/lock/src/Filter/LockedFilter.php +++ b/extensions/lock/src/Filter/LockedFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Lock\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class LockedFilter implements FilterInterface { public function getFilterKey(): string diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 20c7798eff..70200ba79b 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -26,6 +26,7 @@ use Flarum\Post\Event\Revised; use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Tags\Api\Serializer\TagSerializer; use Flarum\Tags\Tag; use Flarum\User\User; @@ -115,9 +116,9 @@ ->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class) ->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class), - (new Extend\SimpleFlarumSearch(PostSearcher::class)) - ->addFilter(Filter\MentionedFilter::class) - ->addFilter(Filter\MentionedPostFilter::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(PostSearcher::class, Filter\MentionedFilter::class) + ->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class), (new Extend\ApiSerializer(CurrentUserSerializer::class)) ->attribute('canMentionGroups', function (CurrentUserSerializer $serializer, User $user): bool { diff --git a/extensions/mentions/src/Filter/MentionedFilter.php b/extensions/mentions/src/Filter/MentionedFilter.php index af0d96ba33..2c555de981 100644 --- a/extensions/mentions/src/Filter/MentionedFilter.php +++ b/extensions/mentions/src/Filter/MentionedFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Mentions\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class MentionedFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/extensions/mentions/src/Filter/MentionedPostFilter.php b/extensions/mentions/src/Filter/MentionedPostFilter.php index 65bf8d2e3a..193ba33953 100644 --- a/extensions/mentions/src/Filter/MentionedPostFilter.php +++ b/extensions/mentions/src/Filter/MentionedPostFilter.php @@ -9,9 +9,13 @@ namespace Flarum\Mentions\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; +/** + * @implements FilterInterface + */ class MentionedPostFilter implements FilterInterface { public function getFilterKey(): string diff --git a/extensions/nicknames/extend.php b/extensions/nicknames/extend.php index 32e47b08a7..8728d35e0e 100644 --- a/extensions/nicknames/extend.php +++ b/extensions/nicknames/extend.php @@ -12,6 +12,7 @@ use Flarum\Api\Serializer\UserSerializer; use Flarum\Extend; use Flarum\Nicknames\Access\UserPolicy; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\User\Event\Saving; use Flarum\User\Search\UserSearcher; use Flarum\User\User; @@ -51,8 +52,8 @@ (new Extend\Validator(UserValidator::class)) ->configure(AddNicknameValidation::class), - (new Extend\SimpleFlarumSearch(UserSearcher::class)) - ->setFullTextFilter(NicknameFullTextFilter::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->setFulltext(UserSearcher::class, NicknameFullTextFilter::class), (new Extend\Policy()) ->modelPolicy(User::class, UserPolicy::class), diff --git a/extensions/nicknames/src/NicknameFullTextFilter.php b/extensions/nicknames/src/NicknameFullTextFilter.php index 9cea9543e4..e8135293bd 100644 --- a/extensions/nicknames/src/NicknameFullTextFilter.php +++ b/extensions/nicknames/src/NicknameFullTextFilter.php @@ -10,10 +10,14 @@ namespace Flarum\Nicknames; use Flarum\Search\AbstractFulltextFilter; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; use Flarum\User\UserRepository; use Illuminate\Database\Eloquent\Builder; +/** + * @extends AbstractFulltextFilter + */ class NicknameFullTextFilter extends AbstractFulltextFilter { public function __construct( diff --git a/extensions/sticky/extend.php b/extensions/sticky/extend.php index 61c55c9123..25b85d121b 100644 --- a/extensions/sticky/extend.php +++ b/extensions/sticky/extend.php @@ -13,6 +13,7 @@ use Flarum\Discussion\Event\Saving; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Sticky\Event\DiscussionWasStickied; use Flarum\Sticky\Event\DiscussionWasUnstickied; use Flarum\Sticky\Listener; @@ -53,7 +54,7 @@ ->listen(DiscussionWasStickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasStickied']) ->listen(DiscussionWasUnstickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasUnstickied']), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addFilter(StickyFilter::class) - ->addSearchMutator(PinStickiedDiscussionsToTop::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(DiscussionSearcher::class, StickyFilter::class) + ->addMutator(DiscussionSearcher::class, PinStickiedDiscussionsToTop::class), ]; diff --git a/extensions/sticky/src/PinStickiedDiscussionsToTop.php b/extensions/sticky/src/PinStickiedDiscussionsToTop.php index 77b44d8028..c8a044216c 100755 --- a/extensions/sticky/src/PinStickiedDiscussionsToTop.php +++ b/extensions/sticky/src/PinStickiedDiscussionsToTop.php @@ -9,13 +9,13 @@ namespace Flarum\Sticky; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchCriteria; -use Flarum\Search\SearchState; use Flarum\Tags\Search\Filter\TagFilter; class PinStickiedDiscussionsToTop { - public function __invoke(SearchState $state, SearchCriteria $criteria): void + public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): void { if ($criteria->sortIsDefault && ! $state->isFulltextSearch()) { $query = $state->getQuery(); diff --git a/extensions/sticky/src/Query/StickyFilter.php b/extensions/sticky/src/Query/StickyFilter.php index 8a498e3e78..74ab036ebf 100644 --- a/extensions/sticky/src/Query/StickyFilter.php +++ b/extensions/sticky/src/Query/StickyFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Sticky\Query; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class StickyFilter implements FilterInterface { public function getFilterKey(): string diff --git a/extensions/subscriptions/extend.php b/extensions/subscriptions/extend.php index 36942714ef..f19feecd4f 100644 --- a/extensions/subscriptions/extend.php +++ b/extensions/subscriptions/extend.php @@ -19,6 +19,7 @@ use Flarum\Post\Event\Hidden; use Flarum\Post\Event\Posted; use Flarum\Post\Event\Restored; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Subscriptions\Filter\SubscriptionFilter; use Flarum\Subscriptions\HideIgnoredFromAllDiscussionsPage; use Flarum\Subscriptions\Listener; @@ -69,9 +70,9 @@ ->listen(Deleted::class, Listener\DeleteNotificationWhenPostIsHiddenOrDeleted::class) ->listen(Posted::class, Listener\FollowAfterReply::class), - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addFilter(SubscriptionFilter::class) - ->addSearchMutator(HideIgnoredFromAllDiscussionsPage::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(DiscussionSearcher::class, SubscriptionFilter::class) + ->addMutator(DiscussionSearcher::class, HideIgnoredFromAllDiscussionsPage::class), (new Extend\User()) ->registerPreference('flarum-subscriptions.notify_for_all_posts', 'boolval', false), diff --git a/extensions/subscriptions/src/Filter/SubscriptionFilter.php b/extensions/subscriptions/src/Filter/SubscriptionFilter.php index 2d3224a9fc..6e6f6d2f44 100644 --- a/extensions/subscriptions/src/Filter/SubscriptionFilter.php +++ b/extensions/subscriptions/src/Filter/SubscriptionFilter.php @@ -9,12 +9,16 @@ namespace Flarum\Subscriptions\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\User; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class SubscriptionFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php b/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php index 63c2eeffd2..2d36132c95 100644 --- a/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php +++ b/extensions/subscriptions/src/HideIgnoredFromAllDiscussionsPage.php @@ -9,12 +9,12 @@ namespace Flarum\Subscriptions; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchCriteria; -use Flarum\Search\SearchState; class HideIgnoredFromAllDiscussionsPage { - public function __invoke(SearchState $state, SearchCriteria $criteria): void + public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): void { // We only want to hide on the "all discussions" page. if (count($state->getActiveFilters()) === 0 && ! $state->isFulltextSearch()) { diff --git a/extensions/suspend/extend.php b/extensions/suspend/extend.php index 935ce1983b..549d2f8503 100644 --- a/extensions/suspend/extend.php +++ b/extensions/suspend/extend.php @@ -10,6 +10,7 @@ use Flarum\Api\Serializer\BasicUserSerializer; use Flarum\Api\Serializer\UserSerializer; use Flarum\Extend; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Suspend\Access\UserPolicy; use Flarum\Suspend\AddUserSuspendAttributes; use Flarum\Suspend\Event\Suspended; @@ -57,8 +58,8 @@ (new Extend\User()) ->permissionGroups(RevokeAccessFromSuspendedUsers::class), - (new Extend\SimpleFlarumSearch(UserSearcher::class)) - ->addFilter(SuspendedFilter::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(UserSearcher::class, SuspendedFilter::class), (new Extend\View()) ->namespace('flarum-suspend', __DIR__.'/views'), diff --git a/extensions/suspend/src/Query/SuspendedFilter.php b/extensions/suspend/src/Query/SuspendedFilter.php index b55c1d66aa..ed9080066e 100644 --- a/extensions/suspend/src/Query/SuspendedFilter.php +++ b/extensions/suspend/src/Query/SuspendedFilter.php @@ -10,12 +10,16 @@ namespace Flarum\Suspend\Query; use Carbon\Carbon; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\User\Guest; use Flarum\User\UserRepository; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class SuspendedFilter implements FilterInterface { public function __construct( diff --git a/extensions/tags/extend.php b/extensions/tags/extend.php index 35b6a17ad7..d63a857f1d 100644 --- a/extensions/tags/extend.php +++ b/extensions/tags/extend.php @@ -19,6 +19,7 @@ use Flarum\Http\RequestUtil; use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Tags\Access; use Flarum\Tags\Api\Controller; use Flarum\Tags\Api\Serializer\TagSerializer; @@ -134,15 +135,11 @@ ->listen(DiscussionWasTagged::class, Listener\CreatePostWhenTagsAreChanged::class) ->subscribe(Listener\UpdateTagMetadata::class), - (new Extend\SimpleFlarumSearch(PostSearcher::class)) - ->addFilter(PostTagFilter::class), - - (new Extend\SimpleFlarumSearch(DiscussionSearcher::class)) - ->addFilter(TagFilter::class) - ->addSearchMutator(HideHiddenTagsFromAllDiscussionsPage::class), - - (new Extend\SimpleFlarumSearch(TagSearcher::class)) - ->setFullTextFilter(FulltextFilter::class), + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(PostSearcher::class, PostTagFilter::class) + ->addFilter(DiscussionSearcher::class, TagFilter::class) + ->addMutator(DiscussionSearcher::class, HideHiddenTagsFromAllDiscussionsPage::class) + ->setFulltext(TagSearcher::class, FulltextFilter::class), (new Extend\ModelUrl(Tag::class)) ->addSlugDriver('default', Utf8SlugDriver::class), diff --git a/extensions/tags/src/Search/Filter/PostTagFilter.php b/extensions/tags/src/Search/Filter/PostTagFilter.php index b694990398..34506a41ef 100644 --- a/extensions/tags/src/Search/Filter/PostTagFilter.php +++ b/extensions/tags/src/Search/Filter/PostTagFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Tags\Search\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class PostTagFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/extensions/tags/src/Search/Filter/TagFilter.php b/extensions/tags/src/Search/Filter/TagFilter.php index 38a6bdc3ee..7bc43e828c 100644 --- a/extensions/tags/src/Search/Filter/TagFilter.php +++ b/extensions/tags/src/Search/Filter/TagFilter.php @@ -10,7 +10,8 @@ namespace Flarum\Tags\Search\Filter; use Flarum\Http\SlugManager; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\Tags\Tag; @@ -18,6 +19,9 @@ use Illuminate\Database\Eloquent\ModelNotFoundException; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class TagFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/extensions/tags/src/Search/FulltextFilter.php b/extensions/tags/src/Search/FulltextFilter.php index af1cf2831a..03ca19549d 100644 --- a/extensions/tags/src/Search/FulltextFilter.php +++ b/extensions/tags/src/Search/FulltextFilter.php @@ -10,10 +10,14 @@ namespace Flarum\Tags\Search; use Flarum\Search\AbstractFulltextFilter; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; use Flarum\Tags\TagRepository; use Illuminate\Database\Eloquent\Builder; +/** + * @extends AbstractFulltextFilter + */ class FulltextFilter extends AbstractFulltextFilter { public function __construct( diff --git a/extensions/tags/src/Search/HideHiddenTagsFromAllDiscussionsPage.php b/extensions/tags/src/Search/HideHiddenTagsFromAllDiscussionsPage.php index 59396c2888..9954602025 100644 --- a/extensions/tags/src/Search/HideHiddenTagsFromAllDiscussionsPage.php +++ b/extensions/tags/src/Search/HideHiddenTagsFromAllDiscussionsPage.php @@ -9,13 +9,13 @@ namespace Flarum\Tags\Search; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchCriteria; -use Flarum\Search\SearchState; use Flarum\Tags\Tag; class HideHiddenTagsFromAllDiscussionsPage { - public function __invoke(SearchState $state, SearchCriteria $queryCriteria): void + public function __invoke(DatabaseSearchState $state, SearchCriteria $queryCriteria): void { if (count($state->getActiveFilters()) > 0 || $state->isFulltextSearch()) { return; diff --git a/extensions/tags/src/Search/TagSearcher.php b/extensions/tags/src/Search/TagSearcher.php index bfa9b1f8e8..a453af95c2 100644 --- a/extensions/tags/src/Search/TagSearcher.php +++ b/extensions/tags/src/Search/TagSearcher.php @@ -9,7 +9,7 @@ namespace Flarum\Tags\Search; -use Flarum\Search\AbstractSearcher; +use Flarum\Search\Database\AbstractSearcher; use Flarum\Tags\Tag; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; diff --git a/framework/core/src/Api/Controller/ListAccessTokensController.php b/framework/core/src/Api/Controller/ListAccessTokensController.php index 8adec05452..5ff2735e80 100644 --- a/framework/core/src/Api/Controller/ListAccessTokensController.php +++ b/framework/core/src/Api/Controller/ListAccessTokensController.php @@ -10,10 +10,11 @@ namespace Flarum\Api\Controller; use Flarum\Api\Serializer\AccessTokenSerializer; -use Flarum\Http\Filter\AccessTokenSearcher; +use Flarum\Http\AccessToken; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchManager; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -23,7 +24,7 @@ class ListAccessTokensController extends AbstractListController public function __construct( protected UrlGenerator $url, - protected AccessTokenSearcher $searcher + protected SearchManager $search ) { } @@ -37,7 +38,9 @@ protected function data(ServerRequestInterface $request, Document $document): it $limit = $this->extractLimit($request); $filter = $this->extractFilter($request); - $tokens = $this->searcher->search(new SearchCriteria($actor, $filter), $limit, $offset); + $tokens = $this->search + ->for(AccessToken::class) + ->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 b8eda4fa91..3c41829d1f 100644 --- a/framework/core/src/Api/Controller/ListDiscussionsController.php +++ b/framework/core/src/Api/Controller/ListDiscussionsController.php @@ -11,10 +11,10 @@ use Flarum\Api\Serializer\DiscussionSerializer; use Flarum\Discussion\Discussion; -use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchManager; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -39,7 +39,7 @@ class ListDiscussionsController extends AbstractListController public array $sortFields = ['lastPostedAt', 'commentCount', 'createdAt']; public function __construct( - protected DiscussionSearcher $searcher, + protected SearchManager $search, protected UrlGenerator $url ) { } @@ -55,8 +55,9 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = array_merge($this->extractInclude($request), ['state']); - $criteria = new SearchCriteria($actor, $filters, $sort, $sortIsDefault); - $results = $this->searcher->search($criteria, $limit, $offset); + $results = $this->search + ->for(Discussion::class) + ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $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 93c8e9e2d8..82bc28f50b 100644 --- a/framework/core/src/Api/Controller/ListGroupsController.php +++ b/framework/core/src/Api/Controller/ListGroupsController.php @@ -10,10 +10,11 @@ namespace Flarum\Api\Controller; use Flarum\Api\Serializer\GroupSerializer; -use Flarum\Group\Filter\GroupSearcher; +use Flarum\Group\Group; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchManager; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -26,7 +27,7 @@ class ListGroupsController extends AbstractListController public int $limit = -1; public function __construct( - protected GroupSearcher $searcher, + protected SearchManager $search, protected UrlGenerator $url ) { } @@ -42,9 +43,9 @@ protected function data(ServerRequestInterface $request, Document $document): it $limit = $this->extractLimit($request); $offset = $this->extractOffset($request); - $criteria = new SearchCriteria($actor, $filters, $sort, $sortIsDefault); - - $queryResults = $this->searcher->search($criteria, $limit, $offset); + $queryResults = $this->search + ->for(Group::class) + ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $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 86a8fa5f42..a2b53eab85 100644 --- a/framework/core/src/Api/Controller/ListPostsController.php +++ b/framework/core/src/Api/Controller/ListPostsController.php @@ -12,9 +12,10 @@ use Flarum\Api\Serializer\PostSerializer; use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; -use Flarum\Post\Filter\PostSearcher; +use Flarum\Post\Post; use Flarum\Post\PostRepository; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchManager; use Illuminate\Support\Arr; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -35,7 +36,7 @@ class ListPostsController extends AbstractListController public array $sortFields = ['number', 'createdAt']; public function __construct( - protected PostSearcher $searcher, + protected SearchManager $search, protected PostRepository $posts, protected UrlGenerator $url ) { @@ -53,7 +54,9 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = $this->extractInclude($request); - $results = $this->searcher->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); + $results = $this->search + ->for(Post::class) + ->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 f89fa2785f..744db3a390 100644 --- a/framework/core/src/Api/Controller/ListUsersController.php +++ b/framework/core/src/Api/Controller/ListUsersController.php @@ -13,7 +13,8 @@ use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; use Flarum\Search\SearchCriteria; -use Flarum\User\Search\UserSearcher; +use Flarum\Search\SearchManager; +use Flarum\User\User; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -32,7 +33,7 @@ class ListUsersController extends AbstractListController ]; public function __construct( - protected UserSearcher $searcher, + protected SearchManager $search, protected UrlGenerator $url ) { } @@ -58,8 +59,9 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = $this->extractInclude($request); - $criteria = new SearchCriteria($actor, $filters, $sort, $sortIsDefault); - $results = $this->searcher->search($criteria, $limit, $offset); + $results = $this->search + ->for(User::class) + ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); $document->addPaginationLinks( $this->url->to('api')->route('users.index'), diff --git a/framework/core/src/Discussion/Search/DiscussionSearcher.php b/framework/core/src/Discussion/Search/DiscussionSearcher.php index 1dfb59e660..912b5a369a 100644 --- a/framework/core/src/Discussion/Search/DiscussionSearcher.php +++ b/framework/core/src/Discussion/Search/DiscussionSearcher.php @@ -10,7 +10,7 @@ namespace Flarum\Discussion\Search; use Flarum\Discussion\Discussion; -use Flarum\Search\AbstractSearcher; +use Flarum\Search\Database\AbstractSearcher; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; diff --git a/framework/core/src/Discussion/Search/Filter/AuthorFilter.php b/framework/core/src/Discussion/Search/Filter/AuthorFilter.php index 3d77c73c48..ef5c44dc79 100644 --- a/framework/core/src/Discussion/Search/Filter/AuthorFilter.php +++ b/framework/core/src/Discussion/Search/Filter/AuthorFilter.php @@ -9,12 +9,16 @@ namespace Flarum\Discussion\Search\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\UserRepository; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class AuthorFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Discussion/Search/Filter/CreatedFilter.php b/framework/core/src/Discussion/Search/Filter/CreatedFilter.php index 3de0d6c516..c66bac114a 100644 --- a/framework/core/src/Discussion/Search/Filter/CreatedFilter.php +++ b/framework/core/src/Discussion/Search/Filter/CreatedFilter.php @@ -9,12 +9,16 @@ namespace Flarum\Discussion\Search\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Illuminate\Database\Query\Builder; use Illuminate\Support\Arr; +/** + * @implements FilterInterface + */ class CreatedFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Discussion/Search/Filter/HiddenFilter.php b/framework/core/src/Discussion/Search/Filter/HiddenFilter.php index ced42868e7..e9e52cae86 100644 --- a/framework/core/src/Discussion/Search/Filter/HiddenFilter.php +++ b/framework/core/src/Discussion/Search/Filter/HiddenFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Discussion\Search\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class HiddenFilter implements FilterInterface { public function getFilterKey(): string diff --git a/framework/core/src/Discussion/Search/Filter/UnreadFilter.php b/framework/core/src/Discussion/Search/Filter/UnreadFilter.php index a4b442bca7..6c12ac6359 100644 --- a/framework/core/src/Discussion/Search/Filter/UnreadFilter.php +++ b/framework/core/src/Discussion/Search/Filter/UnreadFilter.php @@ -10,11 +10,15 @@ namespace Flarum\Discussion\Search\Filter; use Flarum\Discussion\DiscussionRepository; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\User\User; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class UnreadFilter implements FilterInterface { public function __construct( diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 3aa4850dbe..8f0968dbcb 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -12,9 +12,13 @@ use Flarum\Discussion\Discussion; use Flarum\Post\Post; use Flarum\Search\AbstractFulltextFilter; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; use Illuminate\Database\Query\Expression; +/** + * @extends AbstractFulltextFilter + */ class FulltextFilter extends AbstractFulltextFilter { public function search(SearchState $state, string $query): void diff --git a/framework/core/src/Extend/Search.php b/framework/core/src/Extend/Search.php new file mode 100644 index 0000000000..9f4be8147d --- /dev/null +++ b/framework/core/src/Extend/Search.php @@ -0,0 +1,152 @@ + $driverClass: The driver class you are modifying or adding. + * This driver must extend \Flarum\Search\AbstractDriver. + */ + public function __construct( + private readonly string $driverClass + ) { + } + + /** + * Add a filter to this searcher. Filters are used to filter search queries. + * + * @param class-string $modelClass : The class of the model subject to searching/filtering. + * This model must extend \Flarum\Database\AbstractModel. + * @param class-string $searcherClass : The class of the Searcher for this model + * This searcher must implement \Flarum\Search\SearcherInterface. + * Or extend \Flarum\Search\Database\AbstractSearcher if using the default driver. + * @return self + */ + public function addSearcher(string $modelClass, string $searcherClass): self + { + $this->searchers[$modelClass] = $searcherClass; + + return $this; + } + + /** + * Add a filter to this searcher. Filters are used to filter search queries. + * + * @param class-string $searcherClass : The class of the Searcher for this model + * This searcher must implement \Flarum\Search\SearcherInterface. + * Or extend \Flarum\Search\Database\AbstractSearcher if using the default driver. + * @param class-string $filterClass: The ::class attribute of the filter you are adding. + * This filter must implement \Flarum\Search\FilterInterface + * @return self + */ + public function addFilter(string $searcherClass, string $filterClass): self + { + $this->filters[$searcherClass][] = $filterClass; + + return $this; + } + + /** + * Set the full text filter for this searcher. The full text filter actually executes the search. + * + * @param class-string $searcherClass : The class of the Searcher for this model + * This searcher must implement \Flarum\Search\SearcherInterface. + * Or extend \Flarum\Search\Database\AbstractSearcher if using the default driver. + * @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 setFulltext(string $searcherClass, string $fulltextClass): self + { + $this->fulltext[$searcherClass] = $fulltextClass; + + return $this; + } + + /** + * Add a callback through which to run all search queries after filters have been applied. + * + * @param class-string $searcherClass : The class of the Searcher for this model + * This searcher must implement \Flarum\Search\SearcherInterface. + * Or extend \Flarum\Search\Database\AbstractSearcher if using the default driver. + * @param (callable(DatabaseSearchState $search, SearchCriteria $criteria): void)|class-string $callback + * + * The callback can be a closure or an invokable class, and should accept: + * - \Flarum\Search\SearchState $search + * - \Flarum\Query\QueryCriteria $criteria + * + * The callback should return void. + * + * @return self + */ + public function addMutator(string $searcherClass, callable|string $callback): self + { + $this->mutators[$searcherClass][] = $callback; + + return $this; + } + + public function extend(Container $container, Extension $extension = null): void + { + $container->extend('flarum.search.drivers', function (array $oldDrivers) { + $oldDrivers[$this->driverClass] = array_merge( + $oldDrivers[$this->driverClass] ?? [], + $this->searchers + ); + + return $oldDrivers; + }); + + $container->extend('flarum.search.fulltext', function (array $oldFulltextFilters) { + foreach ($this->fulltext as $searcherClass => $fulltextClass) { + $oldFulltextFilters[$searcherClass] = $fulltextClass; + } + + return $oldFulltextFilters; + }); + + $container->extend('flarum.search.filters', function (array $oldFilters) { + foreach ($this->filters as $searcherClass => $filters) { + $oldFilters[$searcherClass] = array_merge( + $oldFilters[$searcherClass] ?? [], + $filters + ); + } + + return $oldFilters; + }); + + $container->extend('flarum.search.mutators', function (array $oldMutators) { + foreach ($this->mutators as $searcherClass => $mutators) { + $oldMutators[$searcherClass] = array_merge( + $oldMutators[$searcherClass] ?? [], + $mutators + ); + } + + return $oldMutators; + }); + } +} diff --git a/framework/core/src/Extend/SimpleFlarumSearch.php b/framework/core/src/Extend/SimpleFlarumSearch.php deleted file mode 100644 index 1fba81fcef..0000000000 --- a/framework/core/src/Extend/SimpleFlarumSearch.php +++ /dev/null @@ -1,112 +0,0 @@ - $searcher: The ::class attribute of the Searcher you are modifying. - * This searcher must extend \Flarum\Search\AbstractSearcher. - */ - public function __construct( - private readonly string $searcher - ) { - } - - /** - * Add a filter to this searcher. Filters are used to filter search queries. - * - * @param class-string $filterClass: The ::class attribute of the filter you are adding. - * This filter must implement \Flarum\Search\FilterInterface - * @return self - */ - public function addFilter(string $filterClass): self - { - $this->filters[] = $filterClass; - - return $this; - } - - /** - * Set the full text filter for this searcher. The full text filter actually executes the search. - * - * @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 setFullTextFilter(string $fulltextClass): self - { - $this->fullTextFilter = $fulltextClass; - - return $this; - } - - /** - * Add a callback through which to run all search queries after filters have been applied. - * - * @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 - * - \Flarum\Query\QueryCriteria $criteria - * - * The callback should return void. - * - * @return self - */ - public function addSearchMutator(callable|string $callback): self - { - $this->searchMutators[] = $callback; - - return $this; - } - - public function extend(Container $container, Extension $extension = null): void - { - if (! is_null($this->fullTextFilter)) { - $container->extend('flarum.simple_search.fulltext_filters', function (array $oldFulltextFilters) { - $oldFulltextFilters[$this->searcher] = $this->fullTextFilter; - - return $oldFulltextFilters; - }); - } - - $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 $oldFilters; - }); - - $container->extend('flarum.simple_search.search_mutators', function (array $oldMutators) { - foreach ($this->searchMutators as $mutator) { - $oldMutators[$this->searcher][] = $mutator; - } - - return $oldMutators; - }); - } -} diff --git a/framework/core/src/Group/Filter/GroupSearcher.php b/framework/core/src/Group/Filter/GroupSearcher.php index 170a7d40cc..745522cfa0 100644 --- a/framework/core/src/Group/Filter/GroupSearcher.php +++ b/framework/core/src/Group/Filter/GroupSearcher.php @@ -10,7 +10,7 @@ namespace Flarum\Group\Filter; use Flarum\Group\Group; -use Flarum\Search\AbstractSearcher; +use Flarum\Search\Database\AbstractSearcher; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; diff --git a/framework/core/src/Group/Filter/HiddenFilter.php b/framework/core/src/Group/Filter/HiddenFilter.php index bc81a0671d..9a4060358b 100644 --- a/framework/core/src/Group/Filter/HiddenFilter.php +++ b/framework/core/src/Group/Filter/HiddenFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Group\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class HiddenFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Http/Filter/AccessTokenSearcher.php b/framework/core/src/Http/Filter/AccessTokenSearcher.php index ab2ecc6bc5..09e384d0e1 100644 --- a/framework/core/src/Http/Filter/AccessTokenSearcher.php +++ b/framework/core/src/Http/Filter/AccessTokenSearcher.php @@ -10,7 +10,7 @@ namespace Flarum\Http\Filter; use Flarum\Http\AccessToken; -use Flarum\Search\AbstractSearcher; +use Flarum\Search\Database\AbstractSearcher; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; diff --git a/framework/core/src/Http/Filter/UserFilter.php b/framework/core/src/Http/Filter/UserFilter.php index d897b4c97b..216151abff 100644 --- a/framework/core/src/Http/Filter/UserFilter.php +++ b/framework/core/src/Http/Filter/UserFilter.php @@ -10,7 +10,8 @@ namespace Flarum\Http\Filter; use Flarum\Api\Controller\ListAccessTokensController; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; @@ -18,6 +19,7 @@ * Filters an access tokens request by the related user. * * @see ListAccessTokensController + * @implements FilterInterface */ class UserFilter implements FilterInterface { diff --git a/framework/core/src/Install/Steps/WriteSettings.php b/framework/core/src/Install/Steps/WriteSettings.php index 28dd0c70e1..a1fa2ab634 100644 --- a/framework/core/src/Install/Steps/WriteSettings.php +++ b/framework/core/src/Install/Steps/WriteSettings.php @@ -59,7 +59,7 @@ private function getDefaults(): array 'forum_description' => '', 'mail_driver' => 'mail', 'mail_from' => 'noreply@localhost', - 'slug_driver_Flarum\User\User' => 'default', + 'slug_driver_Flarum\User\User' => 'default', // @todo: use a morph map instead `User::class => 'user'` = slug_driver_user (below as well) 'theme_colored_header' => '0', 'theme_dark_mode' => '0', 'theme_primary_color' => '#4D698E', diff --git a/framework/core/src/Post/Filter/AuthorFilter.php b/framework/core/src/Post/Filter/AuthorFilter.php index 878470d083..f4bebf67c7 100644 --- a/framework/core/src/Post/Filter/AuthorFilter.php +++ b/framework/core/src/Post/Filter/AuthorFilter.php @@ -9,11 +9,15 @@ namespace Flarum\Post\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\UserRepository; +/** + * @implements FilterInterface + */ class AuthorFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Post/Filter/DiscussionFilter.php b/framework/core/src/Post/Filter/DiscussionFilter.php index 7ce9b59169..dac9523886 100644 --- a/framework/core/src/Post/Filter/DiscussionFilter.php +++ b/framework/core/src/Post/Filter/DiscussionFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Post\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class DiscussionFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Post/Filter/IdFilter.php b/framework/core/src/Post/Filter/IdFilter.php index 1680f30b2e..e6b1befcfa 100644 --- a/framework/core/src/Post/Filter/IdFilter.php +++ b/framework/core/src/Post/Filter/IdFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Post\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class IdFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Post/Filter/NumberFilter.php b/framework/core/src/Post/Filter/NumberFilter.php index 48d06702e9..cab7f37d78 100644 --- a/framework/core/src/Post/Filter/NumberFilter.php +++ b/framework/core/src/Post/Filter/NumberFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Post\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class NumberFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Post/Filter/PostSearcher.php b/framework/core/src/Post/Filter/PostSearcher.php index 4b0d6dc9d9..4d017d36df 100644 --- a/framework/core/src/Post/Filter/PostSearcher.php +++ b/framework/core/src/Post/Filter/PostSearcher.php @@ -10,7 +10,7 @@ namespace Flarum\Post\Filter; use Flarum\Post\Post; -use Flarum\Search\AbstractSearcher; +use Flarum\Search\Database\AbstractSearcher; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; diff --git a/framework/core/src/Post/Filter/TypeFilter.php b/framework/core/src/Post/Filter/TypeFilter.php index 5dd00bb2df..5c9010f64e 100644 --- a/framework/core/src/Post/Filter/TypeFilter.php +++ b/framework/core/src/Post/Filter/TypeFilter.php @@ -9,10 +9,14 @@ namespace Flarum\Post\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; +/** + * @implements FilterInterface + */ class TypeFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/Search/AbstractDriver.php b/framework/core/src/Search/AbstractDriver.php new file mode 100644 index 0000000000..34329410c6 --- /dev/null +++ b/framework/core/src/Search/AbstractDriver.php @@ -0,0 +1,20 @@ +searchers; + } +} diff --git a/framework/core/src/Search/AbstractFulltextFilter.php b/framework/core/src/Search/AbstractFulltextFilter.php index b1b6f07a85..27c26e7d21 100644 --- a/framework/core/src/Search/AbstractFulltextFilter.php +++ b/framework/core/src/Search/AbstractFulltextFilter.php @@ -9,6 +9,12 @@ namespace Flarum\Search; +use Flarum\Search\Filter\FilterInterface; + +/** + * @template TState of SearchState + * @implements FilterInterface + */ abstract class AbstractFulltextFilter implements FilterInterface { public function getFilterKey(): string @@ -21,5 +27,8 @@ public function filter(SearchState $state, array|string $value, bool $negate): v $this->search($state, $value); } + /** + * @param TState $state + */ abstract public function search(SearchState $state, string $query): void; } diff --git a/framework/core/src/Search/AbstractSearcher.php b/framework/core/src/Search/Database/AbstractSearcher.php similarity index 77% rename from framework/core/src/Search/AbstractSearcher.php rename to framework/core/src/Search/Database/AbstractSearcher.php index 83b4b03a41..48c9a0e7ee 100644 --- a/framework/core/src/Search/AbstractSearcher.php +++ b/framework/core/src/Search/Database/AbstractSearcher.php @@ -7,13 +7,17 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Search; +namespace Flarum\Search\Database; +use Flarum\Search\Filter\FilterManager; +use Flarum\Search\SearchCriteria; +use Flarum\Search\SearcherInterface; +use Flarum\Search\SearchResults; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Str; -abstract class AbstractSearcher +abstract class AbstractSearcher implements SearcherInterface { public function __construct( protected FilterManager $filters, @@ -30,7 +34,8 @@ public function search(SearchCriteria $criteria, ?int $limit = null, int $offset $query = $this->getQuery($actor); - $search = new SearchState($query->getQuery(), $actor, in_array('q', array_keys($criteria->filters), true)); + $search = new DatabaseSearchState($actor, in_array('q', array_keys($criteria->filters), true)); + $search->setQuery($query->getQuery()); $this->filters->apply($search, $criteria->filters); @@ -54,7 +59,7 @@ public function search(SearchCriteria $criteria, ?int $limit = null, int $offset return new SearchResults($results, $areMoreResults); } - protected function applySort(SearchState $query, ?array $sort = null, bool $sortIsDefault = false): void + protected function applySort(DatabaseSearchState $query, ?array $sort = null, bool $sortIsDefault = false): void { if ($sortIsDefault && ! empty($query->getDefaultSort())) { $sort = $query->getDefaultSort(); @@ -75,14 +80,14 @@ protected function applySort(SearchState $query, ?array $sort = null, bool $sort } } - protected function applyOffset(SearchState $query, int $offset): void + protected function applyOffset(DatabaseSearchState $query, int $offset): void { if ($offset > 0) { $query->getQuery()->skip($offset); } } - protected function applyLimit(SearchState $query, ?int $limit): void + protected function applyLimit(DatabaseSearchState $query, ?int $limit): void { if ($limit > 0) { $query->getQuery()->take($limit); diff --git a/framework/core/src/Search/Database/DatabaseSearchDriver.php b/framework/core/src/Search/Database/DatabaseSearchDriver.php new file mode 100644 index 0000000000..8f2bb85df6 --- /dev/null +++ b/framework/core/src/Search/Database/DatabaseSearchDriver.php @@ -0,0 +1,13 @@ +query = $query; + } + + public function getQuery(): Builder + { + return $this->query; + } +} diff --git a/framework/core/src/Search/FilterInterface.php b/framework/core/src/Search/Filter/FilterInterface.php similarity index 77% rename from framework/core/src/Search/FilterInterface.php rename to framework/core/src/Search/Filter/FilterInterface.php index 0fd9d1e292..47592b3f70 100644 --- a/framework/core/src/Search/FilterInterface.php +++ b/framework/core/src/Search/Filter/FilterInterface.php @@ -7,8 +7,13 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Search; +namespace Flarum\Search\Filter; +use Flarum\Search\SearchState; + +/** + * @template TState of SearchState + */ interface FilterInterface { /** @@ -18,6 +23,8 @@ public function getFilterKey(): string; /** * Filters a query. + * + * @param TState $state */ 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/Filter/FilterManager.php similarity index 93% rename from framework/core/src/Search/FilterManager.php rename to framework/core/src/Search/Filter/FilterManager.php index 2f21dc2868..3ac0610e6d 100644 --- a/framework/core/src/Search/FilterManager.php +++ b/framework/core/src/Search/Filter/FilterManager.php @@ -7,7 +7,10 @@ * LICENSE file that was distributed with this source code. */ -namespace Flarum\Search; +namespace Flarum\Search\Filter; + +use Flarum\Search\AbstractFulltextFilter; +use Flarum\Search\SearchState; class FilterManager { diff --git a/framework/core/src/Search/SearchManager.php b/framework/core/src/Search/SearchManager.php new file mode 100644 index 0000000000..369692a26d --- /dev/null +++ b/framework/core/src/Search/SearchManager.php @@ -0,0 +1,42 @@ +> */ + protected array $drivers, + protected SettingsRepositoryInterface $settings, + protected Container $container + ) { + } + + public function driver(string $name): AbstractDriver + { + $driver = Arr::first($this->drivers, fn ($driver) => $driver::name() === $name); + + if (! $driver) { + throw new InvalidArgumentException("Driver `$name` is not defined."); + } + + return $this->container->make($driver); + } + + public function for(string $resourceClass): SearcherInterface + { + $driver = $this->driver($this->settings->get("search_driver_$resourceClass")); + $searchers = $driver->searchers(); + + if (! isset($searchers[$resourceClass])) { + throw new InvalidArgumentException("Driver {$driver::name()} does not support searching for $resourceClass."); + } + + return $this->container->make($searchers[$resourceClass]); + } +} diff --git a/framework/core/src/Search/SearchServiceProvider.php b/framework/core/src/Search/SearchServiceProvider.php index ea9251b13b..5cfa32a477 100644 --- a/framework/core/src/Search/SearchServiceProvider.php +++ b/framework/core/src/Search/SearchServiceProvider.php @@ -9,6 +9,7 @@ namespace Flarum\Search; +use Flarum\Discussion\Discussion; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Discussion\Search\Filter as DiscussionFilter; use Flarum\Discussion\Search\FulltextFilter as DiscussionFulltextFilter; @@ -16,13 +17,19 @@ use Flarum\Foundation\ContainerUtil; use Flarum\Group\Filter as GroupFilter; use Flarum\Group\Filter\GroupSearcher; -use Flarum\Http\Filter\AccessTokenSearcher; +use Flarum\Group\Group; +use Flarum\Http\AccessToken; use Flarum\Http\Filter as HttpFilter; +use Flarum\Http\Filter\AccessTokenSearcher; use Flarum\Post\Filter as PostFilter; use Flarum\Post\Filter\PostSearcher; +use Flarum\Post\Post; +use Flarum\Search\Filter\FilterManager; +use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\Search\Filter as UserFilter; use Flarum\User\Search\FulltextFilter as UserFulltextFilter; use Flarum\User\Search\UserSearcher; +use Flarum\User\User; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Arr; @@ -30,14 +37,36 @@ class SearchServiceProvider extends AbstractServiceProvider { public function register(): void { - $this->container->singleton('flarum.simple_search.fulltext_filters', function () { + $this->container->singleton('flarum.search', function (Container $container) { + return new SearchManager( + array_keys($container->make('flarum.search.drivers')), + $container->make(SettingsRepositoryInterface::class), + $container, + ); + }); + + $this->container->alias('flarum.search', SearchManager::class); + + $this->container->singleton('flarum.search.drivers', function () { + return [ + Database\DatabaseSearchDriver::class => [ + Discussion::class => DiscussionSearcher::class, + User::class => UserSearcher::class, + Post::class => PostSearcher::class, + Group::class => GroupSearcher::class, + AccessToken::class => AccessTokenSearcher::class, + ], + ]; + }); + + $this->container->singleton('flarum.search.fulltext', function () { return [ DiscussionSearcher::class => DiscussionFulltextFilter::class, UserSearcher::class => UserFulltextFilter::class ]; }); - $this->container->singleton('flarum.simple_search.filters', function () { + $this->container->singleton('flarum.search.filters', function () { return [ AccessTokenSearcher::class => [ HttpFilter\UserFilter::class, @@ -65,26 +94,33 @@ public function register(): void ]; }); - $this->container->singleton('flarum.simple_search.search_mutators', function () { + $this->container->singleton('flarum.search.mutators', function () { return []; }); } public function boot(Container $container): void { - foreach ($container->make('flarum.simple_search.filters') as $searcher => $filterClasses) { + foreach ($container->make('flarum.search.drivers') as $driverClass => $searchers) { + $container + ->when($driverClass) + ->needs('$searchers') + ->give($searchers); + } + + foreach ($container->make('flarum.search.filters') as $searcher => $filterClasses) { $container ->when($searcher) ->needs(FilterManager::class) ->give(function () use ($container, $searcher) { - $fulltext = $container->make('flarum.simple_search.fulltext_filters'); + $fulltext = $container->make('flarum.search.fulltext'); $fulltextClass = $fulltext[$searcher] ?? null; $manager = new FilterManager( $fulltextClass ? $container->make($fulltextClass) : null ); - foreach (Arr::get($container->make('flarum.simple_search.filters'), $searcher, []) as $filter) { + foreach (Arr::get($container->make('flarum.search.filters'), $searcher, []) as $filter) { $manager->add($container->make($filter)); } @@ -95,7 +131,7 @@ public function boot(Container $container): void ->when($searcher) ->needs('$mutators') ->give(function () use ($container, $searcher) { - $searchMutators = Arr::get($container->make('flarum.simple_search.search_mutators'), $searcher, []); + $searchMutators = Arr::get($container->make('flarum.search.mutators'), $searcher, []); return array_map(function ($mutator) { return ContainerUtil::wrapCallback($mutator, $this->container); diff --git a/framework/core/src/Search/SearchState.php b/framework/core/src/Search/SearchState.php index 24b4aad42e..6f0319feeb 100644 --- a/framework/core/src/Search/SearchState.php +++ b/framework/core/src/Search/SearchState.php @@ -1,17 +1,10 @@ query; - } - public function getActor(): User { return $this->actor; diff --git a/framework/core/src/Search/SearcherInterface.php b/framework/core/src/Search/SearcherInterface.php new file mode 100644 index 0000000000..bbf00f1add --- /dev/null +++ b/framework/core/src/Search/SearcherInterface.php @@ -0,0 +1,8 @@ + '#4D698E', 'theme_secondary_color' => '#4D698E', + 'search_driver_Flarum\User\User' => 'default', // @todo: use a morph map instead `User::class => 'user'` = search_driver_user (below as well) + 'search_driver_Flarum\Discussion\Discussion' => 'default', + 'search_driver_Flarum\Group\Group' => 'default', + 'search_driver_Flarum\Post\Post' => 'default', + 'search_driver_Flarum\Http\AccessToken' => 'default', ]); }); diff --git a/framework/core/src/User/Search/Filter/EmailFilter.php b/framework/core/src/User/Search/Filter/EmailFilter.php index dbb9867f72..c0e4eb49fe 100644 --- a/framework/core/src/User/Search/Filter/EmailFilter.php +++ b/framework/core/src/User/Search/Filter/EmailFilter.php @@ -9,11 +9,15 @@ namespace Flarum\User\Search\Filter; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class EmailFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/User/Search/Filter/GroupFilter.php b/framework/core/src/User/Search/Filter/GroupFilter.php index 3d9fac9a76..b1de2035c9 100644 --- a/framework/core/src/User/Search/Filter/GroupFilter.php +++ b/framework/core/src/User/Search/Filter/GroupFilter.php @@ -10,12 +10,16 @@ namespace Flarum\User\Search\Filter; use Flarum\Group\Group; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchState; use Flarum\Search\ValidateFilterTrait; use Flarum\User\User; use Illuminate\Database\Query\Builder; +/** + * @implements FilterInterface + */ class GroupFilter implements FilterInterface { use ValidateFilterTrait; diff --git a/framework/core/src/User/Search/FulltextFilter.php b/framework/core/src/User/Search/FulltextFilter.php index f26e8e75b3..1857acacc2 100644 --- a/framework/core/src/User/Search/FulltextFilter.php +++ b/framework/core/src/User/Search/FulltextFilter.php @@ -10,11 +10,15 @@ namespace Flarum\User\Search; use Flarum\Search\AbstractFulltextFilter; +use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; use Flarum\User\User; use Flarum\User\UserRepository; use Illuminate\Database\Eloquent\Builder; +/** + * @extends AbstractFulltextFilter + */ class FulltextFilter extends AbstractFulltextFilter { public function __construct( diff --git a/framework/core/src/User/Search/UserSearcher.php b/framework/core/src/User/Search/UserSearcher.php index c758e2f6bd..b5814e96ba 100644 --- a/framework/core/src/User/Search/UserSearcher.php +++ b/framework/core/src/User/Search/UserSearcher.php @@ -9,7 +9,7 @@ namespace Flarum\User\Search; -use Flarum\Search\AbstractSearcher; +use Flarum\Search\Database\AbstractSearcher; use Flarum\User\User; use Illuminate\Database\Eloquent\Builder; diff --git a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php b/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php index 7514a25e1a..cb43065182 100644 --- a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php +++ b/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php @@ -10,13 +10,17 @@ namespace Flarum\Tests\integration\extenders; use Carbon\Carbon; +use Flarum\Discussion\Discussion; use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; use Flarum\Group\Group; use Flarum\Search\AbstractFulltextFilter; -use Flarum\Search\AbstractSearcher; -use Flarum\Search\FilterInterface; +use Flarum\Search\Database\AbstractSearcher; +use Flarum\Search\Database\DatabaseSearchDriver; +use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchManager; use Flarum\Search\SearchState; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; @@ -72,7 +76,7 @@ public function searchDiscussions($query, $limit = null, array $filters = []) $criteria = new SearchCriteria($actor, $filters); - return $this->app()->getContainer()->make(DiscussionSearcher::class)->search($criteria, $limit)->getResults(); + return $this->app()->getContainer()->make(SearchManager::class)->for(Discussion::class)->search($criteria, $limit)->getResults(); } /** @@ -96,7 +100,10 @@ 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))->setFullTextFilter(NoResultFullTextFilter::class)); + $this->extend( + (new Extend\Search(DatabaseSearchDriver::class)) + ->setFulltext(DiscussionSearcher::class, NoResultFullTextFilter::class) + ); $this->assertEquals('[]', json_encode($this->searchDiscussions('in text', 5))); } @@ -106,7 +113,10 @@ public function custom_full_text_gambit_has_effect_if_added() */ public function custom_filter_has_effect_if_added() { - $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->addFilter(NoResultFilter::class)); + $this->extend( + (new Extend\Search(DatabaseSearchDriver::class)) + ->addFilter(DiscussionSearcher::class, NoResultFilter::class) + ); $this->prepDb(); @@ -121,9 +131,12 @@ public function custom_filter_has_effect_if_added() */ public function search_mutator_has_effect_if_added() { - $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->addSearchMutator(function ($search, $criteria) { - $search->getquery()->whereRaw('1=0'); - })); + $this->extend( + (new Extend\Search(DatabaseSearchDriver::class)) + ->addMutator(DiscussionSearcher::class, function (DatabaseSearchState $search) { + $search->getQuery()->whereRaw('1=0'); + }) + ); $this->prepDb(); @@ -134,42 +147,15 @@ public function search_mutator_has_effect_if_added() * @test */ public function search_mutator_has_effect_if_added_with_invokable_class() - { - $this->extend((new Extend\SimpleFlarumSearch(DiscussionSearcher::class))->addSearchMutator(CustomSearchMutator::class)); - - $this->prepDb(); - - $this->assertEquals('[]', json_encode($this->searchDiscussions('in text', 5))); - } - - /** - * @test - */ - public function cant_resolve_custom_searcher_without_fulltext_gambit() - { - $this->expectException(BindingResolutionException::class); - - $this->app()->getContainer()->make(CustomSearcher::class); - } - - /** - * @test - */ - public function can_resolve_custom_searcher_with_fulltext_gambit() { $this->extend( - (new Extend\SimpleFlarumSearch(CustomSearcher::class))->setFullTextFilter(CustomFullTextFilter::class) + (new Extend\Search(DatabaseSearchDriver::class)) + ->addMutator(DiscussionSearcher::class, CustomSearchMutator::class) ); - $anExceptionWasThrown = false; - - try { - $this->app()->getContainer()->make(CustomSearcher::class); - } catch (BindingResolutionException) { - $anExceptionWasThrown = true; - } + $this->prepDb(); - $this->assertFalse($anExceptionWasThrown); + $this->assertEquals('[]', json_encode($this->searchDiscussions('in text', 5))); } } @@ -181,6 +167,9 @@ public function search(SearchState $state, string $query): void } } +/** + * @implements FilterInterface + */ class NoResultFilter implements FilterInterface { public function getFilterKey(): string @@ -205,20 +194,3 @@ public function __invoke($search, $criteria) $search->getQuery()->whereRaw('1=0'); } } - -class CustomSearcher extends AbstractSearcher -{ - // This isn't actually used, we just need it to implement the abstract method. - protected function getQuery(User $actor): Builder - { - return Group::query(); - } -} - -class CustomFullTextFilter extends AbstractFulltextFilter -{ - public function search(SearchState $state, string $query): void - { - // - } -} From 343f1b26efd6f3cd81c23ee6ddc88158626dac7f Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 4 Oct 2023 19:43:02 +0100 Subject: [PATCH 02/15] feat: indexer API & tweaks --- extensions/likes/extend.php | 2 +- extensions/lock/extend.php | 2 +- extensions/mentions/extend.php | 2 +- extensions/nicknames/extend.php | 2 +- .../nicknames/src/NicknameFullTextFilter.php | 4 +- extensions/sticky/extend.php | 2 +- extensions/subscriptions/extend.php | 2 +- extensions/suspend/extend.php | 2 +- extensions/tags/extend.php | 2 +- .../src/Api/Controller/ListTagsController.php | 9 ++- extensions/tags/src/Search/FulltextFilter.php | 4 +- extensions/tags/src/Search/TagSearcher.php | 2 +- .../Controller/ListAccessTokensController.php | 2 +- .../Controller/ListDiscussionsController.php | 2 +- .../Api/Controller/ListGroupsController.php | 2 +- .../Api/Controller/ListPostsController.php | 2 +- .../Api/Controller/ListUsersController.php | 2 +- framework/core/src/Discussion/Discussion.php | 4 ++ .../Discussion/Search/DiscussionSearcher.php | 2 +- .../src/Discussion/Search/FulltextFilter.php | 16 ++--- .../Extend/{Search.php => SearchDriver.php} | 6 +- framework/core/src/Extend/SearchIndex.php | 44 ++++++++++++++ .../core/src/Group/Filter/GroupSearcher.php | 2 +- .../src/Http/Filter/AccessTokenSearcher.php | 2 +- .../core/src/Post/Filter/PostSearcher.php | 2 +- framework/core/src/Search/AbstractDriver.php | 8 +++ .../src/Search/AbstractFulltextFilter.php | 2 +- .../src/Search/Database/AbstractSearcher.php | 11 ++-- .../core/src/Search/Filter/FilterManager.php | 13 +++-- .../core/src/Search/IndexerInterface.php | 30 ++++++++++ framework/core/src/Search/Job/IndexJob.php | 37 ++++++++++++ .../src/Search/Listener/ModelObserver.php | 58 +++++++++++++++++++ framework/core/src/Search/SearchCriteria.php | 4 +- framework/core/src/Search/SearchManager.php | 35 ++++++++++- .../core/src/Search/SearchServiceProvider.php | 19 ++++++ framework/core/src/Search/SearchState.php | 4 +- .../core/src/Search/SearcherInterface.php | 7 ++- .../core/src/User/Search/FulltextFilter.php | 4 +- .../core/src/User/Search/UserSearcher.php | 2 +- framework/core/src/User/User.php | 11 ++++ .../extenders/SimpleFlarumSearchTest.php | 19 +++--- 41 files changed, 325 insertions(+), 62 deletions(-) rename framework/core/src/Extend/{Search.php => SearchDriver.php} (96%) create mode 100644 framework/core/src/Extend/SearchIndex.php create mode 100644 framework/core/src/Search/IndexerInterface.php create mode 100644 framework/core/src/Search/Job/IndexJob.php create mode 100644 framework/core/src/Search/Listener/ModelObserver.php diff --git a/extensions/likes/extend.php b/extensions/likes/extend.php index f22f698dfc..95ee5d1efe 100644 --- a/extensions/likes/extend.php +++ b/extensions/likes/extend.php @@ -77,7 +77,7 @@ ->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class) ->subscribe(Listener\SaveLikesToDatabase::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(PostSearcher::class, LikedByFilter::class) ->addFilter(UserSearcher::class, LikedFilter::class), diff --git a/extensions/lock/extend.php b/extensions/lock/extend.php index 11e1e125b0..26000af97a 100644 --- a/extensions/lock/extend.php +++ b/extensions/lock/extend.php @@ -57,6 +57,6 @@ (new Extend\Policy()) ->modelPolicy(Discussion::class, Access\DiscussionPolicy::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(DiscussionSearcher::class, LockedFilter::class), ]; diff --git a/extensions/mentions/extend.php b/extensions/mentions/extend.php index 70200ba79b..6f3c48f6e4 100644 --- a/extensions/mentions/extend.php +++ b/extensions/mentions/extend.php @@ -116,7 +116,7 @@ ->listen(Hidden::class, Listener\UpdateMentionsMetadataWhenInvisible::class) ->listen(Deleted::class, Listener\UpdateMentionsMetadataWhenInvisible::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(PostSearcher::class, Filter\MentionedFilter::class) ->addFilter(PostSearcher::class, Filter\MentionedPostFilter::class), diff --git a/extensions/nicknames/extend.php b/extensions/nicknames/extend.php index 8728d35e0e..a0394c6197 100644 --- a/extensions/nicknames/extend.php +++ b/extensions/nicknames/extend.php @@ -52,7 +52,7 @@ (new Extend\Validator(UserValidator::class)) ->configure(AddNicknameValidation::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->setFulltext(UserSearcher::class, NicknameFullTextFilter::class), (new Extend\Policy()) diff --git a/extensions/nicknames/src/NicknameFullTextFilter.php b/extensions/nicknames/src/NicknameFullTextFilter.php index e8135293bd..455f424be8 100644 --- a/extensions/nicknames/src/NicknameFullTextFilter.php +++ b/extensions/nicknames/src/NicknameFullTextFilter.php @@ -34,12 +34,12 @@ private function getUserSearchSubQuery(string $searchValue): Builder ->orWhere('nickname', 'like', "{$searchValue}%"); } - public function search(SearchState $state, string $query): void + public function search(SearchState $state, string $value): void { $state->getQuery() ->whereIn( 'id', - $this->getUserSearchSubQuery($query) + $this->getUserSearchSubQuery($value) ); } } diff --git a/extensions/sticky/extend.php b/extensions/sticky/extend.php index 25b85d121b..4ca43eec05 100644 --- a/extensions/sticky/extend.php +++ b/extensions/sticky/extend.php @@ -54,7 +54,7 @@ ->listen(DiscussionWasStickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasStickied']) ->listen(DiscussionWasUnstickied::class, [Listener\CreatePostWhenDiscussionIsStickied::class, 'whenDiscussionWasUnstickied']), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(DiscussionSearcher::class, StickyFilter::class) ->addMutator(DiscussionSearcher::class, PinStickiedDiscussionsToTop::class), ]; diff --git a/extensions/subscriptions/extend.php b/extensions/subscriptions/extend.php index f19feecd4f..f114e11fea 100644 --- a/extensions/subscriptions/extend.php +++ b/extensions/subscriptions/extend.php @@ -70,7 +70,7 @@ ->listen(Deleted::class, Listener\DeleteNotificationWhenPostIsHiddenOrDeleted::class) ->listen(Posted::class, Listener\FollowAfterReply::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(DiscussionSearcher::class, SubscriptionFilter::class) ->addMutator(DiscussionSearcher::class, HideIgnoredFromAllDiscussionsPage::class), diff --git a/extensions/suspend/extend.php b/extensions/suspend/extend.php index 549d2f8503..fe4c1c87e3 100644 --- a/extensions/suspend/extend.php +++ b/extensions/suspend/extend.php @@ -58,7 +58,7 @@ (new Extend\User()) ->permissionGroups(RevokeAccessFromSuspendedUsers::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(UserSearcher::class, SuspendedFilter::class), (new Extend\View()) diff --git a/extensions/tags/extend.php b/extensions/tags/extend.php index d63a857f1d..4ba4735e81 100644 --- a/extensions/tags/extend.php +++ b/extensions/tags/extend.php @@ -135,7 +135,7 @@ ->listen(DiscussionWasTagged::class, Listener\CreatePostWhenTagsAreChanged::class) ->subscribe(Listener\UpdateTagMetadata::class), - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(PostSearcher::class, PostTagFilter::class) ->addFilter(DiscussionSearcher::class, TagFilter::class) ->addMutator(DiscussionSearcher::class, HideHiddenTagsFromAllDiscussionsPage::class) diff --git a/extensions/tags/src/Api/Controller/ListTagsController.php b/extensions/tags/src/Api/Controller/ListTagsController.php index 511c887dd4..26ff9620c5 100644 --- a/extensions/tags/src/Api/Controller/ListTagsController.php +++ b/extensions/tags/src/Api/Controller/ListTagsController.php @@ -13,8 +13,10 @@ use Flarum\Http\RequestUtil; use Flarum\Http\UrlGenerator; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchManager; use Flarum\Tags\Api\Serializer\TagSerializer; use Flarum\Tags\Search\TagSearcher; +use Flarum\Tags\Tag; use Flarum\Tags\TagRepository; use Psr\Http\Message\ServerRequestInterface; use Tobscure\JsonApi\Document; @@ -35,7 +37,7 @@ class ListTagsController extends AbstractListController public function __construct( protected TagRepository $tags, - protected TagSearcher $searcher, + protected SearchManager $search, protected UrlGenerator $url ) { } @@ -53,7 +55,10 @@ protected function data(ServerRequestInterface $request, Document $document): it } if (array_key_exists('q', $filters)) { - $results = $this->searcher->search(new SearchCriteria($actor, $filters), $limit, $offset); + $results = $this->search + ->for(Tag::class) + ->search(new SearchCriteria($actor, $filters, $limit, $offset)); + $tags = $results->getResults(); $document->addPaginationLinks( diff --git a/extensions/tags/src/Search/FulltextFilter.php b/extensions/tags/src/Search/FulltextFilter.php index 03ca19549d..adc71fe7cb 100644 --- a/extensions/tags/src/Search/FulltextFilter.php +++ b/extensions/tags/src/Search/FulltextFilter.php @@ -34,12 +34,12 @@ private function getTagSearchSubQuery(string $searchValue): Builder ->orWhere('slug', 'like', "$searchValue%"); } - public function search(SearchState $state, string $query): void + public function search(SearchState $state, string $value): void { $state->getQuery() ->whereIn( 'id', - $this->getTagSearchSubQuery($query) + $this->getTagSearchSubQuery($value) ); } } diff --git a/extensions/tags/src/Search/TagSearcher.php b/extensions/tags/src/Search/TagSearcher.php index a453af95c2..9224030d71 100644 --- a/extensions/tags/src/Search/TagSearcher.php +++ b/extensions/tags/src/Search/TagSearcher.php @@ -16,7 +16,7 @@ class TagSearcher extends AbstractSearcher { - protected function getQuery(User $actor): Builder + public function getQuery(User $actor): Builder { return Tag::whereVisibleTo($actor)->select('tags.*'); } diff --git a/framework/core/src/Api/Controller/ListAccessTokensController.php b/framework/core/src/Api/Controller/ListAccessTokensController.php index 5ff2735e80..5c8a8211cc 100644 --- a/framework/core/src/Api/Controller/ListAccessTokensController.php +++ b/framework/core/src/Api/Controller/ListAccessTokensController.php @@ -40,7 +40,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $tokens = $this->search ->for(AccessToken::class) - ->search(new SearchCriteria($actor, $filter), $limit, $offset); + ->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 3c41829d1f..a5ba1a6f8d 100644 --- a/framework/core/src/Api/Controller/ListDiscussionsController.php +++ b/framework/core/src/Api/Controller/ListDiscussionsController.php @@ -57,7 +57,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $results = $this->search ->for(Discussion::class) - ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); + ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); $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 82bc28f50b..b3c5c4ce85 100644 --- a/framework/core/src/Api/Controller/ListGroupsController.php +++ b/framework/core/src/Api/Controller/ListGroupsController.php @@ -45,7 +45,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $queryResults = $this->search ->for(Group::class) - ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); + ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); $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 a2b53eab85..21768de32f 100644 --- a/framework/core/src/Api/Controller/ListPostsController.php +++ b/framework/core/src/Api/Controller/ListPostsController.php @@ -56,7 +56,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $results = $this->search ->for(Post::class) - ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); + ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); $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 744db3a390..00136b4569 100644 --- a/framework/core/src/Api/Controller/ListUsersController.php +++ b/framework/core/src/Api/Controller/ListUsersController.php @@ -61,7 +61,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $results = $this->search ->for(User::class) - ->search(new SearchCriteria($actor, $filters, $sort, $sortIsDefault), $limit, $offset); + ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); $document->addPaginationLinks( $this->url->to('api')->route('users.index'), diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index 7cd4a1feb9..321d9c2575 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -143,6 +143,8 @@ public function hide(?User $actor = null): static $this->hidden_user_id = $actor?->id; $this->raise(new Hidden($this)); + + $this->fireCustomModelEvent('hidden', false); } return $this; @@ -155,6 +157,8 @@ public function restore(): static $this->hidden_user_id = null; $this->raise(new Restored($this)); + + $this->fireCustomModelEvent('restored', false); } return $this; diff --git a/framework/core/src/Discussion/Search/DiscussionSearcher.php b/framework/core/src/Discussion/Search/DiscussionSearcher.php index 912b5a369a..a9438c5205 100644 --- a/framework/core/src/Discussion/Search/DiscussionSearcher.php +++ b/framework/core/src/Discussion/Search/DiscussionSearcher.php @@ -16,7 +16,7 @@ class DiscussionSearcher extends AbstractSearcher { - protected function getQuery(User $actor): Builder + public function getQuery(User $actor): Builder { return Discussion::whereVisibleTo($actor)->select('discussions.*'); } diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 8f0968dbcb..62b44b7792 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -21,12 +21,12 @@ */ class FulltextFilter extends AbstractFulltextFilter { - public function search(SearchState $state, string $query): void + public function search(SearchState $state, string $value): 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', ' ', $query); + $value = preg_replace('/[^\p{L}\p{N}\p{M}_]+/u', ' ', $value); $query = $state->getQuery(); $grammar = $query->getGrammar(); @@ -34,7 +34,7 @@ public function search(SearchState $state, string $query): void $discussionSubquery = Discussion::select('id') ->selectRaw('NULL as score') ->selectRaw('first_post_id as most_relevant_post_id') - ->whereRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (? IN BOOLEAN MODE)', [$bit]); + ->whereRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (? IN BOOLEAN MODE)', [$value]); // Construct a subquery to fetch discussions which contain relevant // posts. Retrieve the collective relevance of each discussion's posts, @@ -42,10 +42,10 @@ public function search(SearchState $state, string $query): void // the ID of the most relevant post. $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]) + ->selectRaw('SUM(MATCH('.$grammar->wrap('posts.content').') AGAINST (?)) as score', [$value]) + ->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', [$value]) ->where('posts.type', 'comment') - ->whereRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)', [$bit]) + ->whereRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)', [$value]) ->groupBy('posts.discussion_id') ->union($discussionSubquery); @@ -62,8 +62,8 @@ public function search(SearchState $state, string $query): void ->groupBy('discussions.id') ->addBinding($subquery->getBindings(), 'join'); - $state->setDefaultSort(function ($query) use ($grammar, $bit) { - $query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$bit]); + $state->setDefaultSort(function ($query) use ($grammar, $value) { + $query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$value]); $query->orderBy('posts_ft.score', 'desc'); }); } diff --git a/framework/core/src/Extend/Search.php b/framework/core/src/Extend/SearchDriver.php similarity index 96% rename from framework/core/src/Extend/Search.php rename to framework/core/src/Extend/SearchDriver.php index 9f4be8147d..114fefe870 100644 --- a/framework/core/src/Extend/Search.php +++ b/framework/core/src/Extend/SearchDriver.php @@ -13,12 +13,12 @@ use Flarum\Search\AbstractDriver; use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\Database\AbstractSearcher; -use Flarum\Search\Database\DatabaseSearchState; +use Flarum\Search\SearchState; use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchCriteria; use Illuminate\Contracts\Container\Container; -class Search implements ExtenderInterface +class SearchDriver implements ExtenderInterface { private array $searchers = []; private array $fulltext = []; @@ -91,7 +91,7 @@ public function setFulltext(string $searcherClass, string $fulltextClass): self * @param class-string $searcherClass : The class of the Searcher for this model * This searcher must implement \Flarum\Search\SearcherInterface. * Or extend \Flarum\Search\Database\AbstractSearcher if using the default driver. - * @param (callable(DatabaseSearchState $search, SearchCriteria $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 diff --git a/framework/core/src/Extend/SearchIndex.php b/framework/core/src/Extend/SearchIndex.php new file mode 100644 index 0000000000..5f42be1cf1 --- /dev/null +++ b/framework/core/src/Extend/SearchIndex.php @@ -0,0 +1,44 @@ +indexers[$resourceClass][] = $indexerClass; + + return $this; + } + + public function extend(Container $container, Extension $extension = null): void + { + if (empty($this->indexers)) { + return; + } + + $container->extend('flarum.search.indexers', function (array $indexers) { + foreach ($this->indexers as $resourceClass => $indexerClasses) { + $indexers[$resourceClass] = array_merge( + $indexers[$resourceClass] ?? [], + $indexerClasses + ); + } + + return $indexers; + }); + } +} diff --git a/framework/core/src/Group/Filter/GroupSearcher.php b/framework/core/src/Group/Filter/GroupSearcher.php index 745522cfa0..45569dccea 100644 --- a/framework/core/src/Group/Filter/GroupSearcher.php +++ b/framework/core/src/Group/Filter/GroupSearcher.php @@ -16,7 +16,7 @@ class GroupSearcher extends AbstractSearcher { - protected function getQuery(User $actor): Builder + public function getQuery(User $actor): Builder { return Group::whereVisibleTo($actor)->select('groups.*'); } diff --git a/framework/core/src/Http/Filter/AccessTokenSearcher.php b/framework/core/src/Http/Filter/AccessTokenSearcher.php index 09e384d0e1..8fd3d9d9a9 100644 --- a/framework/core/src/Http/Filter/AccessTokenSearcher.php +++ b/framework/core/src/Http/Filter/AccessTokenSearcher.php @@ -16,7 +16,7 @@ class AccessTokenSearcher extends AbstractSearcher { - protected function getQuery(User $actor): Builder + public function getQuery(User $actor): Builder { return AccessToken::query()->whereVisibleTo($actor); } diff --git a/framework/core/src/Post/Filter/PostSearcher.php b/framework/core/src/Post/Filter/PostSearcher.php index 4d017d36df..46e510dc5c 100644 --- a/framework/core/src/Post/Filter/PostSearcher.php +++ b/framework/core/src/Post/Filter/PostSearcher.php @@ -16,7 +16,7 @@ class PostSearcher extends AbstractSearcher { - protected function getQuery(User $actor): Builder + public function getQuery(User $actor): Builder { return Post::whereVisibleTo($actor)->select('posts.*'); } diff --git a/framework/core/src/Search/AbstractDriver.php b/framework/core/src/Search/AbstractDriver.php index 34329410c6..d35119127b 100644 --- a/framework/core/src/Search/AbstractDriver.php +++ b/framework/core/src/Search/AbstractDriver.php @@ -7,6 +7,9 @@ abstract class AbstractDriver { public function __construct( + /** + * @var array, class-string> + */ protected array $searchers ) { } @@ -17,4 +20,9 @@ public function searchers(): array { return $this->searchers; } + + public function supports(string $modelClass): bool + { + return isset($this->searchers[$modelClass]); + } } diff --git a/framework/core/src/Search/AbstractFulltextFilter.php b/framework/core/src/Search/AbstractFulltextFilter.php index 27c26e7d21..c9c6f5b830 100644 --- a/framework/core/src/Search/AbstractFulltextFilter.php +++ b/framework/core/src/Search/AbstractFulltextFilter.php @@ -30,5 +30,5 @@ public function filter(SearchState $state, array|string $value, bool $negate): v /** * @param TState $state */ - abstract public function search(SearchState $state, string $query): void; + abstract public function search(SearchState $state, string $value): void; } diff --git a/framework/core/src/Search/Database/AbstractSearcher.php b/framework/core/src/Search/Database/AbstractSearcher.php index 48c9a0e7ee..ccc2162ef5 100644 --- a/framework/core/src/Search/Database/AbstractSearcher.php +++ b/framework/core/src/Search/Database/AbstractSearcher.php @@ -10,6 +10,7 @@ namespace Flarum\Search\Database; use Flarum\Search\Filter\FilterManager; +use Flarum\Search\IndexerInterface; use Flarum\Search\SearchCriteria; use Flarum\Search\SearcherInterface; use Flarum\Search\SearchResults; @@ -26,9 +27,7 @@ public function __construct( ) { } - abstract protected function getQuery(User $actor): Builder; - - public function search(SearchCriteria $criteria, ?int $limit = null, int $offset = 0): SearchResults + public function search(SearchCriteria $criteria): SearchResults { $actor = $criteria->actor; @@ -40,8 +39,8 @@ public function search(SearchCriteria $criteria, ?int $limit = null, int $offset $this->filters->apply($search, $criteria->filters); $this->applySort($search, $criteria->sort, $criteria->sortIsDefault); - $this->applyOffset($search, $offset); - $this->applyLimit($search, $limit + 1); + $this->applyOffset($search, $criteria->offset); + $this->applyLimit($search, $criteria->limit + 1); foreach ($this->mutators as $mutator) { $mutator($search, $criteria); @@ -52,7 +51,7 @@ public function search(SearchCriteria $criteria, ?int $limit = null, int $offset // results. If there are, we will get rid of that extra result. $results = $query->get(); - if ($areMoreResults = $limit > 0 && $results->count() > $limit) { + if ($areMoreResults = $criteria->limit > 0 && $results->count() > $criteria->limit) { $results->pop(); } diff --git a/framework/core/src/Search/Filter/FilterManager.php b/framework/core/src/Search/Filter/FilterManager.php index 3ac0610e6d..8164ab86a9 100644 --- a/framework/core/src/Search/Filter/FilterManager.php +++ b/framework/core/src/Search/Filter/FilterManager.php @@ -20,7 +20,7 @@ class FilterManager protected array $filters = []; public function __construct( - protected ?AbstractFulltextFilter $fulltextGambit = null + protected ?AbstractFulltextFilter $fulltext = null ) { } @@ -54,9 +54,14 @@ protected function applyFilters(SearchState $search, array $filters): void protected function applyFulltext(SearchState $search, ?string $query): void { - if ($this->fulltextGambit && $query) { - $search->addActiveFilter($this->fulltextGambit); - $this->fulltextGambit->search($search, $query); + if ($this->fulltext && $query) { + $search->addActiveFilter($this->fulltext); + $this->fulltext->search($search, $query); } } + + public function getFulltext(): ?AbstractFulltextFilter + { + return $this->fulltext; + } } diff --git a/framework/core/src/Search/IndexerInterface.php b/framework/core/src/Search/IndexerInterface.php new file mode 100644 index 0000000000..cf75406dfa --- /dev/null +++ b/framework/core/src/Search/IndexerInterface.php @@ -0,0 +1,30 @@ +models)) { + return; + } + + $indexer = $container->make($this->indexerClass); + + $indexer->{$this->operation}($this->models); + } +} diff --git a/framework/core/src/Search/Listener/ModelObserver.php b/framework/core/src/Search/Listener/ModelObserver.php new file mode 100644 index 0000000000..046ecc7d7d --- /dev/null +++ b/framework/core/src/Search/Listener/ModelObserver.php @@ -0,0 +1,58 @@ +runIndexJob($model, IndexJob::SAVE); + } + + public function updated(AbstractModel $model): void + { + $this->runIndexJob($model, IndexJob::SAVE); + } + + public function hidden(AbstractModel $model): void + { + $this->runIndexJob($model, IndexJob::DELETE); + } + + public function deleted(AbstractModel $model): void + { + $this->runIndexJob($model, IndexJob::DELETE); + } + + public function forceDeleted(AbstractModel $model): void + { + $this->runIndexJob($model, IndexJob::DELETE); + } + + public function restored(AbstractModel $model): void + { + $this->runIndexJob($model, IndexJob::SAVE); + } + + private function runIndexJob(AbstractModel $model, string $operation): void + { + if ($this->search->indexable($model)) { + foreach ($this->search->indexers($model) as $indexerClass) { + $queue = property_exists($indexerClass, 'queue') ? $indexerClass::$queue : null; + + $this->queue->pushOn($queue, new IndexJob($indexerClass, [$model], $operation)); + } + } + } +} diff --git a/framework/core/src/Search/SearchCriteria.php b/framework/core/src/Search/SearchCriteria.php index dfd8fc08a0..1f7382ce9e 100644 --- a/framework/core/src/Search/SearchCriteria.php +++ b/framework/core/src/Search/SearchCriteria.php @@ -21,6 +21,8 @@ class SearchCriteria public function __construct( public User $actor, public array $filters, + public ?int $limit = null, + public int $offset = 0, /** * 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 @@ -33,7 +35,7 @@ public function __construct( * Is the sort for this request the default sort from the controller? * If false, the current request specifies a sort. */ - public bool $sortIsDefault = false + public bool $sortIsDefault = false, ) { } } diff --git a/framework/core/src/Search/SearchManager.php b/framework/core/src/Search/SearchManager.php index 369692a26d..898b938622 100644 --- a/framework/core/src/Search/SearchManager.php +++ b/framework/core/src/Search/SearchManager.php @@ -2,6 +2,8 @@ namespace Flarum\Search; +use Flarum\Database\AbstractModel; +use Flarum\Search\Database\DatabaseSearchDriver; use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Arr; @@ -12,6 +14,8 @@ class SearchManager public function __construct( /** @var array> */ protected array $drivers, + /** @var array, array>> */ + protected array $indexers, protected SettingsRepositoryInterface $settings, protected Container $container ) { @@ -22,15 +26,28 @@ public function driver(string $name): AbstractDriver $driver = Arr::first($this->drivers, fn ($driver) => $driver::name() === $name); if (! $driver) { - throw new InvalidArgumentException("Driver `$name` is not defined."); + $driver = $this->driver(DatabaseSearchDriver::name()); } return $this->container->make($driver); } + public function driverFor(string $resourceClass): AbstractDriver + { + return $this->driver($this->settings->get("search_driver_$resourceClass")); + } + + public function searchable(string $resourceClass): bool + { + return $this->driverFor($resourceClass)->supports($resourceClass); + } + + /** + * @param class-string $resourceClass + */ public function for(string $resourceClass): SearcherInterface { - $driver = $this->driver($this->settings->get("search_driver_$resourceClass")); + $driver = $this->driverFor($resourceClass); $searchers = $driver->searchers(); if (! isset($searchers[$resourceClass])) { @@ -39,4 +56,18 @@ public function for(string $resourceClass): SearcherInterface return $this->container->make($searchers[$resourceClass]); } + + /** + * @param class-string $resourceClass + * @return array> + */ + public function indexers(string $resourceClass): array + { + return $this->indexers[$resourceClass] ?? []; + } + + public function indexable(string $resourceClass): bool + { + return ! empty($this->indexers[$resourceClass]); + } } diff --git a/framework/core/src/Search/SearchServiceProvider.php b/framework/core/src/Search/SearchServiceProvider.php index 5cfa32a477..1f983be025 100644 --- a/framework/core/src/Search/SearchServiceProvider.php +++ b/framework/core/src/Search/SearchServiceProvider.php @@ -25,12 +25,14 @@ use Flarum\Post\Filter\PostSearcher; use Flarum\Post\Post; use Flarum\Search\Filter\FilterManager; +use Flarum\Search\Listener\ModelObserver; use Flarum\Settings\SettingsRepositoryInterface; use Flarum\User\Search\Filter as UserFilter; use Flarum\User\Search\FulltextFilter as UserFulltextFilter; use Flarum\User\Search\UserSearcher; use Flarum\User\User; use Illuminate\Contracts\Container\Container; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; class SearchServiceProvider extends AbstractServiceProvider @@ -40,6 +42,7 @@ public function register(): void $this->container->singleton('flarum.search', function (Container $container) { return new SearchManager( array_keys($container->make('flarum.search.drivers')), + $container->make('flarum.search.indexers'), $container->make(SettingsRepositoryInterface::class), $container, ); @@ -97,6 +100,17 @@ public function register(): void $this->container->singleton('flarum.search.mutators', function () { return []; }); + + // Indexers aren't driver specific. + // For example, a search driver implementation may support searching discussions, + // and would need to index discussions for that, but it would also need to index + // posts without supporting searching them, because it needs to index the posts for + // searching discussions. + $this->container->singleton('flarum.search.indexers', function () { + return [ + // Model::class => [...], + ]; + }); } public function boot(Container $container): void @@ -138,5 +152,10 @@ public function boot(Container $container): void }, $searchMutators); }); } + + /** @var \Flarum\Database\AbstractModel $modelClass */ + foreach ($container->make('flarum.search.indexers') as $modelClass => $indexers) { + $modelClass::observe(ModelObserver::class); + } } } diff --git a/framework/core/src/Search/SearchState.php b/framework/core/src/Search/SearchState.php index 6f0319feeb..5c5c6e3796 100644 --- a/framework/core/src/Search/SearchState.php +++ b/framework/core/src/Search/SearchState.php @@ -15,7 +15,9 @@ class SearchState public final function __construct( protected User $actor, - /** Whether this is a fulltext search or just filtering. */ + /** + * Whether this is a fulltext search or just filtering. + */ protected bool $fulltextSearch, /** * An array of sort-order pairs, where the column diff --git a/framework/core/src/Search/SearcherInterface.php b/framework/core/src/Search/SearcherInterface.php index bbf00f1add..e8f525759a 100644 --- a/framework/core/src/Search/SearcherInterface.php +++ b/framework/core/src/Search/SearcherInterface.php @@ -2,7 +2,12 @@ namespace Flarum\Search; +use Flarum\User\User; +use Illuminate\Database\Eloquent\Builder; + interface SearcherInterface { - public function search(SearchCriteria $criteria, ?int $limit = null, int $offset = 0): SearchResults; + function getQuery(User $actor): Builder; + + function search(SearchCriteria $criteria): SearchResults; } diff --git a/framework/core/src/User/Search/FulltextFilter.php b/framework/core/src/User/Search/FulltextFilter.php index 1857acacc2..3f862d7365 100644 --- a/framework/core/src/User/Search/FulltextFilter.php +++ b/framework/core/src/User/Search/FulltextFilter.php @@ -37,12 +37,12 @@ private function getUserSearchSubQuery(string $searchValue): Builder ->where('username', 'like', "$searchValue%"); } - public function search(SearchState $state, string $query): void + public function search(SearchState $state, string $value): void { $state->getQuery() ->whereIn( 'id', - $this->getUserSearchSubQuery($query) + $this->getUserSearchSubQuery($value) ); } } diff --git a/framework/core/src/User/Search/UserSearcher.php b/framework/core/src/User/Search/UserSearcher.php index b5814e96ba..118e34bd26 100644 --- a/framework/core/src/User/Search/UserSearcher.php +++ b/framework/core/src/User/Search/UserSearcher.php @@ -15,7 +15,7 @@ class UserSearcher extends AbstractSearcher { - protected function getQuery(User $actor): Builder + public function getQuery(User $actor): Builder { return User::whereVisibleTo($actor)->select('users.*'); } diff --git a/framework/core/src/User/User.php b/framework/core/src/User/User.php index a96f63adb1..3d88c69ae1 100644 --- a/framework/core/src/User/User.php +++ b/framework/core/src/User/User.php @@ -55,6 +55,17 @@ * @property \Carbon\Carbon|null $read_notifications_at * @property int $discussion_count * @property int $comment_count + * @property-read Collection $groups + * @property-read Collection $visibleGroups + * @property-read Collection $notifications + * @property-read Collection $accessTokens + * @property-read Collection $posts + * @property-read Collection $discussions + * @property-read Collection $read + * @property-read Collection $unreadNotifications + * @property-read Collection $loginProviders + * @property-read Collection $emailTokens + * @property-read Collection $passwordTokens */ class User extends AbstractModel { diff --git a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php b/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php index cb43065182..1908f99a0b 100644 --- a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php +++ b/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php @@ -74,9 +74,12 @@ public function searchDiscussions($query, $limit = null, array $filters = []) $filters['q'] = $query; - $criteria = new SearchCriteria($actor, $filters); - - return $this->app()->getContainer()->make(SearchManager::class)->for(Discussion::class)->search($criteria, $limit)->getResults(); + return $this->app() + ->getContainer() + ->make(SearchManager::class) + ->for(Discussion::class) + ->search(new SearchCriteria($actor, $filters, $limit)) + ->getResults(); } /** @@ -101,7 +104,7 @@ public function works_as_expected_with_no_modifications() public function custom_full_text_gambit_has_effect_if_added() { $this->extend( - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->setFulltext(DiscussionSearcher::class, NoResultFullTextFilter::class) ); @@ -114,7 +117,7 @@ public function custom_full_text_gambit_has_effect_if_added() public function custom_filter_has_effect_if_added() { $this->extend( - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addFilter(DiscussionSearcher::class, NoResultFilter::class) ); @@ -132,7 +135,7 @@ public function custom_filter_has_effect_if_added() public function search_mutator_has_effect_if_added() { $this->extend( - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addMutator(DiscussionSearcher::class, function (DatabaseSearchState $search) { $search->getQuery()->whereRaw('1=0'); }) @@ -149,7 +152,7 @@ public function search_mutator_has_effect_if_added() public function search_mutator_has_effect_if_added_with_invokable_class() { $this->extend( - (new Extend\Search(DatabaseSearchDriver::class)) + (new Extend\SearchDriver(DatabaseSearchDriver::class)) ->addMutator(DiscussionSearcher::class, CustomSearchMutator::class) ); @@ -161,7 +164,7 @@ public function search_mutator_has_effect_if_added_with_invokable_class() class NoResultFullTextFilter extends AbstractFulltextFilter { - public function search(SearchState $state, string $query): void + public function search(SearchState $state, string $value): void { $state->getQuery()->whereRaw('0=1'); } From b05d6d83bc84987534c6ae666aa5cff25a449214 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Thu, 12 Oct 2023 19:00:43 +0100 Subject: [PATCH 03/15] feat: changes after POC driver --- .../src/Api/Controller/ListTagsController.php | 4 +-- framework/core/src/Api/ApiServiceProvider.php | 2 +- .../Controller/ListAccessTokensController.php | 4 +-- .../Controller/ListDiscussionsController.php | 7 ++-- .../Api/Controller/ListGroupsController.php | 7 ++-- .../Api/Controller/ListPostsController.php | 7 ++-- .../Api/Controller/ListUsersController.php | 7 ++-- .../src/Discussion/Search/FulltextFilter.php | 3 +- framework/core/src/Search/AbstractDriver.php | 11 ++++-- .../src/Search/Database/AbstractSearcher.php | 22 ++++++------ framework/core/src/Search/SearchCriteria.php | 5 +++ framework/core/src/Search/SearchManager.php | 35 +++++++------------ .../src/Settings/SettingsServiceProvider.php | 2 +- 13 files changed, 60 insertions(+), 56 deletions(-) diff --git a/extensions/tags/src/Api/Controller/ListTagsController.php b/extensions/tags/src/Api/Controller/ListTagsController.php index 26ff9620c5..0715ebcbfc 100644 --- a/extensions/tags/src/Api/Controller/ListTagsController.php +++ b/extensions/tags/src/Api/Controller/ListTagsController.php @@ -55,9 +55,7 @@ protected function data(ServerRequestInterface $request, Document $document): it } if (array_key_exists('q', $filters)) { - $results = $this->search - ->for(Tag::class) - ->search(new SearchCriteria($actor, $filters, $limit, $offset)); + $results = $this->search->query(Tag::class, new SearchCriteria($actor, $filters, $limit, $offset)); $tags = $results->getResults(); diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index acd7a8b636..91a481ced6 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -56,7 +56,7 @@ public function register(): void $this->container->singleton('flarum.api.middleware', function () { return [ HttpMiddleware\InjectActorReference::class, - 'flarum.api.error_handler', +// 'flarum.api.error_handler', HttpMiddleware\ParseJsonBody::class, Middleware\FakeHttpMethods::class, HttpMiddleware\StartSession::class, diff --git a/framework/core/src/Api/Controller/ListAccessTokensController.php b/framework/core/src/Api/Controller/ListAccessTokensController.php index 5c8a8211cc..98a3eeb14b 100644 --- a/framework/core/src/Api/Controller/ListAccessTokensController.php +++ b/framework/core/src/Api/Controller/ListAccessTokensController.php @@ -38,9 +38,7 @@ protected function data(ServerRequestInterface $request, Document $document): it $limit = $this->extractLimit($request); $filter = $this->extractFilter($request); - $tokens = $this->search - ->for(AccessToken::class) - ->search(new SearchCriteria($actor, $filter, $limit, $offset)); + $tokens = $this->search->query(AccessToken::class, 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 a5ba1a6f8d..22043b7eb4 100644 --- a/framework/core/src/Api/Controller/ListDiscussionsController.php +++ b/framework/core/src/Api/Controller/ListDiscussionsController.php @@ -55,9 +55,10 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = array_merge($this->extractInclude($request), ['state']); - $results = $this->search - ->for(Discussion::class) - ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); + $results = $this->search->query( + Discussion::class, + new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault) + ); $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 b3c5c4ce85..935964e3b0 100644 --- a/framework/core/src/Api/Controller/ListGroupsController.php +++ b/framework/core/src/Api/Controller/ListGroupsController.php @@ -43,9 +43,10 @@ protected function data(ServerRequestInterface $request, Document $document): it $limit = $this->extractLimit($request); $offset = $this->extractOffset($request); - $queryResults = $this->search - ->for(Group::class) - ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); + $queryResults = $this->search->query( + Group::class, + new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault) + ); $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 21768de32f..4c419bb640 100644 --- a/framework/core/src/Api/Controller/ListPostsController.php +++ b/framework/core/src/Api/Controller/ListPostsController.php @@ -54,9 +54,10 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = $this->extractInclude($request); - $results = $this->search - ->for(Post::class) - ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); + $results = $this->search->query( + Post::class, + new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault) + ); $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 00136b4569..0547afc71b 100644 --- a/framework/core/src/Api/Controller/ListUsersController.php +++ b/framework/core/src/Api/Controller/ListUsersController.php @@ -59,9 +59,10 @@ protected function data(ServerRequestInterface $request, Document $document): it $offset = $this->extractOffset($request); $include = $this->extractInclude($request); - $results = $this->search - ->for(User::class) - ->search(new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault)); + $results = $this->search->query( + User::class, + new SearchCriteria($actor, $filters, $limit, $offset, $sort, $sortIsDefault) + ); $document->addPaginationLinks( $this->url->to('api')->route('users.index'), diff --git a/framework/core/src/Discussion/Search/FulltextFilter.php b/framework/core/src/Discussion/Search/FulltextFilter.php index 62b44b7792..da0b62a039 100644 --- a/framework/core/src/Discussion/Search/FulltextFilter.php +++ b/framework/core/src/Discussion/Search/FulltextFilter.php @@ -14,6 +14,7 @@ use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\Database\DatabaseSearchState; use Flarum\Search\SearchState; +use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Expression; /** @@ -62,7 +63,7 @@ public function search(SearchState $state, string $value): void ->groupBy('discussions.id') ->addBinding($subquery->getBindings(), 'join'); - $state->setDefaultSort(function ($query) use ($grammar, $value) { + $state->setDefaultSort(function (Builder $query) use ($grammar, $value) { $query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$value]); $query->orderBy('posts_ft.score', 'desc'); }); diff --git a/framework/core/src/Search/AbstractDriver.php b/framework/core/src/Search/AbstractDriver.php index d35119127b..983778b42d 100644 --- a/framework/core/src/Search/AbstractDriver.php +++ b/framework/core/src/Search/AbstractDriver.php @@ -3,6 +3,7 @@ namespace Flarum\Search; use Flarum\Database\AbstractModel; +use Illuminate\Contracts\Container\Container; abstract class AbstractDriver { @@ -10,13 +11,14 @@ public function __construct( /** * @var array, class-string> */ - protected array $searchers + protected array $searchers, + protected Container $container ) { } abstract public static function name(): string; - public function searchers(): array + public function getSearchers(): array { return $this->searchers; } @@ -25,4 +27,9 @@ public function supports(string $modelClass): bool { return isset($this->searchers[$modelClass]); } + + public function searcher(string $resourceClass): SearcherInterface + { + return $this->container->make($this->searchers[$resourceClass]); + } } diff --git a/framework/core/src/Search/Database/AbstractSearcher.php b/framework/core/src/Search/Database/AbstractSearcher.php index ccc2162ef5..a6df983511 100644 --- a/framework/core/src/Search/Database/AbstractSearcher.php +++ b/framework/core/src/Search/Database/AbstractSearcher.php @@ -33,7 +33,7 @@ public function search(SearchCriteria $criteria): SearchResults $query = $this->getQuery($actor); - $search = new DatabaseSearchState($actor, in_array('q', array_keys($criteria->filters), true)); + $search = new DatabaseSearchState($actor, $criteria->isFulltext()); $search->setQuery($query->getQuery()); $this->filters->apply($search, $criteria->filters); @@ -58,38 +58,38 @@ public function search(SearchCriteria $criteria): SearchResults return new SearchResults($results, $areMoreResults); } - protected function applySort(DatabaseSearchState $query, ?array $sort = null, bool $sortIsDefault = false): void + protected function applySort(DatabaseSearchState $state, ?array $sort = null, bool $sortIsDefault = false): void { - if ($sortIsDefault && ! empty($query->getDefaultSort())) { - $sort = $query->getDefaultSort(); + if ($sortIsDefault && ! empty($state->getDefaultSort())) { + $sort = $state->getDefaultSort(); } if (is_callable($sort)) { - $sort($query->getQuery()); + $sort($state->getQuery()); } else { foreach ((array) $sort as $field => $order) { if (is_array($order)) { foreach ($order as $value) { - $query->getQuery()->orderByRaw(Str::snake($field).' != ?', [$value]); + $state->getQuery()->orderByRaw(Str::snake($field).' != ?', [$value]); } } else { - $query->getQuery()->orderBy(Str::snake($field), $order); + $state->getQuery()->orderBy(Str::snake($field), $order); } } } } - protected function applyOffset(DatabaseSearchState $query, int $offset): void + protected function applyOffset(DatabaseSearchState $state, int $offset): void { if ($offset > 0) { - $query->getQuery()->skip($offset); + $state->getQuery()->skip($offset); } } - protected function applyLimit(DatabaseSearchState $query, ?int $limit): void + protected function applyLimit(DatabaseSearchState $state, ?int $limit): void { if ($limit > 0) { - $query->getQuery()->take($limit); + $state->getQuery()->take($limit); } } } diff --git a/framework/core/src/Search/SearchCriteria.php b/framework/core/src/Search/SearchCriteria.php index 1f7382ce9e..c13f408d1c 100644 --- a/framework/core/src/Search/SearchCriteria.php +++ b/framework/core/src/Search/SearchCriteria.php @@ -38,4 +38,9 @@ public function __construct( public bool $sortIsDefault = false, ) { } + + public function isFulltext(): bool + { + return in_array('q', array_keys($this->filters), true); + } } diff --git a/framework/core/src/Search/SearchManager.php b/framework/core/src/Search/SearchManager.php index 898b938622..18061105df 100644 --- a/framework/core/src/Search/SearchManager.php +++ b/framework/core/src/Search/SearchManager.php @@ -7,7 +7,6 @@ use Flarum\Settings\SettingsRepositoryInterface; use Illuminate\Contracts\Container\Container; use Illuminate\Support\Arr; -use InvalidArgumentException; class SearchManager { @@ -26,7 +25,7 @@ public function driver(string $name): AbstractDriver $driver = Arr::first($this->drivers, fn ($driver) => $driver::name() === $name); if (! $driver) { - $driver = $this->driver(DatabaseSearchDriver::name()); + return $this->driver(DatabaseSearchDriver::name()); } return $this->container->make($driver); @@ -37,26 +36,6 @@ public function driverFor(string $resourceClass): AbstractDriver return $this->driver($this->settings->get("search_driver_$resourceClass")); } - public function searchable(string $resourceClass): bool - { - return $this->driverFor($resourceClass)->supports($resourceClass); - } - - /** - * @param class-string $resourceClass - */ - public function for(string $resourceClass): SearcherInterface - { - $driver = $this->driverFor($resourceClass); - $searchers = $driver->searchers(); - - if (! isset($searchers[$resourceClass])) { - throw new InvalidArgumentException("Driver {$driver::name()} does not support searching for $resourceClass."); - } - - return $this->container->make($searchers[$resourceClass]); - } - /** * @param class-string $resourceClass * @return array> @@ -70,4 +49,16 @@ public function indexable(string $resourceClass): bool { return ! empty($this->indexers[$resourceClass]); } + + public function query(string $resourceClass, SearchCriteria $criteria): SearchResults + { + $driver = $this->driverFor($resourceClass); + $defaultDriver = $this->driver(DatabaseSearchDriver::name()); + + if ($criteria->isFulltext() || ! $defaultDriver->supports($resourceClass)) { + return $driver->searcher($resourceClass)->search($criteria); + } + + return $defaultDriver->searcher($resourceClass)->search($criteria); + } } diff --git a/framework/core/src/Settings/SettingsServiceProvider.php b/framework/core/src/Settings/SettingsServiceProvider.php index d929b85cd2..0fb6c506cb 100644 --- a/framework/core/src/Settings/SettingsServiceProvider.php +++ b/framework/core/src/Settings/SettingsServiceProvider.php @@ -25,7 +25,7 @@ public function register(): void 'theme_primary_color' => '#4D698E', 'theme_secondary_color' => '#4D698E', 'search_driver_Flarum\User\User' => 'default', // @todo: use a morph map instead `User::class => 'user'` = search_driver_user (below as well) - 'search_driver_Flarum\Discussion\Discussion' => 'default', + 'search_driver_Flarum\Discussion\Discussion' => 'blomstra-elasticsearch', //'blomstra-elasticsearch', 'search_driver_Flarum\Group\Group' => 'default', 'search_driver_Flarum\Post\Post' => 'default', 'search_driver_Flarum\Http\AccessToken' => 'default', From 05324a0195810f518dd3b2420fc0b77a00c29e49 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Thu, 12 Oct 2023 18:09:48 +0000 Subject: [PATCH 04/15] Apply fixes from StyleCI --- .../src/Api/Controller/ListTagsController.php | 1 - framework/core/src/Api/ApiServiceProvider.php | 2 +- framework/core/src/Extend/SearchDriver.php | 2 +- framework/core/src/Extend/SearchIndex.php | 8 +++++++- framework/core/src/Search/AbstractDriver.php | 7 +++++++ .../src/Search/Database/AbstractSearcher.php | 2 -- .../Search/Database/DatabaseSearchDriver.php | 7 +++++++ framework/core/src/Search/IndexerInterface.php | 17 ++++++++++++----- framework/core/src/Search/Job/IndexJob.php | 7 +++++++ .../core/src/Search/Listener/ModelObserver.php | 7 +++++++ framework/core/src/Search/SearchManager.php | 7 +++++++ .../core/src/Search/SearchServiceProvider.php | 2 +- framework/core/src/Search/SearchState.php | 9 ++++++++- framework/core/src/Search/SearcherInterface.php | 11 +++++++++-- .../extenders/SimpleFlarumSearchTest.php | 4 ---- 15 files changed, 74 insertions(+), 19 deletions(-) diff --git a/extensions/tags/src/Api/Controller/ListTagsController.php b/extensions/tags/src/Api/Controller/ListTagsController.php index 0715ebcbfc..2f23267a6a 100644 --- a/extensions/tags/src/Api/Controller/ListTagsController.php +++ b/extensions/tags/src/Api/Controller/ListTagsController.php @@ -15,7 +15,6 @@ use Flarum\Search\SearchCriteria; use Flarum\Search\SearchManager; use Flarum\Tags\Api\Serializer\TagSerializer; -use Flarum\Tags\Search\TagSearcher; use Flarum\Tags\Tag; use Flarum\Tags\TagRepository; use Psr\Http\Message\ServerRequestInterface; diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index 91a481ced6..5d1e28b192 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -56,7 +56,7 @@ public function register(): void $this->container->singleton('flarum.api.middleware', function () { return [ HttpMiddleware\InjectActorReference::class, -// 'flarum.api.error_handler', + // 'flarum.api.error_handler', HttpMiddleware\ParseJsonBody::class, Middleware\FakeHttpMethods::class, HttpMiddleware\StartSession::class, diff --git a/framework/core/src/Extend/SearchDriver.php b/framework/core/src/Extend/SearchDriver.php index 114fefe870..e18808bc7a 100644 --- a/framework/core/src/Extend/SearchDriver.php +++ b/framework/core/src/Extend/SearchDriver.php @@ -13,9 +13,9 @@ use Flarum\Search\AbstractDriver; use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\Database\AbstractSearcher; -use Flarum\Search\SearchState; use Flarum\Search\Filter\FilterInterface; use Flarum\Search\SearchCriteria; +use Flarum\Search\SearchState; use Illuminate\Contracts\Container\Container; class SearchDriver implements ExtenderInterface diff --git a/framework/core/src/Extend/SearchIndex.php b/framework/core/src/Extend/SearchIndex.php index 5f42be1cf1..277ad55f8b 100644 --- a/framework/core/src/Extend/SearchIndex.php +++ b/framework/core/src/Extend/SearchIndex.php @@ -1,8 +1,14 @@ Date: Wed, 18 Oct 2023 12:39:40 +0100 Subject: [PATCH 05/15] fix: properly fire custom observables --- framework/core/src/Discussion/Discussion.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index 321d9c2575..377bd732b3 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -85,6 +85,8 @@ class Discussion extends AbstractModel 'hidden_at' => 'datetime', ]; + protected $observables = ['hidden']; + /** * The user for which the state relationship should be loaded. */ @@ -144,7 +146,9 @@ public function hide(?User $actor = null): static $this->raise(new Hidden($this)); - $this->fireCustomModelEvent('hidden', false); + $this->afterSave(function () { + $this->fireCustomModelEvent('hidden', false); + }); } return $this; @@ -158,7 +162,9 @@ public function restore(): static $this->raise(new Restored($this)); - $this->fireCustomModelEvent('restored', false); + $this->afterSave(function () { + $this->fireCustomModelEvent('restored', false); + }); } return $this; From 2e0d36ff433365ec7d7a585b72d77eba8b7fbf48 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 12:46:45 +0100 Subject: [PATCH 06/15] chore: remove debugging code --- framework/core/src/Api/ApiServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index 5d1e28b192..acd7a8b636 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -56,7 +56,7 @@ public function register(): void $this->container->singleton('flarum.api.middleware', function () { return [ HttpMiddleware\InjectActorReference::class, - // 'flarum.api.error_handler', + 'flarum.api.error_handler', HttpMiddleware\ParseJsonBody::class, Middleware\FakeHttpMethods::class, HttpMiddleware\StartSession::class, From 52043a97824a72599cda8e68351569d4adc3bbcc Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 12:52:21 +0100 Subject: [PATCH 07/15] fix: phpstan --- framework/core/src/Discussion/Discussion.php | 4 ++-- framework/core/src/Search/Listener/ModelObserver.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index 377bd732b3..4ec7d2b526 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -147,7 +147,7 @@ public function hide(?User $actor = null): static $this->raise(new Hidden($this)); $this->afterSave(function () { - $this->fireCustomModelEvent('hidden', false); + $this->fireModelEvent('hidden', false); }); } @@ -163,7 +163,7 @@ public function restore(): static $this->raise(new Restored($this)); $this->afterSave(function () { - $this->fireCustomModelEvent('restored', false); + $this->fireModelEvent('restored', false); }); } diff --git a/framework/core/src/Search/Listener/ModelObserver.php b/framework/core/src/Search/Listener/ModelObserver.php index 525b1f3f1e..b47acbb008 100644 --- a/framework/core/src/Search/Listener/ModelObserver.php +++ b/framework/core/src/Search/Listener/ModelObserver.php @@ -55,7 +55,7 @@ public function restored(AbstractModel $model): void private function runIndexJob(AbstractModel $model, string $operation): void { if ($this->search->indexable($model)) { - foreach ($this->search->indexers($model) as $indexerClass) { + foreach ($this->search->indexers($model::class) as $indexerClass) { $queue = property_exists($indexerClass, 'queue') ? $indexerClass::$queue : null; $this->queue->pushOn($queue, new IndexJob($indexerClass, [$model], $operation)); From 87f4716ed72231c32cc00e479325970a4c8435d7 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 14:53:27 +0100 Subject: [PATCH 08/15] fix: custom eloquent events --- framework/core/src/Database/AbstractModel.php | 10 ++++++++++ framework/core/src/Discussion/Discussion.php | 12 ++++++++---- framework/core/src/Search/Listener/ModelObserver.php | 2 +- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/framework/core/src/Database/AbstractModel.php b/framework/core/src/Database/AbstractModel.php index 7a4679d752..812952fbc9 100644 --- a/framework/core/src/Database/AbstractModel.php +++ b/framework/core/src/Database/AbstractModel.php @@ -237,4 +237,14 @@ public function newCollection(array $models = []): Collection { return new Collection($models); } + + public function __sleep() + { + // Closures cannot be serialized. + // We should not need them if we are serializing a model. + $this->afterSaveCallbacks = []; + $this->afterDeleteCallbacks = []; + + return parent::__sleep(); + } } diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index 4ec7d2b526..cf1b8c721a 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -146,8 +146,10 @@ public function hide(?User $actor = null): static $this->raise(new Hidden($this)); - $this->afterSave(function () { - $this->fireModelEvent('hidden', false); + $this->saved(function (self $model) { + if ($model === $this) { + $model->fireModelEvent('hidden', false); + } }); } @@ -162,8 +164,10 @@ public function restore(): static $this->raise(new Restored($this)); - $this->afterSave(function () { - $this->fireModelEvent('restored', false); + $this->saved(function (self $model) { + if ($model === $this) { + $model->fireModelEvent('restored', false); + } }); } diff --git a/framework/core/src/Search/Listener/ModelObserver.php b/framework/core/src/Search/Listener/ModelObserver.php index b47acbb008..58bcf0f712 100644 --- a/framework/core/src/Search/Listener/ModelObserver.php +++ b/framework/core/src/Search/Listener/ModelObserver.php @@ -54,7 +54,7 @@ public function restored(AbstractModel $model): void private function runIndexJob(AbstractModel $model, string $operation): void { - if ($this->search->indexable($model)) { + if ($this->search->indexable($model::class)) { foreach ($this->search->indexers($model::class) as $indexerClass) { $queue = property_exists($indexerClass, 'queue') ? $indexerClass::$queue : null; From 404040566f07293e5295bdd219534f08075fc2de Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 14:53:44 +0100 Subject: [PATCH 09/15] chore: drop POC usage --- framework/core/src/Settings/SettingsServiceProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/framework/core/src/Settings/SettingsServiceProvider.php b/framework/core/src/Settings/SettingsServiceProvider.php index 0fb6c506cb..d929b85cd2 100644 --- a/framework/core/src/Settings/SettingsServiceProvider.php +++ b/framework/core/src/Settings/SettingsServiceProvider.php @@ -25,7 +25,7 @@ public function register(): void 'theme_primary_color' => '#4D698E', 'theme_secondary_color' => '#4D698E', 'search_driver_Flarum\User\User' => 'default', // @todo: use a morph map instead `User::class => 'user'` = search_driver_user (below as well) - 'search_driver_Flarum\Discussion\Discussion' => 'blomstra-elasticsearch', //'blomstra-elasticsearch', + 'search_driver_Flarum\Discussion\Discussion' => 'default', 'search_driver_Flarum\Group\Group' => 'default', 'search_driver_Flarum\Post\Post' => 'default', 'search_driver_Flarum\Http\AccessToken' => 'default', From 6f22c8c54d62616934f5a840ee51931a3d2cea75 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 18 Oct 2023 13:53:57 +0000 Subject: [PATCH 10/15] Apply fixes from StyleCI --- framework/core/src/Discussion/Discussion.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/core/src/Discussion/Discussion.php b/framework/core/src/Discussion/Discussion.php index cf1b8c721a..908e5231e2 100644 --- a/framework/core/src/Discussion/Discussion.php +++ b/framework/core/src/Discussion/Discussion.php @@ -165,9 +165,9 @@ public function restore(): static $this->raise(new Restored($this)); $this->saved(function (self $model) { - if ($model === $this) { - $model->fireModelEvent('restored', false); - } + if ($model === $this) { + $model->fireModelEvent('restored', false); + } }); } From 6367017a8892624b70625a56d40560b6a01521d6 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 16:10:43 +0100 Subject: [PATCH 11/15] test: indexer extender API --- framework/core/src/Post/CommentPost.php | 14 ++ ...rumSearchTest.php => SearchDriverTest.php} | 5 +- .../integration/extenders/SearchIndexTest.php | 222 ++++++++++++++++++ 3 files changed, 238 insertions(+), 3 deletions(-) rename framework/core/tests/integration/extenders/{SimpleFlarumSearchTest.php => SearchDriverTest.php} (97%) create mode 100644 framework/core/tests/integration/extenders/SearchIndexTest.php diff --git a/framework/core/src/Post/CommentPost.php b/framework/core/src/Post/CommentPost.php index c1b150ddb3..b5d9066aab 100644 --- a/framework/core/src/Post/CommentPost.php +++ b/framework/core/src/Post/CommentPost.php @@ -28,6 +28,8 @@ class CommentPost extends Post public static string $type = 'comment'; protected static Formatter $formatter; + protected $observables = ['hidden']; + public static function reply(int $discussionId, string $content, int $userId, ?string $ipAddress, ?User $actor = null): static { $post = new static; @@ -69,6 +71,12 @@ public function hide(?User $actor = null): static $this->hidden_user_id = $actor?->id; $this->raise(new Hidden($this)); + + $this->saved(function (self $model) { + if ($model === $this) { + $model->fireModelEvent('hidden', false); + } + }); } return $this; @@ -81,6 +89,12 @@ public function restore(): static $this->hidden_user_id = null; $this->raise(new Restored($this)); + + $this->saved(function (self $model) { + if ($model === $this) { + $model->fireModelEvent('restored', false); + } + }); } return $this; diff --git a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php b/framework/core/tests/integration/extenders/SearchDriverTest.php similarity index 97% rename from framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php rename to framework/core/tests/integration/extenders/SearchDriverTest.php index 28c706ac87..8c8d8ea3b3 100644 --- a/framework/core/tests/integration/extenders/SimpleFlarumSearchTest.php +++ b/framework/core/tests/integration/extenders/SearchDriverTest.php @@ -24,7 +24,7 @@ use Flarum\Testing\integration\TestCase; use Flarum\User\User; -class SimpleFlarumSearchTest extends TestCase +class SearchDriverTest extends TestCase { use RetrievesAuthorizedUsers; @@ -73,8 +73,7 @@ public function searchDiscussions($query, $limit = null, array $filters = []) return $this->app() ->getContainer() ->make(SearchManager::class) - ->for(Discussion::class) - ->search(new SearchCriteria($actor, $filters, $limit)) + ->query(Discussion::class, new SearchCriteria($actor, $filters, $limit)) ->getResults(); } diff --git a/framework/core/tests/integration/extenders/SearchIndexTest.php b/framework/core/tests/integration/extenders/SearchIndexTest.php new file mode 100644 index 0000000000..acbb3ead43 --- /dev/null +++ b/framework/core/tests/integration/extenders/SearchIndexTest.php @@ -0,0 +1,222 @@ +prepareDatabase([ + 'discussions' => [ + ['id' => 1, 'title' => 'DISCUSSION 1', 'created_at' => Carbon::now()->subDays(1)->toDateTimeString(), 'hidden_at' => null, 'comment_count' => 1, 'user_id' => 1, 'first_post_id' => 1], + ['id' => 2, 'title' => 'DISCUSSION 2', 'created_at' => Carbon::now()->subDays(2)->toDateTimeString(), 'hidden_at' => Carbon::now(), 'comment_count' => 1, 'user_id' => 1], + ], + 'posts' => [ + ['id' => 1, 'number' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

content

', 'hidden_at' => null], + ['id' => 2, 'number' => 2, 'discussion_id' => 1, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '

content

', 'hidden_at' => Carbon::now()], + ], + ]); + } + + public static function modelProvider(): array + { + return [ + ['discussions', Discussion::class, 'title'], + ['posts', CommentPost::class, 'content'], + ]; + } + + /** @dataProvider modelProvider */ + public function test_indexer_triggered_on_create(string $type, string $modelClass, string $attribute) + { + $this->extend( + (new Extend\SearchIndex()) + ->indexer($modelClass, TestIndexer::class) + ); + + // Create discussion + $response = $this->send( + $this->request('POST', "/api/$type", [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + $attribute => 'test', + ], + 'relationships' => + ($type === 'posts' ? [ + 'discussion' => [ + 'data' => [ + 'type' => 'discussions', + 'id' => 1, + ], + ], + ] : null), + ] + ], + ]), + ); + + Assert::assertEquals('save', TestIndexer::$triggered, $response->getBody()->getContents()); + } + + /** @dataProvider modelProvider */ + public function test_indexer_triggered_on_save(string $type, string $modelClass, string $attribute) + { + $this->extend( + (new Extend\SearchIndex()) + ->indexer($modelClass, TestIndexer::class) + ); + + // Rename discussion + $response = $this->send( + $this->request('PATCH', "/api/$type/1", [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + $attribute => 'changed' + ] + ] + ], + ]), + ); + + Assert::assertEquals('save', TestIndexer::$triggered, $response->getBody()->getContents()); + } + + /** @dataProvider modelProvider */ + public function test_indexer_triggered_on_delete(string $type, string $modelClass, string $attribute) + { + $this->extend( + (new Extend\SearchIndex()) + ->indexer($modelClass, TestIndexer::class) + ); + + // Delete discussion + $response = $this->send( + $this->request('DELETE', "/api/$type/1", [ + 'authenticatedAs' => 1, + 'json' => [], + ]), + ); + + Assert::assertEquals('delete', TestIndexer::$triggered, $response->getBody()->getContents()); + } + + /** @dataProvider modelProvider */ + public function test_indexer_triggered_on_hide(string $type, string $modelClass, string $attribute) + { + $this->extend( + (new Extend\SearchIndex()) + ->indexer($modelClass, TestIndexer::class) + ); + + // Hide discussion + $response = $this->send( + $this->request('PATCH', "/api/$type/1", [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'isHidden' => true + ] + ] + ], + ]), + ); + + Assert::assertEquals('delete', TestIndexer::$triggered, $response->getBody()->getContents()); + } + + /** @dataProvider modelProvider */ + public function test_indexer_triggered_on_restore(string $type, string $modelClass, string $attribute) + { + $this->extend( + (new Extend\SearchIndex()) + ->indexer($modelClass, TestIndexer::class) + ); + + // Restore discussion + $response = $this->send( + $this->request('PATCH', "/api/$type/2", [ + 'authenticatedAs' => 1, + 'json' => [ + 'data' => [ + 'attributes' => [ + 'isHidden' => false + ] + ] + ], + ]), + ); + + Assert::assertEquals('save', TestIndexer::$triggered, $response->getBody()->getContents()); + } + + protected function tearDown(): void + { + TestIndexer::$triggered = null; + + parent::tearDown(); + } +} + +class TestIndexer implements IndexerInterface +{ + public static ?string $triggered = null; + + public static function index(): string + { + return 'test'; + } + + public function save(array $models): void + { + self::$triggered = 'save'; + } + + public function delete(array $models): void + { + self::$triggered = 'delete'; + } + + public function build(): void + { + // + } + + public function flush(): void + { + // + } +} From f27c5e97dd01598c8a9767cc5689d22b68aeedb1 Mon Sep 17 00:00:00 2001 From: StyleCI Bot Date: Wed, 18 Oct 2023 15:11:23 +0000 Subject: [PATCH 12/15] Apply fixes from StyleCI --- framework/core/src/Post/CommentPost.php | 6 ++--- .../integration/extenders/SearchIndexTest.php | 24 ++++++------------- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/framework/core/src/Post/CommentPost.php b/framework/core/src/Post/CommentPost.php index b5d9066aab..c93216f65a 100644 --- a/framework/core/src/Post/CommentPost.php +++ b/framework/core/src/Post/CommentPost.php @@ -91,9 +91,9 @@ public function restore(): static $this->raise(new Restored($this)); $this->saved(function (self $model) { - if ($model === $this) { - $model->fireModelEvent('restored', false); - } + if ($model === $this) { + $model->fireModelEvent('restored', false); + } }); } diff --git a/framework/core/tests/integration/extenders/SearchIndexTest.php b/framework/core/tests/integration/extenders/SearchIndexTest.php index acbb3ead43..0ceccaa471 100644 --- a/framework/core/tests/integration/extenders/SearchIndexTest.php +++ b/framework/core/tests/integration/extenders/SearchIndexTest.php @@ -11,20 +11,11 @@ use Carbon\Carbon; use Flarum\Discussion\Discussion; -use Flarum\Discussion\Search\DiscussionSearcher; use Flarum\Extend; use Flarum\Post\CommentPost; -use Flarum\Search\AbstractFulltextFilter; -use Flarum\Search\Database\DatabaseSearchDriver; -use Flarum\Search\Database\DatabaseSearchState; -use Flarum\Search\Filter\FilterInterface; use Flarum\Search\IndexerInterface; -use Flarum\Search\SearchCriteria; -use Flarum\Search\SearchManager; -use Flarum\Search\SearchState; use Flarum\Testing\integration\RetrievesAuthorizedUsers; use Flarum\Testing\integration\TestCase; -use Flarum\User\User; use PHPUnit\Framework\Assert; class SearchIndexTest extends TestCase @@ -72,15 +63,14 @@ public function test_indexer_triggered_on_create(string $type, string $modelClas 'attributes' => [ $attribute => 'test', ], - 'relationships' => - ($type === 'posts' ? [ - 'discussion' => [ - 'data' => [ - 'type' => 'discussions', - 'id' => 1, - ], + 'relationships' => ($type === 'posts' ? [ + 'discussion' => [ + 'data' => [ + 'type' => 'discussions', + 'id' => 1, ], - ] : null), + ], + ] : null), ] ], ]), From dc54747b88288143507660b543659c7813987881 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 17:26:56 +0100 Subject: [PATCH 13/15] fix: extension searcher fails without filters --- extensions/tags/extend.php | 1 + .../api/tags/ListWithFulltextSearchTest.php | 4 +- framework/core/src/Search/SearchManager.php | 2 +- .../core/src/Search/SearchServiceProvider.php | 60 +++++++++---------- 4 files changed, 34 insertions(+), 33 deletions(-) diff --git a/extensions/tags/extend.php b/extensions/tags/extend.php index 4ba4735e81..f17c16904f 100644 --- a/extensions/tags/extend.php +++ b/extensions/tags/extend.php @@ -139,6 +139,7 @@ ->addFilter(PostSearcher::class, PostTagFilter::class) ->addFilter(DiscussionSearcher::class, TagFilter::class) ->addMutator(DiscussionSearcher::class, HideHiddenTagsFromAllDiscussionsPage::class) + ->addSearcher(Tag::class, TagSearcher::class) ->setFulltext(TagSearcher::class, FulltextFilter::class), (new Extend\ModelUrl(Tag::class)) diff --git a/extensions/tags/tests/integration/api/tags/ListWithFulltextSearchTest.php b/extensions/tags/tests/integration/api/tags/ListWithFulltextSearchTest.php index 059e15da5e..d6ff6920ee 100644 --- a/extensions/tags/tests/integration/api/tags/ListWithFulltextSearchTest.php +++ b/extensions/tags/tests/integration/api/tags/ListWithFulltextSearchTest.php @@ -49,9 +49,9 @@ public function can_search_for_tags(string $search, array $expected) ]) ); - $data = json_decode($response->getBody()->getContents(), true)['data']; + $data = json_decode($contents = $response->getBody()->getContents(), true)['data'] ?? []; - $this->assertEquals(200, $response->getStatusCode()); + $this->assertEquals(200, $response->getStatusCode(), $contents); $this->assertEquals($expected, Arr::pluck($data, 'id')); } diff --git a/framework/core/src/Search/SearchManager.php b/framework/core/src/Search/SearchManager.php index 484d7953bd..dfaafaa31a 100644 --- a/framework/core/src/Search/SearchManager.php +++ b/framework/core/src/Search/SearchManager.php @@ -27,7 +27,7 @@ public function __construct( ) { } - public function driver(string $name): AbstractDriver + public function driver(?string $name): AbstractDriver { $driver = Arr::first($this->drivers, fn ($driver) => $driver::name() === $name); diff --git a/framework/core/src/Search/SearchServiceProvider.php b/framework/core/src/Search/SearchServiceProvider.php index 1d2b543754..95c1452f94 100644 --- a/framework/core/src/Search/SearchServiceProvider.php +++ b/framework/core/src/Search/SearchServiceProvider.php @@ -120,37 +120,37 @@ public function boot(Container $container): void ->when($driverClass) ->needs('$searchers') ->give($searchers); - } - - foreach ($container->make('flarum.search.filters') as $searcher => $filterClasses) { - $container - ->when($searcher) - ->needs(FilterManager::class) - ->give(function () use ($container, $searcher) { - $fulltext = $container->make('flarum.search.fulltext'); - $fulltextClass = $fulltext[$searcher] ?? null; - - $manager = new FilterManager( - $fulltextClass ? $container->make($fulltextClass) : null - ); - foreach (Arr::get($container->make('flarum.search.filters'), $searcher, []) as $filter) { - $manager->add($container->make($filter)); - } - - return $manager; - }); - - $container - ->when($searcher) - ->needs('$mutators') - ->give(function () use ($container, $searcher) { - $searchMutators = Arr::get($container->make('flarum.search.mutators'), $searcher, []); - - return array_map(function ($mutator) { - return ContainerUtil::wrapCallback($mutator, $this->container); - }, $searchMutators); - }); + foreach ($searchers as $searcher) { + $container + ->when($searcher) + ->needs(FilterManager::class) + ->give(function () use ($container, $searcher) { + $fulltext = $container->make('flarum.search.fulltext'); + $fulltextClass = $fulltext[$searcher] ?? null; + + $manager = new FilterManager( + $fulltextClass ? $container->make($fulltextClass) : null + ); + + foreach (Arr::get($container->make('flarum.search.filters'), $searcher, []) as $filter) { + $manager->add($container->make($filter)); + } + + return $manager; + }); + + $container + ->when($searcher) + ->needs('$mutators') + ->give(function () use ($container, $searcher) { + $searchMutators = Arr::get($container->make('flarum.search.mutators'), $searcher, []); + + return array_map(function ($mutator) { + return ContainerUtil::wrapCallback($mutator, $this->container); + }, $searchMutators); + }); + } } /** @var \Flarum\Database\AbstractModel $modelClass */ From 55e24e535ee72d33e324da1dce39848cf89bff93 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 17:30:43 +0100 Subject: [PATCH 14/15] fix: phpstan --- framework/core/src/Extend/SearchDriver.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/framework/core/src/Extend/SearchDriver.php b/framework/core/src/Extend/SearchDriver.php index e18808bc7a..b0a5069ce4 100644 --- a/framework/core/src/Extend/SearchDriver.php +++ b/framework/core/src/Extend/SearchDriver.php @@ -9,6 +9,7 @@ namespace Flarum\Extend; +use Flarum\Database\AbstractModel; use Flarum\Extension\Extension; use Flarum\Search\AbstractDriver; use Flarum\Search\AbstractFulltextFilter; @@ -37,7 +38,7 @@ public function __construct( /** * Add a filter to this searcher. Filters are used to filter search queries. * - * @param class-string $modelClass : The class of the model subject to searching/filtering. + * @param class-string $modelClass : The class of the model subject to searching/filtering. * This model must extend \Flarum\Database\AbstractModel. * @param class-string $searcherClass : The class of the Searcher for this model * This searcher must implement \Flarum\Search\SearcherInterface. From eb51cd513ecc861209adc5543e2656104e0cfd58 Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Wed, 18 Oct 2023 18:09:37 +0100 Subject: [PATCH 15/15] fix: frontend created gambit --- .../core/js/src/common/query/discussions/CreatedGambit.ts | 2 +- framework/core/js/tests/unit/common/GambitManager.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/framework/core/js/src/common/query/discussions/CreatedGambit.ts b/framework/core/js/src/common/query/discussions/CreatedGambit.ts index 1b84ed044d..087196165d 100644 --- a/framework/core/js/src/common/query/discussions/CreatedGambit.ts +++ b/framework/core/js/src/common/query/discussions/CreatedGambit.ts @@ -2,7 +2,7 @@ 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 { diff --git a/framework/core/js/tests/unit/common/GambitManager.test.ts b/framework/core/js/tests/unit/common/GambitManager.test.ts index 50e40ed104..d27e1a6146 100644 --- a/framework/core/js/tests/unit/common/GambitManager.test.ts +++ b/framework/core/js/tests/unit/common/GambitManager.test.ts @@ -25,9 +25,9 @@ test('gambits are only applied for the correct resource type', function () { q: 'lorem created:2023-07-07 is:hidden author:behz', email: 'behz@machine.local', }); - expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07 is:hidden -author:behz email:behz@machine.local' })).toStrictEqual({ + expect(gambits.apply('discussions', { q: 'lorem created:2023-07-07..2023-10-18 is:hidden -author:behz email:behz@machine.local' })).toStrictEqual({ q: 'lorem email:behz@machine.local', - created: '2023-07-07', + created: '2023-07-07..2023-10-18', hidden: true, '-author': ['behz'], });