diff --git a/src/Api/Controller/ListDiscussionsController.php b/src/Api/Controller/ListDiscussionsController.php
index dc825d9cd..c2b4af6d9 100644
--- a/src/Api/Controller/ListDiscussionsController.php
+++ b/src/Api/Controller/ListDiscussionsController.php
@@ -11,10 +11,10 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\DiscussionSerializer;
use Flarum\Discussion\Discussion;
+use Flarum\Discussion\Filter\DiscussionFilterer;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
-use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -43,11 +43,21 @@ class ListDiscussionsController extends AbstractListController
'lastPost'
];
+ /**
+ * {@inheritDoc}
+ */
+ public $sort = ['lastPostedAt' => 'desc'];
+
/**
* {@inheritdoc}
*/
public $sortFields = ['lastPostedAt', 'commentCount', 'createdAt'];
+ /**
+ * @var DiscussionFilterer
+ */
+ protected $filterer;
+
/**
* @var DiscussionSearcher
*/
@@ -59,11 +69,13 @@ class ListDiscussionsController extends AbstractListController
protected $url;
/**
+ * @param DiscussionFilterer $filterer
* @param DiscussionSearcher $searcher
* @param UrlGenerator $url
*/
- public function __construct(DiscussionSearcher $searcher, UrlGenerator $url)
+ public function __construct(DiscussionFilterer $filterer, DiscussionSearcher $searcher, UrlGenerator $url)
{
+ $this->filterer = $filterer;
$this->searcher = $searcher;
$this->url = $url;
}
@@ -74,16 +86,19 @@ class ListDiscussionsController extends AbstractListController
protected function data(ServerRequestInterface $request, Document $document)
{
$actor = $request->getAttribute('actor');
- $query = Arr::get($this->extractFilter($request), 'q');
+ $filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
- $criteria = new SearchCriteria($actor, $query, $sort);
-
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
- $load = array_merge($this->extractInclude($request), ['state']);
+ $include = array_merge($this->extractInclude($request), ['state']);
- $results = $this->searcher->search($criteria, $limit, $offset);
+ $criteria = new SearchCriteria($actor, $filters, $sort);
+ if (array_key_exists('q', $filters)) {
+ $results = $this->searcher->search($criteria, $limit, $offset);
+ } else {
+ $results = $this->filterer->filter($criteria, $limit, $offset);
+ }
$document->addPaginationLinks(
$this->url->to('api')->route('discussions.index'),
@@ -95,9 +110,9 @@ class ListDiscussionsController extends AbstractListController
Discussion::setStateUser($actor);
- $results = $results->getResults()->load($load);
+ $results = $results->getResults()->load($include);
- if ($relations = array_intersect($load, ['firstPost', 'lastPost'])) {
+ if ($relations = array_intersect($include, ['firstPost', 'lastPost'])) {
foreach ($results as $discussion) {
foreach ($relations as $relation) {
if ($discussion->$relation) {
diff --git a/src/Api/Controller/ListUsersController.php b/src/Api/Controller/ListUsersController.php
index 5f44c0984..6e7bb4f84 100644
--- a/src/Api/Controller/ListUsersController.php
+++ b/src/Api/Controller/ListUsersController.php
@@ -12,8 +12,8 @@ namespace Flarum\Api\Controller;
use Flarum\Api\Serializer\UserSerializer;
use Flarum\Http\UrlGenerator;
use Flarum\Search\SearchCriteria;
+use Flarum\User\Filter\UserFilterer;
use Flarum\User\Search\UserSearcher;
-use Illuminate\Support\Arr;
use Psr\Http\Message\ServerRequestInterface;
use Tobscure\JsonApi\Document;
@@ -40,6 +40,11 @@ class ListUsersController extends AbstractListController
'joinedAt'
];
+ /**
+ * @var UserFilterer
+ */
+ protected $filterer;
+
/**
* @var UserSearcher
*/
@@ -51,11 +56,13 @@ class ListUsersController extends AbstractListController
protected $url;
/**
+ * @param UserFilterer $filterer
* @param UserSearcher $searcher
* @param UrlGenerator $url
*/
- public function __construct(UserSearcher $searcher, UrlGenerator $url)
+ public function __construct(UserFilterer $filterer, UserSearcher $searcher, UrlGenerator $url)
{
+ $this->filterer = $filterer;
$this->searcher = $searcher;
$this->url = $url;
}
@@ -69,16 +76,19 @@ class ListUsersController extends AbstractListController
$actor->assertCan('viewUserList');
- $query = Arr::get($this->extractFilter($request), 'q');
+ $filters = $this->extractFilter($request);
$sort = $this->extractSort($request);
- $criteria = new SearchCriteria($actor, $query, $sort);
-
$limit = $this->extractLimit($request);
$offset = $this->extractOffset($request);
- $load = $this->extractInclude($request);
+ $include = $this->extractInclude($request);
- $results = $this->searcher->search($criteria, $limit, $offset, $load);
+ $criteria = new SearchCriteria($actor, $filters, $sort);
+ if (array_key_exists('q', $filters)) {
+ $results = $this->searcher->search($criteria, $limit, $offset);
+ } else {
+ $results = $this->filterer->filter($criteria, $limit, $offset);
+ }
$document->addPaginationLinks(
$this->url->to('api')->route('users.index'),
@@ -88,6 +98,6 @@ class ListUsersController extends AbstractListController
$results->areMoreResults() ? null : 0
);
- return $results->getResults();
+ return $results->getResults()->load($include);
}
}
diff --git a/src/Discussion/Event/Searching.php b/src/Discussion/Event/Searching.php
index 138a4f469..dde2b47b9 100644
--- a/src/Discussion/Event/Searching.php
+++ b/src/Discussion/Event/Searching.php
@@ -9,8 +9,8 @@
namespace Flarum\Discussion\Event;
-use Flarum\Discussion\Search\DiscussionSearch;
use Flarum\Search\SearchCriteria;
+use Flarum\Search\SearchState;
/**
* @deprecated beta 16, remove beta 17
@@ -18,7 +18,7 @@ use Flarum\Search\SearchCriteria;
class Searching
{
/**
- * @var DiscussionSearch
+ * @var SearchState
*/
public $search;
@@ -28,10 +28,10 @@ class Searching
public $criteria;
/**
- * @param DiscussionSearch $search
+ * @param SearchState $search
* @param \Flarum\Search\SearchCriteria $criteria
*/
- public function __construct(DiscussionSearch $search, SearchCriteria $criteria)
+ public function __construct(SearchState $search, SearchCriteria $criteria)
{
$this->search = $search;
$this->criteria = $criteria;
diff --git a/src/Discussion/Filter/AuthorFilterGambit.php b/src/Discussion/Filter/AuthorFilterGambit.php
new file mode 100644
index 000000000..616b8be38
--- /dev/null
+++ b/src/Discussion/Filter/AuthorFilterGambit.php
@@ -0,0 +1,72 @@
+users = $users;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getGambitPattern()
+ {
+ return 'author:(.+)';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function conditions(SearchState $search, array $matches, $negate)
+ {
+ $this->constrain($search->getQuery(), $matches[1], $negate);
+ }
+
+ public function getFilterKey(): string
+ {
+ return 'author';
+ }
+
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ $this->constrain($filterState->getQuery(), $filterValue, $negate);
+ }
+
+ protected function constrain(Builder $query, $rawUsernames, $negate)
+ {
+ $usernames = trim($rawUsernames, '"');
+ $usernames = explode(',', $usernames);
+
+ $ids = [];
+ foreach ($usernames as $username) {
+ $ids[] = $this->users->getIdForUsername($username);
+ }
+
+ $query->whereIn('discussions.user_id', $ids, 'and', $negate);
+ }
+}
diff --git a/src/Discussion/Filter/CreatedFilterGambit.php b/src/Discussion/Filter/CreatedFilterGambit.php
new file mode 100644
index 000000000..638d84884
--- /dev/null
+++ b/src/Discussion/Filter/CreatedFilterGambit.php
@@ -0,0 +1,61 @@
+constrain($search->getQuery(), Arr::get($matches, 1), Arr::get($matches, 3), $negate);
+ }
+
+ public function getFilterKey(): string
+ {
+ return 'created';
+ }
+
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ preg_match('/^'.$this->getGambitPattern().'$/i', 'created:'.$filterValue, $matches);
+
+ $this->constrain($filterState->getQuery(), Arr::get($matches, 1), Arr::get($matches, 3), $negate);
+ }
+
+ public function constrain(Builder $query, ?string $firstDate, ?string $secondDate, $negate)
+ {
+ // If we've just been provided with a single YYYY-MM-DD date, then find
+ // discussions that were started on that exact date. But if we've been
+ // provided with a YYYY-MM-DD..YYYY-MM-DD range, then find discussions
+ // that were started during that period.
+ if (empty($secondDate)) {
+ $query->whereDate('created_at', $negate ? '!=' : '=', $firstDate);
+ } else {
+ $query->whereBetween('created_at', [$firstDate, $secondDate], 'and', $negate);
+ }
+ }
+}
diff --git a/src/Discussion/Filter/DiscussionFilterer.php b/src/Discussion/Filter/DiscussionFilterer.php
new file mode 100644
index 000000000..8ada10502
--- /dev/null
+++ b/src/Discussion/Filter/DiscussionFilterer.php
@@ -0,0 +1,40 @@
+discussions = $discussions;
+ }
+
+ protected function getQuery(User $actor): Builder
+ {
+ return $this->discussions->query()->select('discussions.*')->whereVisibleTo($actor);
+ }
+}
diff --git a/src/Discussion/Filter/HiddenFilterGambit.php b/src/Discussion/Filter/HiddenFilterGambit.php
new file mode 100644
index 000000000..133167a36
--- /dev/null
+++ b/src/Discussion/Filter/HiddenFilterGambit.php
@@ -0,0 +1,56 @@
+constrain($search->getQuery(), $negate);
+ }
+
+ public function getFilterKey(): string
+ {
+ return 'hidden';
+ }
+
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ $this->constrain($filterState->getQuery(), $negate);
+ }
+
+ protected function constrain(Builder $query, bool $negate)
+ {
+ $query->where(function ($query) use ($negate) {
+ if ($negate) {
+ $query->whereNull('hidden_at')->where('comment_count', '>', 0);
+ } else {
+ $query->whereNotNull('hidden_at')->orWhere('comment_count', 0);
+ }
+ });
+ }
+}
diff --git a/src/Discussion/Search/Gambit/UnreadGambit.php b/src/Discussion/Filter/UnreadFilterGambit.php
similarity index 53%
rename from src/Discussion/Search/Gambit/UnreadGambit.php
rename to src/Discussion/Filter/UnreadFilterGambit.php
index 33aac7c91..06e6efb8e 100644
--- a/src/Discussion/Search/Gambit/UnreadGambit.php
+++ b/src/Discussion/Filter/UnreadFilterGambit.php
@@ -7,21 +7,18 @@
* LICENSE file that was distributed with this source code.
*/
-namespace Flarum\Discussion\Search\Gambit;
+namespace Flarum\Discussion\Filter;
use Flarum\Discussion\DiscussionRepository;
-use Flarum\Discussion\Search\DiscussionSearch;
+use Flarum\Filter\FilterInterface;
+use Flarum\Filter\FilterState;
use Flarum\Search\AbstractRegexGambit;
-use Flarum\Search\AbstractSearch;
-use LogicException;
+use Flarum\Search\SearchState;
+use Flarum\User\User;
+use Illuminate\Database\Query\Builder;
-class UnreadGambit extends AbstractRegexGambit
+class UnreadFilterGambit extends AbstractRegexGambit implements FilterInterface
{
- /**
- * {@inheritdoc}
- */
- protected $pattern = 'is:unread';
-
/**
* @var \Flarum\Discussion\DiscussionRepository
*/
@@ -38,18 +35,35 @@ class UnreadGambit extends AbstractRegexGambit
/**
* {@inheritdoc}
*/
- protected function conditions(AbstractSearch $search, array $matches, $negate)
+ public function getGambitPattern()
{
- if (! $search instanceof DiscussionSearch) {
- throw new LogicException('This gambit can only be applied on a DiscussionSearch');
- }
+ return 'is:unread';
+ }
- $actor = $search->getActor();
+ /**
+ * {@inheritdoc}
+ */
+ protected function conditions(SearchState $search, array $matches, $negate)
+ {
+ $this->constrain($search->getQuery(), $search->getActor(), $negate);
+ }
+ public function getFilterKey(): string
+ {
+ return 'unread';
+ }
+
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ $this->constrain($filterState->getQuery(), $filterState->getActor(), $negate);
+ }
+
+ protected function constrain(Builder $query, User $actor, bool $negate)
+ {
if ($actor->exists) {
$readIds = $this->discussions->getReadIds($actor);
- $search->getQuery()->where(function ($query) use ($readIds, $negate, $actor) {
+ $query->where(function ($query) use ($readIds, $negate, $actor) {
if (! $negate) {
$query->whereNotIn('id', $readIds)->where('last_posted_at', '>', $actor->marked_all_as_read_at ?: 0);
} else {
diff --git a/src/Discussion/Search/DiscussionSearch.php b/src/Discussion/Search/DiscussionSearch.php
deleted file mode 100644
index 4a07492b2..000000000
--- a/src/Discussion/Search/DiscussionSearch.php
+++ /dev/null
@@ -1,51 +0,0 @@
- 'desc'];
-
- /**
- * @var array
- */
- protected $relevantPostIds = [];
-
- /**
- * Get the related IDs for each result.
- *
- * @return int[]
- */
- public function getRelevantPostIds()
- {
- return $this->relevantPostIds;
- }
-
- /**
- * Set the relevant post IDs for the results.
- *
- * @param array $relevantPostIds
- * @return void
- */
- public function setRelevantPostIds(array $relevantPostIds)
- {
- $this->relevantPostIds = $relevantPostIds;
- }
-}
diff --git a/src/Discussion/Search/DiscussionSearcher.php b/src/Discussion/Search/DiscussionSearcher.php
index edc755b8c..05a16a3bb 100644
--- a/src/Discussion/Search/DiscussionSearcher.php
+++ b/src/Discussion/Search/DiscussionSearcher.php
@@ -11,10 +11,10 @@ namespace Flarum\Discussion\Search;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Discussion\Event\Searching;
-use Flarum\Search\AbstractSearch;
use Flarum\Search\AbstractSearcher;
use Flarum\Search\GambitManager;
use Flarum\Search\SearchCriteria;
+use Flarum\Search\SearchState;
use Flarum\User\User;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
@@ -50,15 +50,10 @@ class DiscussionSearcher extends AbstractSearcher
return $this->discussions->query()->select('discussions.*')->whereVisibleTo($actor);
}
- protected function getSearch(Builder $query, User $actor): AbstractSearch
- {
- return new DiscussionSearch($query->getQuery(), $actor);
- }
-
/**
* @deprecated along with the Searching event, remove in Beta 17.
*/
- protected function mutateSearch(AbstractSearch $search, SearchCriteria $criteria)
+ protected function mutateSearch(SearchState $search, SearchCriteria $criteria)
{
parent::mutateSearch($search, $criteria);
diff --git a/src/Discussion/Search/Gambit/AuthorGambit.php b/src/Discussion/Search/Gambit/AuthorGambit.php
deleted file mode 100644
index 819a8885b..000000000
--- a/src/Discussion/Search/Gambit/AuthorGambit.php
+++ /dev/null
@@ -1,57 +0,0 @@
-users = $users;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function conditions(AbstractSearch $search, array $matches, $negate)
- {
- if (! $search instanceof DiscussionSearch) {
- throw new LogicException('This gambit can only be applied on a DiscussionSearch');
- }
-
- $usernames = trim($matches[1], '"');
- $usernames = explode(',', $usernames);
-
- $ids = [];
- foreach ($usernames as $username) {
- $ids[] = $this->users->getIdForUsername($username);
- }
-
- $search->getQuery()->whereIn('discussions.user_id', $ids, 'and', $negate);
- }
-}
diff --git a/src/Discussion/Search/Gambit/CreatedGambit.php b/src/Discussion/Search/Gambit/CreatedGambit.php
deleted file mode 100644
index 3d24d5f0f..000000000
--- a/src/Discussion/Search/Gambit/CreatedGambit.php
+++ /dev/null
@@ -1,43 +0,0 @@
-getQuery()->whereDate('created_at', $negate ? '!=' : '=', $matches[1]);
- } else {
- $search->getQuery()->whereBetween('created_at', [$matches[1], $matches[3]], 'and', $negate);
- }
- }
-}
diff --git a/src/Discussion/Search/Gambit/FulltextGambit.php b/src/Discussion/Search/Gambit/FulltextGambit.php
index ea3fe3ede..f57e13c76 100644
--- a/src/Discussion/Search/Gambit/FulltextGambit.php
+++ b/src/Discussion/Search/Gambit/FulltextGambit.php
@@ -9,24 +9,18 @@
namespace Flarum\Discussion\Search\Gambit;
-use Flarum\Discussion\Search\DiscussionSearch;
use Flarum\Post\Post;
-use Flarum\Search\AbstractSearch;
use Flarum\Search\GambitInterface;
+use Flarum\Search\SearchState;
use Illuminate\Database\Query\Expression;
-use LogicException;
class FulltextGambit implements GambitInterface
{
/**
* {@inheritdoc}
*/
- public function apply(AbstractSearch $search, $bit)
+ public function apply(SearchState $search, $bit)
{
- if (! $search instanceof DiscussionSearch) {
- throw new LogicException('This gambit can only be applied on a DiscussionSearch');
- }
-
// 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
diff --git a/src/Discussion/Search/Gambit/HiddenGambit.php b/src/Discussion/Search/Gambit/HiddenGambit.php
deleted file mode 100644
index e658ba0e6..000000000
--- a/src/Discussion/Search/Gambit/HiddenGambit.php
+++ /dev/null
@@ -1,41 +0,0 @@
-getQuery()->where(function ($query) use ($negate) {
- if ($negate) {
- $query->whereNull('hidden_at')->where('comment_count', '>', 0);
- } else {
- $query->whereNotNull('hidden_at')->orWhere('comment_count', 0);
- }
- });
- }
-}
diff --git a/src/Extend/Filter.php b/src/Extend/Filter.php
new file mode 100644
index 000000000..49f54c541
--- /dev/null
+++ b/src/Extend/Filter.php
@@ -0,0 +1,74 @@
+filtererClass = $filtererClass;
+ }
+
+ /**
+ * Add a filter to run when the filtererClass is filtered.
+ *
+ * @param string $filterClass: The ::class attribute of the filter you are adding.
+ */
+ public function addFilter(string $filterClass)
+ {
+ $this->filters[] = $filterClass;
+
+ return $this;
+ }
+
+ /**
+ * Add a callback through which to run all filter queries after filters have been applied.
+ *
+ * @param callable|string $callback
+ *
+ * The callback can be a closure or an invokable class, and should accept:
+ * - Flarum\Filter\FilterState $filter
+ * - Flarum\Search\SearchCriteria $criteria
+ */
+ public function addFilterMutator($callback)
+ {
+ $this->filterMutators[] = $callback;
+
+ return $this;
+ }
+
+ public function extend(Container $container, Extension $extension = null)
+ {
+ $container->extend('flarum.filter.filters', function ($originalFilters) {
+ foreach ($this->filters as $filter) {
+ $originalFilters[$this->filtererClass][] = $filter;
+ }
+
+ return $originalFilters;
+ });
+ $container->extend('flarum.filter.filter_mutators', function ($originalMutators) {
+ foreach ($this->filterMutators as $mutator) {
+ $originalMutators[$this->filtererClass][] = $mutator;
+ }
+
+ return $originalMutators;
+ });
+ }
+}
diff --git a/src/Extend/SimpleFlarumSearch.php b/src/Extend/SimpleFlarumSearch.php
index 6f308a506..678ed6754 100644
--- a/src/Extend/SimpleFlarumSearch.php
+++ b/src/Extend/SimpleFlarumSearch.php
@@ -60,7 +60,7 @@ class SimpleFlarumSearch implements ExtenderInterface
* @param callable|string $callback
*
* The callback can be a closure or an invokable class, and should accept:
- * - Flarum\Search\AbstractSearch $search
+ * - Flarum\Search\SearchState $search
* - Flarum\Search\SearchCriteria $criteria
*/
public function addSearchMutator($callback)
diff --git a/src/Filter/AbstractFilterer.php b/src/Filter/AbstractFilterer.php
new file mode 100644
index 000000000..c7ddc0542
--- /dev/null
+++ b/src/Filter/AbstractFilterer.php
@@ -0,0 +1,86 @@
+filters = $filters;
+ $this->filterMutators = $filterMutators;
+ }
+
+ abstract protected function getQuery(User $actor): Builder;
+
+ /**
+ * @param SearchCriteria $criteria
+ * @param mixed|null $limit
+ * @param int $offset
+ *
+ * @return SearchResults
+ * @throws InvalidArgumentException
+ */
+ public function filter(SearchCriteria $criteria, int $limit = null, int $offset = 0): SearchResults
+ {
+ $actor = $criteria->actor;
+
+ $query = $this->getQuery($actor);
+
+ $filterState = new FilterState($query->getQuery(), $actor);
+
+ foreach ($criteria->query as $filterKey => $filterValue) {
+ $negate = false;
+ if (substr($filterKey, 0, 1) == '-') {
+ $negate = true;
+ $filterKey = substr($filterKey, 1);
+ }
+ foreach (Arr::get($this->filters, $filterKey, []) as $filter) {
+ $filter->filter($filterState, $filterValue, $negate);
+ }
+ }
+
+ $this->applySort($filterState, $criteria->sort);
+ $this->applyOffset($filterState, $offset);
+ $this->applyLimit($filterState, $limit + 1);
+
+ foreach ($this->filterMutators as $mutator) {
+ $mutator($query, $actor, $criteria->query, $criteria->sort);
+ }
+
+ // Execute the filter query and retrieve the results. We get one more
+ // results than the user asked for, so that we can say if there are more
+ // results. If there are, we will get rid of that extra result.
+ $results = $query->get();
+
+ if ($areMoreResults = $limit > 0 && $results->count() > $limit) {
+ $results->pop();
+ }
+
+ return new SearchResults($results, $areMoreResults);
+ }
+}
diff --git a/src/Filter/FilterInterface.php b/src/Filter/FilterInterface.php
new file mode 100644
index 000000000..a5853c4bd
--- /dev/null
+++ b/src/Filter/FilterInterface.php
@@ -0,0 +1,26 @@
+app->singleton('flarum.filter.filters', function () {
+ return [
+ DiscussionFilterer::class => [
+ AuthorFilterGambit::class,
+ CreatedFilterGambit::class,
+ HiddenFilterGambit::class,
+ UnreadFilterGambit::class,
+ ],
+ UserFilterer::class => [
+ EmailFilterGambit::class,
+ GroupFilterGambit::class,
+ ]
+ ];
+ });
+
+ $this->app->singleton('flarum.filter.filter_mutators', function () {
+ return [];
+ });
+ }
+
+ public function boot()
+ {
+ // We can resolve the filter mutators in the when->needs->give callback,
+ // but we need to resolve at least one regardless so we know which
+ // filterers we need to register filters for.
+ $filters = $this->app->make('flarum.filter.filters');
+
+ foreach ($filters as $filterer => $filterClasses) {
+ $this->app
+ ->when($filterer)
+ ->needs('$filters')
+ ->give(function () use ($filterClasses) {
+ $compiled = [];
+
+ foreach ($filterClasses as $filterClass) {
+ $filter = $this->app->make($filterClass);
+ $compiled[$filter->getFilterKey()][] = $filter;
+ }
+
+ return $compiled;
+ });
+
+ $this->app
+ ->when($filterer)
+ ->needs('$filterMutators')
+ ->give(function () use ($filterer) {
+ return array_map(function ($filterMutatorClass) {
+ return ContainerUtil::wrapCallback($filterMutatorClass, $this->app);
+ }, Arr::get($this->app->make('flarum.filter.filter_mutators'), $filterer, []));
+ });
+ }
+ }
+}
diff --git a/src/Filter/FilterState.php b/src/Filter/FilterState.php
new file mode 100644
index 000000000..cc5a4a259
--- /dev/null
+++ b/src/Filter/FilterState.php
@@ -0,0 +1,87 @@
+query = $query;
+ $this->actor = $actor;
+ $this->defaultSort = $defaultSort;
+ }
+
+ /**
+ * Get the query builder for the search results query.
+ *
+ * @return Builder
+ */
+ public function getQuery()
+ {
+ return $this->query;
+ }
+
+ /**
+ * Get the user who is performing the search.
+ *
+ * @return User
+ */
+ public function getActor()
+ {
+ return $this->actor;
+ }
+
+ /**
+ * Get the default sort order for the search.
+ *
+ * @return array
+ */
+ public function getDefaultSort()
+ {
+ return $this->defaultSort;
+ }
+
+ /**
+ * Set the default sort order for the search. This will only be applied if
+ * a sort order has not been specified in the search criteria.
+ *
+ * @param mixed $defaultSort An array of sort-order pairs, where the column
+ * is the key, and the order is the value. The order may be 'asc',
+ * 'desc', or an array of IDs to order by.
+ * Alternatively, a callable may be used.
+ * @return mixed
+ */
+ public function setDefaultSort($defaultSort)
+ {
+ $this->defaultSort = $defaultSort;
+ }
+}
diff --git a/src/Foundation/InstalledSite.php b/src/Foundation/InstalledSite.php
index 8e944e827..ec52fb9cc 100644
--- a/src/Foundation/InstalledSite.php
+++ b/src/Foundation/InstalledSite.php
@@ -16,6 +16,7 @@ use Flarum\Console\ConsoleServiceProvider;
use Flarum\Database\DatabaseServiceProvider;
use Flarum\Discussion\DiscussionServiceProvider;
use Flarum\Extension\ExtensionServiceProvider;
+use Flarum\Filter\FilterServiceProvider;
use Flarum\Formatter\FormatterServiceProvider;
use Flarum\Forum\ForumServiceProvider;
use Flarum\Frontend\FrontendServiceProvider;
@@ -117,6 +118,7 @@ class InstalledSite implements SiteInterface
$laravel->register(ExtensionServiceProvider::class);
$laravel->register(ErrorServiceProvider::class);
$laravel->register(FilesystemServiceProvider::class);
+ $laravel->register(FilterServiceProvider::class);
$laravel->register(FormatterServiceProvider::class);
$laravel->register(ForumServiceProvider::class);
$laravel->register(FrontendServiceProvider::class);
diff --git a/src/Search/AbstractRegexGambit.php b/src/Search/AbstractRegexGambit.php
index 204b05c32..c1dd0fc2c 100644
--- a/src/Search/AbstractRegexGambit.php
+++ b/src/Search/AbstractRegexGambit.php
@@ -13,15 +13,15 @@ abstract class AbstractRegexGambit implements GambitInterface
{
/**
* The regex pattern to match the bit against.
- *
- * @var string
*/
- protected $pattern;
+ protected function getGambitPattern()
+ {
+ }
/**
* {@inheritdoc}
*/
- public function apply(AbstractSearch $search, $bit)
+ public function apply(SearchState $search, $bit)
{
if ($matches = $this->match($bit)) {
list($negate) = array_splice($matches, 1, 1);
@@ -40,7 +40,8 @@ abstract class AbstractRegexGambit implements GambitInterface
*/
protected function match($bit)
{
- if (preg_match('/^(-?)'.$this->pattern.'$/i', $bit, $matches)) {
+ // @deprecated, remove use of $this->pattern during beta 17.
+ if (preg_match('/^(-?)'.($this->pattern ?? $this->getGambitPattern()).'$/i', $bit, $matches)) {
return $matches;
}
}
@@ -48,11 +49,12 @@ abstract class AbstractRegexGambit implements GambitInterface
/**
* Apply conditions to the search, given that the gambit was matched.
*
- * @param AbstractSearch $search The search object.
+ * @param SearchState $search The search object.
* @param array $matches An array of matches from the search bit.
* @param bool $negate Whether or not the bit was negated, and thus whether
* or not the conditions should be negated.
* @return mixed
*/
- abstract protected function conditions(AbstractSearch $search, array $matches, $negate);
+ // Uncomment for beta 17
+ // abstract protected function conditions(SearchState $search, array $matches, $negate);
}
diff --git a/src/Search/AbstractSearch.php b/src/Search/AbstractSearch.php
index 1de7dcac9..adac8ceb2 100644
--- a/src/Search/AbstractSearch.php
+++ b/src/Search/AbstractSearch.php
@@ -9,90 +9,19 @@
namespace Flarum\Search;
-use Flarum\User\User;
-use Illuminate\Database\Query\Builder;
+use Flarum\Filter\FilterState;
/**
- * An object which represents the internal state of a generic search:
- * the search query, the user performing the search, the fallback sort order,
- * and a log of which gambits have been used.
+ * @deprecated, use SearchState instead.
+ * These methods should be transferred over to SearchState in beta 17.
*/
-abstract class AbstractSearch
+class AbstractSearch extends FilterState
{
- /**
- * @var Builder
- */
- protected $query;
-
- /**
- * @var User
- */
- protected $actor;
-
- /**
- * @var array
- */
- protected $defaultSort = [];
-
/**
* @var GambitInterface[]
*/
protected $activeGambits = [];
- /**
- * @param Builder $query
- * @param User $actor
- */
- public function __construct(Builder $query, User $actor)
- {
- $this->query = $query;
- $this->actor = $actor;
- }
-
- /**
- * Get the query builder for the search results query.
- *
- * @return Builder
- */
- public function getQuery()
- {
- return $this->query;
- }
-
- /**
- * Get the user who is performing the search.
- *
- * @return User
- */
- public function getActor()
- {
- return $this->actor;
- }
-
- /**
- * Get the default sort order for the search.
- *
- * @return array
- */
- public function getDefaultSort()
- {
- return $this->defaultSort;
- }
-
- /**
- * Set the default sort order for the search. This will only be applied if
- * a sort order has not been specified in the search criteria.
- *
- * @param mixed $defaultSort An array of sort-order pairs, where the column
- * is the key, and the order is the value. The order may be 'asc',
- * 'desc', or an array of IDs to order by.
- * @return mixed
- */
- public function setDefaultSort($defaultSort)
- {
- $this->defaultSort = $defaultSort;
- }
-
/**
* Get a list of the gambits that are active in this search.
*
diff --git a/src/Search/AbstractSearcher.php b/src/Search/AbstractSearcher.php
index 9ccb33d59..58de86243 100644
--- a/src/Search/AbstractSearcher.php
+++ b/src/Search/AbstractSearcher.php
@@ -34,9 +34,7 @@ abstract class AbstractSearcher
abstract protected function getQuery(User $actor): Builder;
- abstract protected function getSearch(Builder $query, User $actor): AbstractSearch;
-
- protected function mutateSearch(AbstractSearch $search, SearchCriteria $criteria)
+ protected function mutateSearch(SearchState $search, SearchCriteria $criteria)
{
foreach ($this->searchMutators as $mutator) {
$mutator($search, $criteria);
@@ -49,16 +47,17 @@ abstract class AbstractSearcher
* @param int $offset
*
* @return SearchResults
+ * @throws InvalidArgumentException
*/
- public function search(SearchCriteria $criteria, $limit = null, $offset = 0, array $load = [])
+ public function search(SearchCriteria $criteria, $limit = null, $offset = 0): SearchResults
{
$actor = $criteria->actor;
$query = $this->getQuery($actor);
- $search = $this->getSearch($query, $actor);
+ $search = new SearchState($query->getQuery(), $actor);
- $this->gambits->apply($search, $criteria->query);
+ $this->gambits->apply($search, $criteria->query['q']);
$this->applySort($search, $criteria->sort);
$this->applyOffset($search, $offset);
$this->applyLimit($search, $limit + 1);
@@ -74,8 +73,6 @@ abstract class AbstractSearcher
$results->pop();
}
- $results->load($load);
-
return new SearchResults($results, $areMoreResults);
}
}
diff --git a/src/Search/ApplySearchParametersTrait.php b/src/Search/ApplySearchParametersTrait.php
index 0e5798743..2b77a5bd8 100644
--- a/src/Search/ApplySearchParametersTrait.php
+++ b/src/Search/ApplySearchParametersTrait.php
@@ -9,6 +9,7 @@
namespace Flarum\Search;
+use Flarum\Filter\FilterState;
use Illuminate\Support\Str;
trait ApplySearchParametersTrait
@@ -16,10 +17,10 @@ trait ApplySearchParametersTrait
/**
* Apply sort criteria to a discussion search.
*
- * @param AbstractSearch $search
+ * @param FilterState $search
* @param array $sort
*/
- protected function applySort(AbstractSearch $search, array $sort = null)
+ protected function applySort(FilterState $search, array $sort = null)
{
$sort = $sort ?: $search->getDefaultSort();
@@ -39,10 +40,10 @@ trait ApplySearchParametersTrait
}
/**
- * @param AbstractSearch $search
+ * @param FilterState $search
* @param int $offset
*/
- protected function applyOffset(AbstractSearch $search, $offset)
+ protected function applyOffset(FilterState $search, $offset)
{
if ($offset > 0) {
$search->getQuery()->skip($offset);
@@ -50,10 +51,10 @@ trait ApplySearchParametersTrait
}
/**
- * @param AbstractSearch $search
+ * @param FilterState $search
* @param int|null $limit
*/
- protected function applyLimit(AbstractSearch $search, $limit)
+ protected function applyLimit(FilterState $search, $limit)
{
if ($limit > 0) {
$search->getQuery()->take($limit);
diff --git a/src/Search/GambitInterface.php b/src/Search/GambitInterface.php
index 4a190a2c8..cbb691e63 100644
--- a/src/Search/GambitInterface.php
+++ b/src/Search/GambitInterface.php
@@ -14,9 +14,9 @@ interface GambitInterface
/**
* Apply conditions to the searcher for a bit of the search string.
*
- * @param AbstractSearch $search
+ * @param SearchState $search
* @param string $bit The piece of the search string.
* @return bool Whether or not the gambit was active for this bit.
*/
- public function apply(AbstractSearch $search, $bit);
+ public function apply(SearchState $search, $bit);
}
diff --git a/src/Search/GambitManager.php b/src/Search/GambitManager.php
index 775af42b6..b9e5b0b0f 100644
--- a/src/Search/GambitManager.php
+++ b/src/Search/GambitManager.php
@@ -55,10 +55,10 @@ class GambitManager
/**
* Apply gambits to a search, given a search query.
*
- * @param AbstractSearch $search
+ * @param SearchState $search
* @param string $query
*/
- public function apply(AbstractSearch $search, $query)
+ public function apply(SearchState $search, $query)
{
$query = $this->applyGambits($search, $query);
@@ -89,11 +89,11 @@ class GambitManager
}
/**
- * @param AbstractSearch $search
+ * @param SearchState $search
* @param string $query
* @return string
*/
- protected function applyGambits(AbstractSearch $search, $query)
+ protected function applyGambits(SearchState $search, $query)
{
$bits = $this->explode($query);
@@ -121,10 +121,10 @@ class GambitManager
}
/**
- * @param AbstractSearch $search
+ * @param SearchState $search
* @param string $query
*/
- protected function applyFulltext(AbstractSearch $search, $query)
+ protected function applyFulltext(SearchState $search, $query)
{
if (! $this->fulltextGambit) {
return;
diff --git a/src/Search/SearchCriteria.php b/src/Search/SearchCriteria.php
index 6e8dd6e36..30820dd67 100644
--- a/src/Search/SearchCriteria.php
+++ b/src/Search/SearchCriteria.php
@@ -13,22 +13,22 @@ use Flarum\User\User;
/**
* Represents the criteria that will determine the entire result set of a
- * search. The limit and offset are not included because they only determine
+ * query. The limit and offset are not included because they only determine
* which part of the entire result set will be returned.
*/
class SearchCriteria
{
/**
- * The user performing the search.
+ * The user performing the query.
*
* @var User
*/
public $actor;
/**
- * The search query.
+ * Query params.
*
- * @var string
+ * @var array
*/
public $query;
@@ -42,8 +42,8 @@ class SearchCriteria
public $sort;
/**
- * @param User $actor The user performing the search.
- * @param string $query The search query.
+ * @param User $actor The user performing the query.
+ * @param array $query The query params.
* @param array $sort An array of sort-order pairs, where the column is the
* key, and the order is the value. The order may be 'asc', 'desc', or
* an array of IDs to order by.
diff --git a/src/Search/SearchServiceProvider.php b/src/Search/SearchServiceProvider.php
index fee38bf95..264958f92 100644
--- a/src/Search/SearchServiceProvider.php
+++ b/src/Search/SearchServiceProvider.php
@@ -9,19 +9,19 @@
namespace Flarum\Search;
+use Flarum\Discussion\Filter\AuthorFilterGambit;
+use Flarum\Discussion\Filter\CreatedFilterGambit;
+use Flarum\Discussion\Filter\HiddenFilterGambit;
+use Flarum\Discussion\Filter\UnreadFilterGambit;
use Flarum\Discussion\Search\DiscussionSearcher;
-use Flarum\Discussion\Search\Gambit\AuthorGambit;
-use Flarum\Discussion\Search\Gambit\CreatedGambit;
use Flarum\Discussion\Search\Gambit\FulltextGambit as DiscussionFulltextGambit;
-use Flarum\Discussion\Search\Gambit\HiddenGambit;
-use Flarum\Discussion\Search\Gambit\UnreadGambit;
use Flarum\Event\ConfigureDiscussionGambits;
use Flarum\Event\ConfigureUserGambits;
use Flarum\Foundation\AbstractServiceProvider;
use Flarum\Foundation\ContainerUtil;
-use Flarum\User\Search\Gambit\EmailGambit;
+use Flarum\User\Filter\EmailFilterGambit;
+use Flarum\User\Filter\GroupFilterGambit;
use Flarum\User\Search\Gambit\FulltextGambit as UserFulltextGambit;
-use Flarum\User\Search\Gambit\GroupGambit;
use Flarum\User\Search\UserSearcher;
use Illuminate\Support\Arr;
@@ -42,14 +42,14 @@ class SearchServiceProvider extends AbstractServiceProvider
$this->app->singleton('flarum.simple_search.gambits', function () {
return [
DiscussionSearcher::class => [
- AuthorGambit::class,
- CreatedGambit::class,
- HiddenGambit::class,
- UnreadGambit::class
+ AuthorFilterGambit::class,
+ CreatedFilterGambit::class,
+ HiddenFilterGambit::class,
+ UnreadFilterGambit::class
],
UserSearcher::class => [
- EmailGambit::class,
- GroupGambit::class
+ EmailFilterGambit::class,
+ GroupFilterGambit::class
]
];
});
diff --git a/src/User/Search/UserSearch.php b/src/Search/SearchState.php
similarity index 63%
rename from src/User/Search/UserSearch.php
rename to src/Search/SearchState.php
index 37106e538..b57fbeee2 100644
--- a/src/User/Search/UserSearch.php
+++ b/src/Search/SearchState.php
@@ -7,10 +7,8 @@
* LICENSE file that was distributed with this source code.
*/
-namespace Flarum\User\Search;
+namespace Flarum\Search;
-use Flarum\Search\AbstractSearch;
-
-class UserSearch extends AbstractSearch
+class SearchState extends AbstractSearch
{
}
diff --git a/src/User/Event/Searching.php b/src/User/Event/Searching.php
index ffa0db36c..ff6debac7 100644
--- a/src/User/Event/Searching.php
+++ b/src/User/Event/Searching.php
@@ -10,7 +10,7 @@
namespace Flarum\User\Event;
use Flarum\Search\SearchCriteria;
-use Flarum\User\Search\UserSearch;
+use Flarum\Search\SearchState;
/**
* @deprecated beta 16, remove beta 17
@@ -18,7 +18,7 @@ use Flarum\User\Search\UserSearch;
class Searching
{
/**
- * @var \Flarum\User\Search\UserSearch
+ * @var \Flarum\User\Search\SearchState
*/
public $search;
@@ -28,10 +28,10 @@ class Searching
public $criteria;
/**
- * @param UserSearch $search
+ * @param SearchState $search
* @param SearchCriteria $criteria
*/
- public function __construct(UserSearch $search, SearchCriteria $criteria)
+ public function __construct(SearchState $search, SearchCriteria $criteria)
{
$this->search = $search;
$this->criteria = $criteria;
diff --git a/src/User/Filter/EmailFilterGambit.php b/src/User/Filter/EmailFilterGambit.php
new file mode 100644
index 000000000..3fc4ae9ab
--- /dev/null
+++ b/src/User/Filter/EmailFilterGambit.php
@@ -0,0 +1,68 @@
+getActor()->hasPermission('user.edit')) {
+ return false;
+ }
+
+ return parent::apply($search, $bit);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getGambitPattern()
+ {
+ return 'email:(.+)';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function conditions(SearchState $search, array $matches, $negate)
+ {
+ $this->constrain($search->getQuery(), $matches[1], $negate);
+ }
+
+ public function getFilterKey(): string
+ {
+ return 'email';
+ }
+
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ if (! $filterState->getActor()->hasPermission('user.edit')) {
+ return;
+ }
+
+ $this->constrain($filterState->getQuery(), $filterValue, $negate);
+ }
+
+ protected function constrain(Builder $query, $rawEmail, bool $negate)
+ {
+ $email = trim($rawEmail, '"');
+
+ $query->where('email', $negate ? '!=' : '=', $email);
+ }
+}
diff --git a/src/User/Filter/GroupFilterGambit.php b/src/User/Filter/GroupFilterGambit.php
new file mode 100644
index 000000000..4d1c445f8
--- /dev/null
+++ b/src/User/Filter/GroupFilterGambit.php
@@ -0,0 +1,68 @@
+constrain($search->getQuery(), $search->getActor(), $matches[1], $negate);
+ }
+
+ public function getFilterKey(): string
+ {
+ return 'group';
+ }
+
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ $this->constrain($filterState->getQuery(), $filterState->getActor(), $filterValue, $negate);
+ }
+
+ protected function constrain(Builder $query, User $actor, string $rawQuery, bool $negate)
+ {
+ $groupIdentifiers = explode(',', trim($rawQuery, '"'));
+
+ $groupQuery = Group::whereVisibleTo($actor);
+
+ foreach ($groupIdentifiers as $identifier) {
+ if (is_numeric($identifier)) {
+ $groupQuery->orWhere('id', $identifier);
+ } else {
+ $groupQuery->orWhere('name_singular', $identifier)->orWhere('name_plural', $identifier);
+ }
+ }
+
+ $userIds = $groupQuery->join('group_user', 'groups.id', 'group_user.group_id')
+ ->pluck('group_user.user_id')
+ ->all();
+
+ $query->whereIn('id', $userIds, 'and', $negate);
+ }
+}
diff --git a/src/User/Filter/UserFilterer.php b/src/User/Filter/UserFilterer.php
new file mode 100644
index 000000000..418dca379
--- /dev/null
+++ b/src/User/Filter/UserFilterer.php
@@ -0,0 +1,40 @@
+users = $users;
+ }
+
+ protected function getQuery(User $actor): Builder
+ {
+ return $this->users->query()->whereVisibleTo($actor);
+ }
+}
diff --git a/src/User/Search/Gambit/EmailGambit.php b/src/User/Search/Gambit/EmailGambit.php
deleted file mode 100644
index f3ca088b6..000000000
--- a/src/User/Search/Gambit/EmailGambit.php
+++ /dev/null
@@ -1,63 +0,0 @@
-users = $users;
- }
-
- /**
- * {@inheritdoc}
- */
- public function apply(AbstractSearch $search, $bit)
- {
- if (! $search->getActor()->hasPermission('user.edit')) {
- return false;
- }
-
- return parent::apply($search, $bit);
- }
-
- /**
- * {@inheritdoc}
- */
- protected function conditions(AbstractSearch $search, array $matches, $negate)
- {
- if (! $search instanceof UserSearch) {
- throw new LogicException('This gambit can only be applied on a UserSearch');
- }
-
- $email = trim($matches[1], '"');
-
- $search->getQuery()->where('email', $negate ? '!=' : '=', $email);
- }
-}
diff --git a/src/User/Search/Gambit/FulltextGambit.php b/src/User/Search/Gambit/FulltextGambit.php
index 410d74810..0522e76d2 100644
--- a/src/User/Search/Gambit/FulltextGambit.php
+++ b/src/User/Search/Gambit/FulltextGambit.php
@@ -9,8 +9,8 @@
namespace Flarum\User\Search\Gambit;
-use Flarum\Search\AbstractSearch;
use Flarum\Search\GambitInterface;
+use Flarum\Search\SearchState;
use Flarum\User\UserRepository;
class FulltextGambit implements GambitInterface
@@ -43,7 +43,7 @@ class FulltextGambit implements GambitInterface
/**
* {@inheritdoc}
*/
- public function apply(AbstractSearch $search, $searchValue)
+ public function apply(SearchState $search, $searchValue)
{
$search->getQuery()
->whereIn(
diff --git a/src/User/Search/Gambit/GroupGambit.php b/src/User/Search/Gambit/GroupGambit.php
deleted file mode 100644
index 5e113e6df..000000000
--- a/src/User/Search/Gambit/GroupGambit.php
+++ /dev/null
@@ -1,77 +0,0 @@
-groups = $groups;
- }
-
- /**
- * {@inheritdoc}
- */
- protected function conditions(AbstractSearch $search, array $matches, $negate)
- {
- if (! $search instanceof UserSearch) {
- throw new LogicException('This gambit can only be applied on a UserSearch');
- }
-
- $groupIdentifiers = $this->extractGroupIdentifiers($matches);
-
- $groupQuery = Group::whereVisibleTo($search->getActor());
-
- foreach ($groupIdentifiers as $identifier) {
- if (is_numeric($identifier)) {
- $groupQuery->orWhere('id', $identifier);
- } else {
- $groupQuery->orWhere('name_singular', $identifier)->orWhere('name_plural', $identifier);
- }
- }
-
- $userIds = $groupQuery->join('group_user', 'groups.id', 'group_user.group_id')
- ->pluck('group_user.user_id')
- ->all();
-
- $search->getQuery()->whereIn('id', $userIds, 'and', $negate);
- }
-
- /**
- * Extract the group names from the pattern match.
- *
- * @param array $matches
- * @return array
- */
- protected function extractGroupIdentifiers(array $matches)
- {
- return explode(',', trim($matches[1], '"'));
- }
-}
diff --git a/src/User/Search/UserSearcher.php b/src/User/Search/UserSearcher.php
index cecd75651..d0ac09970 100644
--- a/src/User/Search/UserSearcher.php
+++ b/src/User/Search/UserSearcher.php
@@ -9,10 +9,10 @@
namespace Flarum\User\Search;
-use Flarum\Search\AbstractSearch;
use Flarum\Search\AbstractSearcher;
use Flarum\Search\GambitManager;
use Flarum\Search\SearchCriteria;
+use Flarum\Search\SearchState;
use Flarum\User\Event\Searching;
use Flarum\User\User;
use Flarum\User\UserRepository;
@@ -54,15 +54,10 @@ class UserSearcher extends AbstractSearcher
return $this->users->query()->whereVisibleTo($actor);
}
- protected function getSearch(Builder $query, User $actor): AbstractSearch
- {
- return new UserSearch($query->getQuery(), $actor);
- }
-
/**
* @deprecated along with the Searching event, remove in Beta 17.
*/
- protected function mutateSearch(AbstractSearch $search, SearchCriteria $criteria)
+ protected function mutateSearch(SearchState $search, SearchCriteria $criteria)
{
parent::mutateSearch($search, $criteria);
diff --git a/tests/integration/api/discussions/ListTest.php b/tests/integration/api/discussions/ListTest.php
index 48e1ee949..9a16f411f 100644
--- a/tests/integration/api/discussions/ListTest.php
+++ b/tests/integration/api/discussions/ListTest.php
@@ -12,6 +12,8 @@ namespace Flarum\Tests\integration\api\discussions;
use Carbon\Carbon;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
+use Flarum\User\User;
+use Illuminate\Support\Arr;
class ListTest extends TestCase
{
@@ -26,10 +28,16 @@ class ListTest extends TestCase
$this->prepareDatabase([
'discussions' => [
- ['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1],
+ ['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'last_posted_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1],
+ ['id' => 2, 'title' => 'lightsail in title', 'created_at' => Carbon::createFromDate(1985, 5, 21)->toDateTimeString(), 'last_posted_at' => Carbon::createFromDate(1985, 5, 21)->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
+ ['id' => 3, 'title' => 'not in title', 'created_at' => Carbon::createFromDate(1995, 5, 21)->toDateTimeString(), 'last_posted_at' => Carbon::createFromDate(1995, 5, 21)->toDateTimeString(), 'user_id' => 2, 'comment_count' => 1],
+ ['id' => 4, 'title' => 'hidden', 'created_at' => Carbon::createFromDate(2005, 5, 21)->toDateTimeString(), 'last_posted_at' => Carbon::createFromDate(2005, 5, 21)->toDateTimeString(), 'hidden_at' => Carbon::now()->toDateTimeString(), 'user_id' => 1, 'comment_count' => 1],
],
'posts' => [
- ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'foo bar
'],
+ ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => 'foo bar
'],
+ ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1985, 5, 21)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'not in text
'],
+ ['id' => 3, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1995, 5, 21)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'lightsail in text
'],
+ ['id' => 4, 'discussion_id' => 4, 'created_at' => Carbon::createFromDate(2005, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => 'lightsail in text
'],
],
'users' => [
$this->normalUser(),
@@ -37,6 +45,16 @@ class ListTest extends TestCase
]);
}
+ /**
+ * Mark some discussions, but not others, as read to test that filter/gambit.
+ */
+ protected function read()
+ {
+ $user = User::find(2);
+ $user->marked_all_as_read_at = Carbon::createFromDate(1990, 0, 0)->toDateTimeString();
+ $user->save();
+ }
+
/**
* @test
*/
@@ -49,22 +67,398 @@ class ListTest extends TestCase
$this->assertEquals(200, $response->getStatusCode());
$data = json_decode($response->getBody()->getContents(), true);
- $this->assertEquals(1, count($data['data']));
+ $this->assertEquals(3, count($data['data']));
}
/**
* @test
*/
- public function can_search_for_author()
+ public function author_filter_works()
{
$response = $this->send(
$this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['author' => 'normal'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function author_filter_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['-author' => 'normal'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_filter_works_with_date()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['created' => '1995-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_filter_works_negated_with_date()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['-created' => '1995-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_filter_works_with_range()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['created' => '1980-05-21..2000-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_filter_works_negated_with_range()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['-created' => '1980-05-21..2000-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function hidden_filter_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 1])
+ ->withQueryParams([
+ 'filter' => ['hidden' => ''],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['4'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function hidden_filter_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 1])
+ ->withQueryParams([
+ 'filter' => ['-hidden' => ''],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1', '2', '3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function unread_filter_works()
+ {
+ $this->app();
+ $this->read();
+
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 2])
->withQueryParams([
- 'filter' => ['q' => 'author:normal foo'],
+ 'filter' => ['unread' => ''],
'include' => 'mostRelevantPost',
])
);
- $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function unread_filter_works_when_negated()
+ {
+ $this->app();
+ $this->read();
+
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 2])
+ ->withQueryParams([
+ 'filter' => ['-unread' => ''],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function author_gambit_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['q' => 'author:normal'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function author_gambit_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['q' => '-author:normal'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_gambit_works_with_date()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['q' => 'created:1995-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_gambit_works_negated_with_date()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['q' => '-created:1995-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_gambit_works_with_range()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['q' => 'created:1980-05-21..2000-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function created_gambit_works_negated_with_range()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions')
+ ->withQueryParams([
+ 'filter' => ['q' => '-created:1980-05-21..2000-05-21'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function hidden_gambit_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 1])
+ ->withQueryParams([
+ 'filter' => ['q' => 'is:hidden'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['4'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function hidden_gambit_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 1])
+ ->withQueryParams([
+ 'filter' => ['q' => '-is:hidden'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1', '2', '3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function unread_gambit_works()
+ {
+ $this->app();
+ $this->read();
+
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 2])
+ ->withQueryParams([
+ 'filter' => ['q' => 'is:unread'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
+ }
+
+ /**
+ * @test
+ */
+ public function unread_gambit_works_when_negated()
+ {
+ $this->app();
+ $this->read();
+
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', ['authenticatedAs' => 2])
+ ->withQueryParams([
+ 'filter' => ['q' => '-is:unread'],
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ // Order-independent comparison
+ $this->assertEquals(['1', '2'], Arr::pluck($data, 'id'), 'IDs do not match', 0.0, 10, true);
}
}
diff --git a/tests/integration/api/users/ListTest.php b/tests/integration/api/users/ListTest.php
index c2b05a941..72199e3b4 100644
--- a/tests/integration/api/users/ListTest.php
+++ b/tests/integration/api/users/ListTest.php
@@ -11,11 +11,26 @@ namespace Flarum\Tests\integration\api\users;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
+use Illuminate\Support\Arr;
class ListTest extends TestCase
{
use RetrievesAuthorizedUsers;
+ /**
+ * @inheritDoc
+ */
+ protected function setUp(): void
+ {
+ parent::setUp();
+
+ $this->prepareDatabase([
+ 'users' => [
+ $this->normalUser(),
+ ],
+ ]);
+ }
+
/**
* @test
*/
@@ -59,4 +74,200 @@ class ListTest extends TestCase
$this->assertEquals(200, $response->getStatusCode());
}
+
+ /**
+ * @test
+ */
+ public function shows_full_results_without_search_or_filter()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['1', '2'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function group_filter_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['group' => '1'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function group_filter_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['-group' => '1'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['2'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function email_filter_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['email' => 'admin@machine.local'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function email_filter_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['-email' => 'admin@machine.local'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['2'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function email_filter_only_works_for_admin()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 2,
+ ])->withQueryParams([
+ 'filter' => ['email' => 'admin@machine.local'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['1', '2'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function group_gambit_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['q' => 'group:1'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function group_gambit_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['q' => '-group:1'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['2'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function email_gambit_works()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['q' => 'email:admin@machine.local'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['1'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function email_gambit_works_negated()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => ['q' => '-email:admin@machine.local'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals(['2'], Arr::pluck($data, 'id'));
+ }
+
+ /**
+ * @test
+ */
+ public function email_gambit_only_works_for_admin()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/users', [
+ 'authenticatedAs' => 2,
+ ])->withQueryParams([
+ 'filter' => ['q' => 'email:admin@machine.local'],
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+ $this->assertEquals([], Arr::pluck($data, 'id'));
+ }
}
diff --git a/tests/integration/extenders/FilterTest.php b/tests/integration/extenders/FilterTest.php
new file mode 100644
index 000000000..68b81bd7e
--- /dev/null
+++ b/tests/integration/extenders/FilterTest.php
@@ -0,0 +1,134 @@
+prepareDatabase([
+ 'discussions' => [
+ ['id' => 1, 'title' => 'DISCUSSION 1', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 1],
+ ['id' => 2, 'title' => 'DISCUSSION 2', 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'first_post_id' => 2, 'comment_count' => 1],
+ ],
+ 'posts' => [
+ ['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'foo bar
'],
+ ['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now()->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => 'foo bar not the same
'],
+ ],
+ 'users' => [
+ $this->normalUser(),
+ ],
+ ]);
+ }
+
+ public function filterDiscussions($filters, $limit = null)
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/discussions', [
+ 'authenticatedAs' => 1,
+ ])->withQueryParams([
+ 'filter' => $filters,
+ 'include' => 'mostRelevantPost',
+ ])
+ );
+
+ return json_decode($response->getBody()->getContents(), true)['data'];
+ }
+
+ /**
+ * @test
+ */
+ public function works_as_expected_with_no_modifications()
+ {
+ $this->prepDb();
+
+ $searchForAll = json_encode($this->filterDiscussions([], 5));
+ $this->assertContains('DISCUSSION 1', $searchForAll);
+ $this->assertContains('DISCUSSION 2', $searchForAll);
+ }
+
+ /**
+ * @test
+ */
+ public function custom_filter_has_effect_if_added()
+ {
+ $this->extend((new Extend\Filter(DiscussionFilterer::class))->addFilter(NoResultFilter::class));
+
+ $this->prepDb();
+
+ $withResultSearch = json_encode($this->filterDiscussions(['noResult' => 0], 5));
+ $this->assertContains('DISCUSSION 1', $withResultSearch);
+ $this->assertContains('DISCUSSION 2', $withResultSearch);
+ $this->assertEquals([], $this->filterDiscussions(['noResult' => 1], 5));
+ }
+
+ /**
+ * @test
+ */
+ public function filter_mutator_has_effect_if_added()
+ {
+ $this->extend((new Extend\Filter(DiscussionFilterer::class))->addFilterMutator(function ($query, $actor, $filters, $sort) {
+ $query->getQuery()->whereRaw('1=0');
+ }));
+
+ $this->prepDb();
+
+ $this->assertEquals([], $this->filterDiscussions([], 5));
+ }
+
+ /**
+ * @test
+ */
+ public function filter_mutator_has_effect_if_added_with_invokable_class()
+ {
+ $this->extend((new Extend\Filter(DiscussionFilterer::class))->addFilterMutator(CustomFilterMutator::class));
+
+ $this->prepDb();
+
+ $this->assertEquals([], $this->filterDiscussions([], 5));
+ }
+}
+
+class NoResultFilter implements FilterInterface
+{
+ public function getFilterKey(): string
+ {
+ return 'noResult';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function filter(FilterState $filterState, string $filterValue, bool $negate)
+ {
+ if ($filterValue) {
+ $filterState->getQuery()
+ ->whereRaw('0=1');
+ }
+ }
+}
+
+class CustomFilterMutator
+{
+ public function __invoke($query, $actor, $filters, $sort)
+ {
+ $query->getQuery()->whereRaw('1=0');
+ }
+}
diff --git a/tests/integration/extenders/SimpleFlarumSearchTest.php b/tests/integration/extenders/SimpleFlarumSearchTest.php
index 2a874cf76..52d8dfa9a 100644
--- a/tests/integration/extenders/SimpleFlarumSearchTest.php
+++ b/tests/integration/extenders/SimpleFlarumSearchTest.php
@@ -13,9 +13,9 @@ use Carbon\Carbon;
use Flarum\Discussion\Search\DiscussionSearcher;
use Flarum\Extend;
use Flarum\Search\AbstractRegexGambit;
-use Flarum\Search\AbstractSearch;
use Flarum\Search\GambitInterface;
use Flarum\Search\SearchCriteria;
+use Flarum\Search\SearchState;
use Flarum\Tests\integration\RetrievesAuthorizedUsers;
use Flarum\Tests\integration\TestCase;
use Flarum\User\User;
@@ -64,7 +64,7 @@ class SimpleFlarumSearchTest extends TestCase
$actor = User::find(1);
- $criteria = new SearchCriteria($actor, $query);
+ $criteria = new SearchCriteria($actor, ['q' => $query]);
return $this->app()->getContainer()->make(DiscussionSearcher::class)->search($criteria, $limit)->getResults();
}
@@ -142,7 +142,7 @@ class NoResultFullTextGambit implements GambitInterface
/**
* {@inheritdoc}
*/
- public function apply(AbstractSearch $search, $searchValue)
+ public function apply(SearchState $search, $searchValue)
{
$search->getQuery()
->whereRaw('0=1');
@@ -151,12 +151,18 @@ class NoResultFullTextGambit implements GambitInterface
class NoResultFilterGambit extends AbstractRegexGambit
{
- protected $pattern = 'noResult:(.+)';
+ /**
+ * {@inheritdoc}
+ */
+ public function getGambitPattern()
+ {
+ return 'noResult:(.+)';
+ }
/**
* {@inheritdoc}
*/
- public function conditions(AbstractSearch $search, array $matches, $negate)
+ public function conditions(SearchState $search, array $matches, $negate)
{
$noResults = trim($matches[1], ' ');
if ($noResults == '1') {