From 2900467678c80d89c042bab274c6917744600ae2 Mon Sep 17 00:00:00 2001 From: Toby Zerner Date: Thu, 12 Mar 2015 10:37:02 +1030 Subject: [PATCH] Implement user searching & minor search refactor --- .../Api/Actions/Discussions/IndexAction.php | 9 +-- .../src/Api/Actions/Users/IndexAction.php | 72 ++++++++--------- .../core/src/Api/Actions/Users/ShowAction.php | 8 +- .../core/src/Core/CoreServiceProvider.php | 24 ++++-- .../DiscussionRepositoryInterface.php | 2 +- .../EloquentDiscussionRepository.php | 2 +- .../Repositories/EloquentUserRepository.php | 28 +++++++ .../Repositories/UserRepositoryInterface.php | 17 ++++ .../Search/Discussions/DiscussionSearcher.php | 11 ++- .../Discussions/Gambits/AuthorGambit.php | 6 +- .../Discussions/Gambits/FulltextGambit.php | 6 +- .../Discussions/Gambits/UnreadGambit.php | 8 +- .../core/src/Core/Search/GambitAbstract.php | 4 +- .../src/Core/Search/SearcherInterface.php | 8 ++ .../Search/Users/Gambits/FulltextGambit.php | 24 ++++++ .../Core/Search/Users/UserSearchCriteria.php | 20 +++++ .../Core/Search/Users/UserSearchResults.php | 32 ++++++++ .../src/Core/Search/Users/UserSearcher.php | 79 +++++++++++++++++++ 18 files changed, 291 insertions(+), 69 deletions(-) create mode 100644 framework/core/src/Core/Search/SearcherInterface.php create mode 100644 framework/core/src/Core/Search/Users/Gambits/FulltextGambit.php create mode 100644 framework/core/src/Core/Search/Users/UserSearchCriteria.php create mode 100644 framework/core/src/Core/Search/Users/UserSearchResults.php create mode 100644 framework/core/src/Core/Search/Users/UserSearcher.php diff --git a/framework/core/src/Api/Actions/Discussions/IndexAction.php b/framework/core/src/Api/Actions/Discussions/IndexAction.php index b6881a0d8..2e12cac40 100644 --- a/framework/core/src/Api/Actions/Discussions/IndexAction.php +++ b/framework/core/src/Api/Actions/Discussions/IndexAction.php @@ -12,14 +12,14 @@ class IndexAction extends BaseAction /** * The discussion searcher. * - * @var DiscussionSearcher + * @var \Flarum\Core\Search\Discussions\DiscussionSearcher */ protected $searcher; /** * Instantiate the action. * - * @param DiscussionSearcher $searcher + * @param \Flarum\Core\Search\Discussions\DiscussionSearcher $searcher */ public function __construct(Actor $actor, DiscussionSearcher $searcher) { @@ -30,8 +30,7 @@ class IndexAction extends BaseAction /** * Show a list of discussions. * - * @todo custom rate limit for this function? determined by if $key was valid? - * @return Response + * @return \Illuminate\Http\Response */ protected function run(ApiParams $params) { @@ -43,7 +42,7 @@ class IndexAction extends BaseAction $relations = array_merge(['startUser', 'lastUser'], $include); - // Set up the discussion finder with our search criteria, and get the + // Set up the discussion searcher with our search criteria, and get the // requested range of results with the necessary relations loaded. $criteria = new DiscussionSearchCriteria($this->actor->getUser(), $query, $sort['field'], $sort['order']); $load = array_merge($relations, ['state']); diff --git a/framework/core/src/Api/Actions/Users/IndexAction.php b/framework/core/src/Api/Actions/Users/IndexAction.php index 67d07f7f1..0a2c1635c 100644 --- a/framework/core/src/Api/Actions/Users/IndexAction.php +++ b/framework/core/src/Api/Actions/Users/IndexAction.php @@ -1,83 +1,79 @@ finder = $finder; + $this->actor = $actor; + $this->searcher = $searcher; } /** * Show a list of users. * - * @todo custom rate limit for this function? determined by if $key was valid? - * @return Response + * @return \Illuminate\Http\Response */ - protected function run() + protected function run(ApiParams $params) { - $query = $this->input('q'); - $key = $this->input('key'); - $sort = $this->sort(['', 'username', 'posts', 'discussions', 'lastActive', 'created']); - $start = $this->start(); - $count = $this->count(50, 100); - $include = $this->included(['groups']); + $query = $params->get('q'); + $start = $params->start(); + $include = $params->included(['groups']); + $count = $params->count(20, 50); + $sort = $params->sort(['', 'username', 'posts', 'discussions', 'lastActive', 'created']); + $relations = array_merge(['groups'], $include); - // Set up the user finder with our search criteria, and get the + // Set up the user searcher with our search criteria, and get the // requested range of results with the necessary relations loaded. - $this->finder->setUser(User::current()); - $this->finder->setQuery($query); - $this->finder->setSort($sort['by']); - $this->finder->setOrder($sort['order']); - $this->finder->setKey($key); + $criteria = new UserSearchCriteria($this->actor->getUser(), $query, $sort['field'], $sort['order']); - $users = $this->finder->results($count, $start); - $users->load($relations); + $results = $this->searcher->search($criteria, $count, $start, $relations); - if (($total = $this->finder->getCount()) !== null) { - $this->document->addMeta('total', $total); - } - if (($key = $this->finder->getKey()) !== null) { - $this->document->addMeta('key', $key); + $document = $this->document(); + + if (($total = $results->getTotal()) !== null) { + $document->addMeta('total', $total); } // If there are more results, then we need to construct a URL to the // next results page and add that to the metadata. We do this by // compacting all of the valid query parameters which have been // specified. - if ($this->finder->areMoreResults()) { + if ($results->areMoreResults()) { $start += $count; $include = implode(',', $include); $sort = $sort['string']; - $input = array_filter(compact('query', 'key', 'sort', 'start', 'count', 'include')); + $input = array_filter(compact('query', 'sort', 'start', 'count', 'include')); $moreUrl = $this->buildUrl('users.index', [], $input); } else { $moreUrl = ''; } - $this->document->addMeta('moreUrl', $moreUrl); + $document->addMeta('moreUrl', $moreUrl); - // Finally, we can set up the user serializer and use it to create - // a collection of user results. + // Finally, we can set up the discussion serializer and use it to create + // a collection of discussion results. $serializer = new UserSerializer($relations); - $this->document->setPrimaryElement($serializer->collection($users)); + $document->setPrimaryElement($serializer->collection($results->getUsers())); - return $this->respondWithDocument(); + return $this->respondWithDocument($document); } } diff --git a/framework/core/src/Api/Actions/Users/ShowAction.php b/framework/core/src/Api/Actions/Users/ShowAction.php index e4a0d79b5..f6cc8aae0 100644 --- a/framework/core/src/Api/Actions/Users/ShowAction.php +++ b/framework/core/src/Api/Actions/Users/ShowAction.php @@ -25,7 +25,13 @@ class ShowAction extends BaseAction */ public function run(ApiParams $params) { - $user = $this->users->findOrFail($params->get('id'), $this->actor->getUser()); + $id = $params->get('id'); + + if (! is_numeric($id)) { + $id = $this->users->getIdForUsername($id); + } + + $user = $this->users->findOrFail($id, $this->actor->getUser()); // Set up the user serializer, which we will use to create the // document's primary resource. We will specify that we want the diff --git a/framework/core/src/Core/CoreServiceProvider.php b/framework/core/src/Core/CoreServiceProvider.php index b1369ba90..d4e4937b2 100644 --- a/framework/core/src/Core/CoreServiceProvider.php +++ b/framework/core/src/Core/CoreServiceProvider.php @@ -77,13 +77,23 @@ class CoreServiceProvider extends ServiceProvider public function registerGambits() { - $this->app->bind('Flarum\Core\Search\GambitManager', function () { - $gambits = new GambitManager($this->app); - $gambits->add('Flarum\Core\Search\Discussions\Gambits\AuthorGambit'); - $gambits->add('Flarum\Core\Search\Discussions\Gambits\UnreadGambit'); - $gambits->setFulltextGambit('Flarum\Core\Search\Discussions\Gambits\FulltextGambit'); - return $gambits; - }); + $this->app->when('Flarum\Core\Search\Discussions\DiscussionSearcher') + ->needs('Flarum\Core\Search\GambitManager') + ->give(function () { + $gambits = new GambitManager($this->app); + $gambits->add('Flarum\Core\Search\Discussions\Gambits\AuthorGambit'); + $gambits->add('Flarum\Core\Search\Discussions\Gambits\UnreadGambit'); + $gambits->setFulltextGambit('Flarum\Core\Search\Discussions\Gambits\FulltextGambit'); + return $gambits; + }); + + $this->app->when('Flarum\Core\Search\Users\UserSearcher') + ->needs('Flarum\Core\Search\GambitManager') + ->give(function () { + $gambits = new GambitManager($this->app); + $gambits->setFulltextGambit('Flarum\Core\Search\Users\Gambits\FulltextGambit'); + return $gambits; + }); } public function registerPostTypes() diff --git a/framework/core/src/Core/Repositories/DiscussionRepositoryInterface.php b/framework/core/src/Core/Repositories/DiscussionRepositoryInterface.php index 6eb148c8b..025576863 100644 --- a/framework/core/src/Core/Repositories/DiscussionRepositoryInterface.php +++ b/framework/core/src/Core/Repositories/DiscussionRepositoryInterface.php @@ -5,7 +5,7 @@ use Flarum\Core\Models\User; interface DiscussionRepositoryInterface { /** - * Get a new query builder for ths discussions table. + * Get a new query builder for the discussions table. * * @return \Illuminate\Database\Eloquent\Builder */ diff --git a/framework/core/src/Core/Repositories/EloquentDiscussionRepository.php b/framework/core/src/Core/Repositories/EloquentDiscussionRepository.php index b5f99f0ee..3f82fb068 100644 --- a/framework/core/src/Core/Repositories/EloquentDiscussionRepository.php +++ b/framework/core/src/Core/Repositories/EloquentDiscussionRepository.php @@ -7,7 +7,7 @@ use Flarum\Core\Models\User; class EloquentDiscussionRepository implements DiscussionRepositoryInterface { /** - * Get a new query builder for ths discussions table. + * Get a new query builder for the discussions table. * * @return \Illuminate\Database\Eloquent\Builder */ diff --git a/framework/core/src/Core/Repositories/EloquentUserRepository.php b/framework/core/src/Core/Repositories/EloquentUserRepository.php index c5cd1e0f3..a5bd0a193 100644 --- a/framework/core/src/Core/Repositories/EloquentUserRepository.php +++ b/framework/core/src/Core/Repositories/EloquentUserRepository.php @@ -5,6 +5,16 @@ use Flarum\Core\Models\User; class EloquentUserRepository implements UserRepositoryInterface { + /** + * Get a new query builder for the users table. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function query() + { + return User::query(); + } + /** * Find a user by ID, optionally making sure it is visible to a certain * user, or throw an exception. @@ -49,6 +59,24 @@ class EloquentUserRepository implements UserRepositoryInterface return $this->scopeVisibleForUser($query, $user)->pluck('id'); } + /** + * Find users by matching a string of words against their username, + * optionally making sure they are visible to a certain user. + * + * @param string $string + * @param \Flarum\Core\Models\User|null $user + * @return array + */ + public function getIdsForUsername($string, User $user = null) + { + $query = User::select('id') + ->where('username', 'like', '%'.$string.'%') + ->orderByRaw('username = ? desc', [$string]) + ->orderByRaw('username like ? desc', [$string.'%']); + + return $this->scopeVisibleForUser($query, $user)->lists('id'); + } + /** * Scope a query to only include records that are visible to a user. * diff --git a/framework/core/src/Core/Repositories/UserRepositoryInterface.php b/framework/core/src/Core/Repositories/UserRepositoryInterface.php index 7def0bb59..e3a0cb752 100644 --- a/framework/core/src/Core/Repositories/UserRepositoryInterface.php +++ b/framework/core/src/Core/Repositories/UserRepositoryInterface.php @@ -4,6 +4,13 @@ use Flarum\Core\Models\User; interface UserRepositoryInterface { + /** + * Get a new query builder for the users table. + * + * @return \Illuminate\Database\Eloquent\Builder + */ + public function query(); + /** * Find a user by ID, optionally making sure it is visible to a certain * user, or throw an exception. @@ -32,4 +39,14 @@ interface UserRepositoryInterface * @return integer|null */ public function getIdForUsername($username, User $user = null); + + /** + * Find users by matching a string of words against their username, + * optionally making sure they are visible to a certain user. + * + * @param string $string + * @param \Flarum\Core\Models\User|null $user + * @return array + */ + public function getIdsForUsername($string, User $user = null); } diff --git a/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php b/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php index 7706576d3..827ce0663 100644 --- a/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php +++ b/framework/core/src/Core/Search/Discussions/DiscussionSearcher.php @@ -1,18 +1,19 @@ ['last_time', 'desc'], 'replies' => ['comments_count', 'desc'], - 'created' => ['start_time', 'desc'] + 'created' => ['start_time', 'asc'] ]; protected $defaultSort = 'lastPost'; @@ -43,6 +44,11 @@ class DiscussionSearcher $this->defaultSort = $defaultSort; } + public function query() + { + return $this->query; + } + public function search(DiscussionSearchCriteria $criteria, $count = null, $start = 0, $load = []) { $this->user = $criteria->user; @@ -56,7 +62,6 @@ class DiscussionSearcher if (empty($sort)) { $sort = $this->defaultSort; } - // dd($sort); if (is_array($sort)) { foreach ($sort as $id) { $this->query->orderByRaw('id != '.(int) $id); diff --git a/framework/core/src/Core/Search/Discussions/Gambits/AuthorGambit.php b/framework/core/src/Core/Search/Discussions/Gambits/AuthorGambit.php index 94134459a..4b08bb19c 100644 --- a/framework/core/src/Core/Search/Discussions/Gambits/AuthorGambit.php +++ b/framework/core/src/Core/Search/Discussions/Gambits/AuthorGambit.php @@ -1,7 +1,7 @@ users = $users; } - public function conditions($matches, DiscussionSearcher $searcher) + public function conditions($matches, SearcherInterface $searcher) { $username = trim($matches[1], '"'); $id = $this->users->getIdForUsername($username); - $searcher->query->where('start_user_id', $id); + $searcher->query()->where('start_user_id', $id); } } diff --git a/framework/core/src/Core/Search/Discussions/Gambits/FulltextGambit.php b/framework/core/src/Core/Search/Discussions/Gambits/FulltextGambit.php index ce2ea5a1e..8aca89890 100644 --- a/framework/core/src/Core/Search/Discussions/Gambits/FulltextGambit.php +++ b/framework/core/src/Core/Search/Discussions/Gambits/FulltextGambit.php @@ -1,7 +1,7 @@ posts = $posts; } - public function apply($string, DiscussionSearcher $searcher) + public function apply($string, SearcherInterface $searcher) { $posts = $this->posts->findByContent($string, $searcher->user); @@ -24,7 +24,7 @@ class FulltextGambit extends GambitAbstract } $discussions = array_unique($discussions); - $searcher->query->whereIn('id', $discussions); + $searcher->query()->whereIn('id', $discussions); $searcher->setDefaultSort($discussions); } diff --git a/framework/core/src/Core/Search/Discussions/Gambits/UnreadGambit.php b/framework/core/src/Core/Search/Discussions/Gambits/UnreadGambit.php index a18c13180..999bb5568 100644 --- a/framework/core/src/Core/Search/Discussions/Gambits/UnreadGambit.php +++ b/framework/core/src/Core/Search/Discussions/Gambits/UnreadGambit.php @@ -1,7 +1,7 @@ discussions = $discussions; } - protected function conditions($matches, DiscussionSearcher $searcher) + protected function conditions($matches, SearcherInterface $searcher) { $user = $searcher->user; @@ -27,9 +27,9 @@ class UnreadGambit extends GambitAbstract $readIds = $this->discussions->getReadIds($user); if ($matches[1] === 'true') { - $searcher->query->whereNotIn('id', $readIds)->where('last_time', '>', $user->read_time ?: 0); + $searcher->query()->whereNotIn('id', $readIds)->where('last_time', '>', $user->read_time ?: 0); } else { - $searcher->query->whereIn('id', $readIds)->orWhere('last_time', '<=', $user->read_time ?: 0); + $searcher->query()->whereIn('id', $readIds)->orWhere('last_time', '<=', $user->read_time ?: 0); } } } diff --git a/framework/core/src/Core/Search/GambitAbstract.php b/framework/core/src/Core/Search/GambitAbstract.php index 3dd4e67aa..39d91bcd7 100644 --- a/framework/core/src/Core/Search/GambitAbstract.php +++ b/framework/core/src/Core/Search/GambitAbstract.php @@ -1,12 +1,10 @@ match($bit)) { $this->conditions($matches, $searcher); diff --git a/framework/core/src/Core/Search/SearcherInterface.php b/framework/core/src/Core/Search/SearcherInterface.php new file mode 100644 index 000000000..25945ecc7 --- /dev/null +++ b/framework/core/src/Core/Search/SearcherInterface.php @@ -0,0 +1,8 @@ +users = $users; + } + + public function apply($string, SearcherInterface $searcher) + { + $users = $this->users->getIdsForUsername($string, $searcher->user); + + $searcher->query()->whereIn('id', $users); + + $searcher->setDefaultSort($users); + } +} diff --git a/framework/core/src/Core/Search/Users/UserSearchCriteria.php b/framework/core/src/Core/Search/Users/UserSearchCriteria.php new file mode 100644 index 000000000..016b5b542 --- /dev/null +++ b/framework/core/src/Core/Search/Users/UserSearchCriteria.php @@ -0,0 +1,20 @@ +user = $user; + $this->query = $query; + $this->sort = $sort; + $this->order = $order; + } +} diff --git a/framework/core/src/Core/Search/Users/UserSearchResults.php b/framework/core/src/Core/Search/Users/UserSearchResults.php new file mode 100644 index 000000000..e033bea7e --- /dev/null +++ b/framework/core/src/Core/Search/Users/UserSearchResults.php @@ -0,0 +1,32 @@ +users = $users; + $this->areMoreResults = $areMoreResults; + $this->total = $total; + } + + public function getUsers() + { + return $this->users; + } + + public function getTotal() + { + return $this->total; + } + + public function areMoreResults() + { + return $this->areMoreResults; + } +} diff --git a/framework/core/src/Core/Search/Users/UserSearcher.php b/framework/core/src/Core/Search/Users/UserSearcher.php new file mode 100644 index 000000000..51beb3793 --- /dev/null +++ b/framework/core/src/Core/Search/Users/UserSearcher.php @@ -0,0 +1,79 @@ + ['username', 'asc'], + 'posts' => ['comments_count', 'desc'], + 'discussions' => ['discussions_count', 'desc'], + 'lastActive' => ['last_seen_time', 'desc'], + 'created' => ['join_time', 'asc'] + ]; + + protected $defaultSort = 'username'; + + protected $users; + + public function __construct(GambitManager $gambits, UserRepositoryInterface $users) + { + $this->gambits = $gambits; + $this->users = $users; + } + + public function setDefaultSort($defaultSort) + { + $this->defaultSort = $defaultSort; + } + + public function query() + { + return $this->query; + } + + public function search(UserSearchCriteria $criteria, $count = null, $start = 0, $load = []) + { + $this->user = $criteria->user; + $this->query = $this->users->query()->whereCan($criteria->user, 'view'); + + $this->gambits->apply($criteria->query, $this); + + $total = $this->query->count(); + + $sort = $criteria->sort; + if (empty($sort)) { + $sort = $this->defaultSort; + } + if (is_array($sort)) { + foreach ($sort as $id) { + $this->query->orderByRaw('id != '.(int) $id); + } + } else { + list($column, $order) = $this->sortMap[$sort]; + $this->query->orderBy($column, $criteria->order ?: $order); + } + + if ($start > 0) { + $this->query->skip($start); + } + if ($count > 0) { + $this->query->take($count + 1); + } + + $users = $this->query->get(); + + if ($count > 0 && $areMoreResults = $users->count() > $count) { + $users->pop(); + } + + $users->load($load); + + return new UserSearchResults($users, $areMoreResults, $total); + } +}