From 73ff024f8a3cca310bded35ed9bd1d3d0b96e88a Mon Sep 17 00:00:00 2001 From: Sami Mazouz Date: Sat, 21 Oct 2023 17:52:42 +0100 Subject: [PATCH] feat: allow getting total search results and replacing filters (#3906) * feat: allow accessing total search results * feat: allow replacing filters * chore: phpstan --- framework/core/src/Extend/SearchDriver.php | 37 +++++++++++++++++-- .../src/Search/Database/AbstractSearcher.php | 29 ++++++++++++++- framework/core/src/Search/SearchResults.php | 10 ++++- .../extenders/SearchDriverTest.php | 18 +++++++++ 4 files changed, 88 insertions(+), 6 deletions(-) diff --git a/framework/core/src/Extend/SearchDriver.php b/framework/core/src/Extend/SearchDriver.php index b0a5069ce4..3e4a227652 100644 --- a/framework/core/src/Extend/SearchDriver.php +++ b/framework/core/src/Extend/SearchDriver.php @@ -18,6 +18,7 @@ use Flarum\Search\SearchCriteria; use Flarum\Search\SearchState; use Illuminate\Contracts\Container\Container; +use Illuminate\Support\Arr; class SearchDriver implements ExtenderInterface { @@ -64,7 +65,28 @@ public function addSearcher(string $modelClass, string $searcherClass): self */ public function addFilter(string $searcherClass, string $filterClass): self { - $this->filters[$searcherClass][] = $filterClass; + $this->filters[$searcherClass][] = compact('filterClass'); + + return $this; + } + + /** + * Replace a filter from 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 $replaceFilterClass : The ::class attribute of the filter you are replacing. + * @param class-string $filterClass : The ::class attribute of the filter you are adding. + * This filter must implement \Flarum\Search\FilterInterface + * @return self + */ + public function replaceFilter(string $searcherClass, string $replaceFilterClass, string $filterClass): self + { + $this->filters[$searcherClass][] = [ + 'replace' => $replaceFilterClass, + 'filterClass' => $filterClass, + ]; return $this; } @@ -130,9 +152,18 @@ public function extend(Container $container, Extension $extension = null): void $container->extend('flarum.search.filters', function (array $oldFilters) { foreach ($this->filters as $searcherClass => $filters) { - $oldFilters[$searcherClass] = array_merge( + // Start by removing any filters that will be replaced. + $oldFilters[$searcherClass] = array_filter( $oldFilters[$searcherClass] ?? [], - $filters + function ($filter) use ($filters) { + return ! in_array($filter, Arr::pluck($filters, 'replace')); + } + ); + + // Add the new filters. + $oldFilters[$searcherClass] = array_merge( + $oldFilters[$searcherClass], + Arr::pluck($filters, 'filterClass') ); } diff --git a/framework/core/src/Search/Database/AbstractSearcher.php b/framework/core/src/Search/Database/AbstractSearcher.php index 1f29681b47..86f026f0ea 100644 --- a/framework/core/src/Search/Database/AbstractSearcher.php +++ b/framework/core/src/Search/Database/AbstractSearcher.php @@ -9,11 +9,12 @@ namespace Flarum\Search\Database; +use Closure; 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 implements SearcherInterface @@ -53,7 +54,31 @@ public function search(SearchCriteria $criteria): SearchResults $results->pop(); } - return new SearchResults($results, $areMoreResults); + return new SearchResults($results, $areMoreResults, $this->getTotalResults($query)); + } + + protected function getTotalResults(Builder $query): Closure + { + return function () use ($query) { + $query = $query->toBase(); + + if ($query->unions) { + $query->unions = null; // @phpstan-ignore-line + $query->unionLimit = null; // @phpstan-ignore-line + $query->unionOffset = null; // @phpstan-ignore-line + $query->unionOrders = null; // @phpstan-ignore-line + $query->setBindings([], 'union'); + } + + $query->offset = null; // @phpstan-ignore-line + $query->limit = null; // @phpstan-ignore-line + $query->orders = null; // @phpstan-ignore-line + $query->setBindings([], 'order'); + + return $query->getConnection() + ->table($query, 'results') + ->count(); + }; } protected function applySort(DatabaseSearchState $state, ?array $sort = null, bool $sortIsDefault = false): void diff --git a/framework/core/src/Search/SearchResults.php b/framework/core/src/Search/SearchResults.php index 9df3a85336..fb3dde8231 100644 --- a/framework/core/src/Search/SearchResults.php +++ b/framework/core/src/Search/SearchResults.php @@ -9,13 +9,16 @@ namespace Flarum\Search; +use Closure; use Illuminate\Database\Eloquent\Collection; class SearchResults { public function __construct( protected Collection $results, - protected bool $areMoreResults + protected bool $areMoreResults, + /** @var Closure(): int */ + protected Closure $totalResults ) { } @@ -28,4 +31,9 @@ public function areMoreResults(): bool { return $this->areMoreResults; } + + public function getTotalResults(): int + { + return ($this->totalResults)(); + } } diff --git a/framework/core/tests/integration/extenders/SearchDriverTest.php b/framework/core/tests/integration/extenders/SearchDriverTest.php index 8c8d8ea3b3..802598da24 100644 --- a/framework/core/tests/integration/extenders/SearchDriverTest.php +++ b/framework/core/tests/integration/extenders/SearchDriverTest.php @@ -12,6 +12,7 @@ use Carbon\Carbon; use Flarum\Discussion\Discussion; use Flarum\Discussion\Search\DiscussionSearcher; +use Flarum\Discussion\Search\Filter\UnreadFilter; use Flarum\Extend; use Flarum\Search\AbstractFulltextFilter; use Flarum\Search\Database\DatabaseSearchDriver; @@ -124,6 +125,23 @@ public function custom_filter_has_effect_if_added() $this->assertEquals('[]', json_encode($this->searchDiscussions('', 5, ['noResult' => '1']))); } + /** + * @test + */ + public function existing_filter_can_be_replaced() + { + $this->extend( + (new Extend\SearchDriver(DatabaseSearchDriver::class)) + ->replaceFilter(DiscussionSearcher::class, UnreadFilter::class, NoResultFilter::class) + ); + + $this->prepDb(); + + $this->assertNotContains(UnreadFilter::class, $this->app()->getContainer()->make('flarum.search.filters')[DiscussionSearcher::class]); + $this->assertContains(NoResultFilter::class, $this->app()->getContainer()->make('flarum.search.filters')[DiscussionSearcher::class]); + $this->assertEquals('[]', json_encode($this->searchDiscussions('', 5, ['noResult' => '1']))); + } + /** * @test */