diff --git a/config/search.php b/config/search.php index ee4d6bd98dd..83eb5749529 100644 --- a/config/search.php +++ b/config/search.php @@ -79,4 +79,31 @@ 'fields' => ['title'], ], + /* + |-------------------------------------------------------------------------- + | Indexing Queue + |-------------------------------------------------------------------------- + | + | Here you may configure the queue name and connection used when indexing + | documents. + | + */ + + 'queue' => env('STATAMIC_SEARCH_QUEUE'), + + 'queue_connection' => env('STATAMIC_SEARCH_QUEUE_CONNECTION'), + + /* + |-------------------------------------------------------------------------- + | Chunk Size + |-------------------------------------------------------------------------- + | + | Here you can configure the chunk size used when indexing documents. + | The higher you make it, the more memory it will use, but the quicker + | the indexing process will be. + | + */ + + 'chunk_size' => 100, + ]; diff --git a/src/Search/Algolia/Index.php b/src/Search/Algolia/Index.php index 137f5d3fb71..6eab453935b 100644 --- a/src/Search/Algolia/Index.php +++ b/src/Search/Algolia/Index.php @@ -39,7 +39,7 @@ public function search($query) return (new Query($this))->query($query); } - protected function insertDocuments(Documents $documents) + public function insertDocuments(Documents $documents) { $documents = $documents->map(function ($item, $id) { $item['objectID'] = $id; diff --git a/src/Search/Comb/Index.php b/src/Search/Comb/Index.php index 343e552aa5a..d3003d93c65 100644 --- a/src/Search/Comb/Index.php +++ b/src/Search/Comb/Index.php @@ -99,7 +99,7 @@ public function delete($document) $this->save($data); } - protected function insertDocuments(Documents $documents) + public function insertDocuments(Documents $documents) { try { $data = $this->data(); diff --git a/src/Search/Index.php b/src/Search/Index.php index 78eaaf65de6..8546c112578 100644 --- a/src/Search/Index.php +++ b/src/Search/Index.php @@ -4,7 +4,6 @@ use Closure; use Statamic\Contracts\Search\Searchable; -use Statamic\Support\Arr; use Statamic\Support\Str; abstract class Index @@ -20,7 +19,7 @@ abstract public function delete($document); abstract public function exists(); - abstract protected function insertDocuments(Documents $documents); + abstract public function insertDocuments(Documents $documents); abstract protected function deleteIndex(); @@ -84,20 +83,27 @@ public function ensureExists() public function insert($document) { - return $this->insertMultiple(Arr::wrap($document)); + return $this->insertMultiple(collect($document)); } public function insertMultiple($documents) { - $documents = (new Documents($documents))->mapWithKeys(function (Searchable $item) { - return [$item->getSearchReference() => $this->searchables()->fields($item)]; - }); - - $this->insertDocuments($documents); + $documents + ->chunk(config('statamic.search.chunk_size')) + ->each(fn ($documents) => InsertMultipleJob::dispatch( + name: Str::beforeLast($this->name, '_'), + locale: $this->locale, + documents: $documents + )); return $this; } + public function fields(Searchable $searchable) + { + return $this->searchables()->fields($searchable); + } + public function shouldIndex($searchable) { return $this->searchables()->contains($searchable); diff --git a/src/Search/InsertMultipleJob.php b/src/Search/InsertMultipleJob.php new file mode 100644 index 00000000000..f723d688745 --- /dev/null +++ b/src/Search/InsertMultipleJob.php @@ -0,0 +1,52 @@ +onConnection($connection = config('statamic.search.queue_connection', config('queue.default'))); + $this->onQueue(config('statamic.search.queue', config("queue.connections.{$connection}.queue"))); + } + + /** + * Execute the job. + */ + public function handle(): void + { + $providers = app(Providers::class); + $index = Search::index($this->name, $this->locale); + + $documents = $this->documents + ->groupBy(fn ($document) => explode('::', $document)[0]) + ->flatMap(function ($documents, $prefix) use ($providers) { + return $providers + ->getByPrefix($prefix) + ->find($documents->map(fn ($reference) => Str::after($reference, '::'))->all()) + ->all(); + }) + ->mapWithKeys(function (Searchable $item) use ($index) { + return [$item->getSearchReference() => $index->fields($item)]; + }); + + $index->insertDocuments(new Documents($documents)); + } +} diff --git a/src/Search/Null/NullIndex.php b/src/Search/Null/NullIndex.php index 52ca4a39c64..10ebc808ade 100644 --- a/src/Search/Null/NullIndex.php +++ b/src/Search/Null/NullIndex.php @@ -22,7 +22,7 @@ public function exists() return true; } - protected function insertDocuments(Documents $documents) + public function insertDocuments(Documents $documents) { // } diff --git a/src/Search/Search.php b/src/Search/Search.php index 890bc331728..ddaeeaa40fc 100644 --- a/src/Search/Search.php +++ b/src/Search/Search.php @@ -51,7 +51,7 @@ public function updateWithinIndexes(Searchable $searchable) $exists = $index->exists(); if ($shouldIndex && $exists) { - $index->insert($searchable); + $index->insert($searchable->getSearchReference()); } elseif ($shouldIndex && ! $exists) { $index->update(); } elseif ($exists) { diff --git a/src/Search/Searchables/Assets.php b/src/Search/Searchables/Assets.php index bc70922cace..a4848314db9 100644 --- a/src/Search/Searchables/Assets.php +++ b/src/Search/Searchables/Assets.php @@ -26,7 +26,9 @@ public function provide(): Collection : AssetCollection::make($this->keys) ->flatMap(fn ($key) => Asset::whereContainer($key)); - return $assets->filter($this->filter())->values(); + // TODO: query scope support? + + return $assets->filter($this->filter())->values()->map->reference(); } public function contains($searchable): bool diff --git a/src/Search/Searchables/Entries.php b/src/Search/Searchables/Entries.php index 5556340b8d3..f9a1a808051 100644 --- a/src/Search/Searchables/Entries.php +++ b/src/Search/Searchables/Entries.php @@ -31,7 +31,19 @@ public function provide(): Collection|LazyCollection $query->where('site', $site); } - return $query->lazy(100)->filter($this->filter())->values(); + $this->applyQueryScope($query); + + if ($this->hasFilter()) { + return $query + ->lazy(config('statamic.search.chunk_size')) + ->filter($this->filter()) + ->values() + ->map->reference(); + } + + $query->where('status', 'published'); + + return $query->pluck('reference'); } public function contains($searchable): bool @@ -48,7 +60,17 @@ public function contains($searchable): bool return false; } - return $this->filter()($searchable); + if ($this->hasFilter()) { + return $this->filter()($searchable); + } + + $query = Entry::query() + ->where('status', 'published') + ->where('id', $searchable->id()); + + $this->applyQueryScope($query); + + return $query->exists(); } public function find(array $ids): Collection diff --git a/src/Search/Searchables/Provider.php b/src/Search/Searchables/Provider.php index c1036ee9597..93252c2c346 100644 --- a/src/Search/Searchables/Provider.php +++ b/src/Search/Searchables/Provider.php @@ -2,9 +2,11 @@ namespace Statamic\Search\Searchables; +use Statamic\Facades\Scope; use Statamic\Facades\Search; use Statamic\Search\Index; use Statamic\Search\ProvidesSearchables; +use Statamic\Support\Arr; abstract class Provider implements ProvidesSearchables { @@ -55,6 +57,20 @@ protected function usesWildcard() return in_array('*', $this->keys); } + protected function applyQueryScope($query) + { + if (! $scope = $this->index->config()['query_scope'] ?? null) { + return; + } + + Scope::find($scope)->apply($query, []); + } + + protected function hasFilter() + { + return Arr::has($this->index->config(), 'filter'); + } + protected function filter() { $filter = $this->index->config()['filter'] ?? null; diff --git a/src/Search/Searchables/Terms.php b/src/Search/Searchables/Terms.php index d7d54337a98..601a0ba65bd 100644 --- a/src/Search/Searchables/Terms.php +++ b/src/Search/Searchables/Terms.php @@ -32,7 +32,17 @@ public function provide(): Collection|LazyCollection $query->where('site', $site); } - return $query->lazy(100)->filter($this->filter())->values(); + $this->applyQueryScope($query); + + if ($this->hasFilter()) { + return $query + ->lazy(config('statamic.search.chunk_size')) + ->filter($this->filter()) + ->values() + ->map->reference(); + } + + return $query->pluck('reference'); } public function contains($searchable): bool @@ -49,7 +59,15 @@ public function contains($searchable): bool return false; } - return $this->filter()($searchable); + if ($this->hasFilter()) { + return $this->filter()($searchable); + } + + $query = Term::query()->where('reference', $searchable->reference()); + + $this->applyQueryScope($query); + + return $query->exists(); } public function find(array $refs): Collection diff --git a/src/Search/Searchables/Users.php b/src/Search/Searchables/Users.php index 31cd91052d9..e14cf1659b4 100644 --- a/src/Search/Searchables/Users.php +++ b/src/Search/Searchables/Users.php @@ -3,6 +3,7 @@ namespace Statamic\Search\Searchables; use Illuminate\Support\Collection; +use Illuminate\Support\LazyCollection; use Statamic\Contracts\Auth\User as UserContract; use Statamic\Facades\User; @@ -18,9 +19,21 @@ public static function referencePrefix(): string return 'user'; } - public function provide(): Collection + public function provide(): Collection|LazyCollection { - return User::all()->filter($this->filter())->values(); + $query = User::query(); + + $this->applyQueryScope($query); + + if ($this->hasFilter()) { + return $query + ->lazy(config('statamic.search.chunk_size')) + ->filter($this->filter()) + ->values() + ->map->reference(); + } + + return $query->pluck('reference'); } public function contains($searchable): bool @@ -29,7 +42,15 @@ public function contains($searchable): bool return false; } - return $this->filter()($searchable); + if ($this->hasFilter()) { + return $this->filter()($searchable); + } + + $query = User::query()->where('id', $searchable->id()); + + $this->applyQueryScope($query); + + return $query->exists(); } public function find(array $ids): Collection diff --git a/tests/Search/SearchTest.php b/tests/Search/SearchTest.php index 91ec395514a..fb796c2d206 100644 --- a/tests/Search/SearchTest.php +++ b/tests/Search/SearchTest.php @@ -19,6 +19,7 @@ public function it_updates_indexes($updateMock) { $index = Mockery::mock(Index::class); $item = Mockery::mock(Searchable::class); + $item->shouldReceive('getSearchReference')->andReturn('a'); $updateMock($index, $item); @@ -37,7 +38,7 @@ public static function saveProvider() function ($mock, $entry) { $mock->shouldReceive('shouldIndex')->with($entry)->andReturnTrue(); $mock->shouldReceive('exists')->andReturnTrue(); - $mock->shouldReceive('insert')->once()->with($entry); + $mock->shouldReceive('insert')->once()->with($entry->getSearchReference()); }, ], diff --git a/tests/Search/Searchables/AssetsTest.php b/tests/Search/Searchables/AssetsTest.php index da1902bab7d..e8da1350c60 100644 --- a/tests/Search/Searchables/AssetsTest.php +++ b/tests/Search/Searchables/AssetsTest.php @@ -41,12 +41,12 @@ public function it_gets_assets($locale, $config, $expected) $provider = $this->makeProvider($locale, $config); // Check if it provides the expected assets. - $this->assertEquals($expected, $provider->provide()->map->filename()->all()); + $this->assertEquals($expected, $provider->provide()->all()); // Check if the assets are contained by the provider or not. foreach (Asset::all() as $asset) { $this->assertEquals( - $shouldBeIn = in_array($asset->filename(), $expected), + $shouldBeIn = in_array($asset->reference(), $expected), $provider->contains($asset), "Asset {$asset->filename()} should ".($shouldBeIn ? '' : 'not ').'be contained in the provider.' ); @@ -59,64 +59,64 @@ public static function assetsProvider() 'all' => [ null, ['searchables' => 'all'], - ['a', 'b', 'y', 'z'], + ['asset::images::a.jpg', 'asset::images::b.jpg', 'asset::documents::y.txt', 'asset::documents::z.txt'], ], 'all containers' => [ null, ['searchables' => ['assets:*']], - ['a', 'b', 'y', 'z'], + ['asset::images::a.jpg', 'asset::images::b.jpg', 'asset::documents::y.txt', 'asset::documents::z.txt'], ], 'images' => [ null, ['searchables' => ['assets:images']], - ['a', 'b'], + ['asset::images::a.jpg', 'asset::images::b.jpg'], ], 'documents' => [ null, ['searchables' => ['assets:documents']], - ['y', 'z'], + ['asset::documents::y.txt', 'asset::documents::z.txt'], ], 'all, english' => [ 'en', ['searchables' => 'all'], - ['a', 'b', 'y', 'z'], + ['asset::images::a.jpg', 'asset::images::b.jpg', 'asset::documents::y.txt', 'asset::documents::z.txt'], ], 'all containers, english' => [ 'en', ['searchables' => ['assets:*']], - ['a', 'b', 'y', 'z'], + ['asset::images::a.jpg', 'asset::images::b.jpg', 'asset::documents::y.txt', 'asset::documents::z.txt'], ], 'images, english' => [ 'en', ['searchables' => ['assets:images']], - ['a', 'b'], + ['asset::images::a.jpg', 'asset::images::b.jpg'], ], 'documents, english' => [ 'en', ['searchables' => ['assets:documents']], - ['y', 'z'], + ['asset::documents::y.txt', 'asset::documents::z.txt'], ], 'all, french' => [ 'fr', ['searchables' => 'all'], - ['a', 'b', 'y', 'z'], + ['asset::images::a.jpg', 'asset::images::b.jpg', 'asset::documents::y.txt', 'asset::documents::z.txt'], ], 'all containers, french' => [ 'fr', ['searchables' => ['assets:*']], - ['a', 'b', 'y', 'z'], + ['asset::images::a.jpg', 'asset::images::b.jpg', 'asset::documents::y.txt', 'asset::documents::z.txt'], ], 'images, french' => [ 'fr', ['searchables' => ['assets:images']], - ['a', 'b'], + ['asset::images::a.jpg', 'asset::images::b.jpg'], ], 'documents, french' => [ 'fr', ['searchables' => ['assets:documents']], - ['y', 'z'], + ['asset::documents::y.txt', 'asset::documents::z.txt'], ], ]; } @@ -142,7 +142,10 @@ public function it_can_use_a_custom_filter($filter) 'filter' => $filter, ]); - $this->assertEquals(['a', 'c', 'd'], $provider->provide()->map->filename()->all()); + $this->assertEquals( + ['asset::images::a.jpg', 'asset::images::c.jpg', 'asset::images::d.jpg'], + $provider->provide()->all() + ); $this->assertTrue($provider->contains($a)); $this->assertFalse($provider->contains($b)); @@ -162,6 +165,8 @@ function ($entry) { ]; } + // TODO: query scope support? + private function makeProvider($locale, $config) { $index = $this->makeIndex($locale, $config); diff --git a/tests/Search/Searchables/EntriesTest.php b/tests/Search/Searchables/EntriesTest.php index 003bff7c2c1..a79d4a8713b 100644 --- a/tests/Search/Searchables/EntriesTest.php +++ b/tests/Search/Searchables/EntriesTest.php @@ -7,6 +7,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Entries\Entry; use Statamic\Facades\Collection; +use Statamic\Query\Scopes\Scope; use Statamic\Search\Searchables\Entries; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -27,26 +28,26 @@ public function it_gets_entries($locale, $config, $expected) Collection::make('blog')->sites(['en', 'fr'])->save(); Collection::make('pages')->sites(['en'])->save(); - EntryFactory::collection('blog')->slug('alfa')->create(); - EntryFactory::collection('blog')->slug('bravo')->published(false)->create(); - EntryFactory::collection('blog')->slug('charlie')->create(); - EntryFactory::collection('blog')->slug('delta')->locale('fr')->create(); - EntryFactory::collection('blog')->slug('echo')->locale('fr')->published(false)->create(); - EntryFactory::collection('blog')->slug('foxtrot')->locale('fr')->create(); + EntryFactory::collection('blog')->id('alfa')->create(); + EntryFactory::collection('blog')->id('bravo')->published(false)->create(); + EntryFactory::collection('blog')->id('charlie')->create(); + EntryFactory::collection('blog')->id('delta')->locale('fr')->create(); + EntryFactory::collection('blog')->id('echo')->locale('fr')->published(false)->create(); + EntryFactory::collection('blog')->id('foxtrot')->locale('fr')->create(); - EntryFactory::collection('pages')->slug('xray')->create(); - EntryFactory::collection('pages')->slug('yankee')->published(false)->create(); - EntryFactory::collection('pages')->slug('zulu')->create(); + EntryFactory::collection('pages')->id('xray')->create(); + EntryFactory::collection('pages')->id('yankee')->published(false)->create(); + EntryFactory::collection('pages')->id('zulu')->create(); $provider = $this->makeProvider($locale, $config); // Check if it provides the expected entries. - $this->assertEquals($expected, $provider->provide()->map->slug()->all()); + $this->assertEquals($expected, $provider->provide()->all()); // Check if the entries are contained by the provider or not. foreach (Entry::all() as $entry) { $this->assertEquals( - $shouldBeIn = in_array($entry->slug(), $expected), + $shouldBeIn = in_array($entry->reference(), $expected), $provider->contains($entry), "Entry {$entry->slug()} should ".($shouldBeIn ? '' : 'not ').'be contained in the provider.' ); @@ -59,59 +60,59 @@ public static function entriesProvider() 'all' => [ null, ['searchables' => 'all'], - ['alfa', 'charlie', 'delta', 'foxtrot', 'xray', 'zulu'], + ['entry::alfa', 'entry::charlie', 'entry::delta', 'entry::foxtrot', 'entry::xray', 'entry::zulu'], ], 'all collections' => [ null, ['searchables' => ['collection:*']], - ['alfa', 'charlie', 'delta', 'foxtrot', 'xray', 'zulu'], + ['entry::alfa', 'entry::charlie', 'entry::delta', 'entry::foxtrot', 'entry::xray', 'entry::zulu'], ], 'blog' => [ null, ['searchables' => ['collection:blog']], - ['alfa', 'charlie', 'delta', 'foxtrot'], + ['entry::alfa', 'entry::charlie', 'entry::delta', 'entry::foxtrot'], ], 'pages' => [ null, ['searchables' => ['collection:pages']], - ['xray', 'zulu'], + ['entry::xray', 'entry::zulu'], ], 'all, english' => [ 'en', ['searchables' => 'all'], - ['alfa', 'charlie', 'xray', 'zulu'], + ['entry::alfa', 'entry::charlie', 'entry::xray', 'entry::zulu'], ], 'all collections, english' => [ 'en', ['searchables' => ['collection:*']], - ['alfa', 'charlie', 'xray', 'zulu'], + ['entry::alfa', 'entry::charlie', 'entry::xray', 'entry::zulu'], ], 'blog, english' => [ 'en', ['searchables' => ['collection:blog']], - ['alfa', 'charlie'], + ['entry::alfa', 'entry::charlie'], ], 'pages, english' => [ 'en', ['searchables' => ['collection:pages']], - ['xray', 'zulu'], + ['entry::xray', 'entry::zulu'], ], 'all, french' => [ 'fr', ['searchables' => 'all'], - ['delta', 'foxtrot'], + ['entry::delta', 'entry::foxtrot'], ], 'all collections, french' => [ 'fr', ['searchables' => ['collection:*']], - ['delta', 'foxtrot'], + ['entry::delta', 'entry::foxtrot'], ], 'blog, french' => [ 'fr', ['searchables' => ['collection:blog']], - ['delta', 'foxtrot'], + ['entry::delta', 'entry::foxtrot'], ], 'pages, french' => [ 'fr', @@ -126,18 +127,21 @@ public static function entriesProvider() public function it_can_use_a_custom_filter($filter) { Collection::make('blog')->save(); - $a = EntryFactory::collection('blog')->slug('a')->create(); - $b = EntryFactory::collection('blog')->slug('b')->published(false)->create(); - $c = EntryFactory::collection('blog')->slug('c')->data(['is_searchable' => false])->create(); - $d = EntryFactory::collection('blog')->slug('d')->data(['is_searchable' => true])->create(); - $e = EntryFactory::collection('blog')->slug('e')->create(); + $a = EntryFactory::collection('blog')->id('a')->create(); + $b = EntryFactory::collection('blog')->id('b')->published(false)->create(); + $c = EntryFactory::collection('blog')->id('c')->data(['is_searchable' => false])->create(); + $d = EntryFactory::collection('blog')->id('d')->data(['is_searchable' => true])->create(); + $e = EntryFactory::collection('blog')->id('e')->create(); $provider = $this->makeProvider(null, [ 'searchables' => 'all', 'filter' => $filter, ]); - $this->assertEquals(['a', 'b', 'd', 'e'], $provider->provide()->map->slug()->all()); + $this->assertEquals( + ['entry::a', 'entry::b', 'entry::d', 'entry::e'], + $provider->provide()->all() + ); $this->assertTrue($provider->contains($a)); $this->assertTrue($provider->contains($b)); @@ -158,6 +162,35 @@ function ($entry) { ]; } + #[Test] + public function it_can_use_a_query_scope() + { + CustomEntriesScope::register(); + + Collection::make('blog')->save(); + $a = EntryFactory::collection('blog')->id('a')->create(); + $b = EntryFactory::collection('blog')->id('b')->create(); + $c = EntryFactory::collection('blog')->id('c')->data(['is_searchable' => false])->create(); + $d = EntryFactory::collection('blog')->id('d')->data(['is_searchable' => true])->create(); + $e = EntryFactory::collection('blog')->id('e')->create(); + + $provider = $this->makeProvider(null, [ + 'searchables' => 'all', + 'query_scope' => 'custom_entries_scope', + ]); + + $this->assertEquals( + ['entry::a', 'entry::b', 'entry::d', 'entry::e'], + $provider->provide()->all() + ); + + $this->assertTrue($provider->contains($a)); + $this->assertTrue($provider->contains($b)); + $this->assertFalse($provider->contains($c)); + $this->assertTrue($provider->contains($d)); + $this->assertTrue($provider->contains($e)); + } + private function makeProvider($locale, $config) { $index = $this->makeIndex($locale, $config); @@ -194,3 +227,11 @@ public function handle($item) return $item->get('is_searchable') !== false; } } + +class CustomEntriesScope extends Scope +{ + public function apply($query, $params) + { + $query->where('is_searchable', '!=', false); + } +} diff --git a/tests/Search/Searchables/TermsTest.php b/tests/Search/Searchables/TermsTest.php index 23b63cde015..05d94c58759 100644 --- a/tests/Search/Searchables/TermsTest.php +++ b/tests/Search/Searchables/TermsTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\Taxonomy; use Statamic\Facades\Term; +use Statamic\Query\Scopes\Scope; use Statamic\Search\Searchables\Terms; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -62,7 +63,7 @@ public function it_gets_terms($locale, $config, $expected) $provider = $this->makeProvider($locale, $config); // Check if it provides the expected entries. - $this->assertEquals($expected, $provider->provide()->map->reference()->all()); + $this->assertEquals($expected, $provider->provide()->all()); // Check if the entries are contained by the provider or not. foreach (Term::all() as $term) { @@ -204,7 +205,10 @@ public function it_can_use_a_custom_filter($filter) 'filter' => $filter, ]); - $this->assertEquals(['a', 'c', 'd'], $provider->provide()->map->slug()->all()); + $this->assertEquals( + ['term::tags::a::en', 'term::tags::c::en', 'term::tags::d::en'], + $provider->provide()->all() + ); $this->assertTrue($provider->contains($a->in('en'))); $this->assertFalse($provider->contains($b->in('en'))); @@ -224,6 +228,33 @@ function ($entry) { ]; } + #[Test] + public function it_can_use_a_query_scope() + { + CustomTermsScope::register(); + + Taxonomy::make('tags')->sites(['en'])->save(); + $a = tap(Term::make('a')->taxonomy('tags')->dataForLocale('en', []))->save(); + $b = tap(Term::make('b')->taxonomy('tags')->dataForLocale('en', ['is_searchable' => false]))->save(); + $c = tap(Term::make('c')->taxonomy('tags')->dataForLocale('en', ['is_searchable' => true]))->save(); + $d = tap(Term::make('d')->taxonomy('tags')->dataForLocale('en', []))->save(); + + $provider = $this->makeProvider(null, [ + 'searchables' => 'all', + 'query_scope' => 'custom_terms_scope', + ]); + + $this->assertEquals( + ['term::tags::a::en', 'term::tags::c::en', 'term::tags::d::en'], + $provider->provide()->all() + ); + + $this->assertTrue($provider->contains($a->in('en'))); + $this->assertFalse($provider->contains($b->in('en'))); + $this->assertTrue($provider->contains($c->in('en'))); + $this->assertTrue($provider->contains($d->in('en'))); + } + private function makeProvider($locale, $config) { $index = $this->makeIndex($locale, $config); @@ -260,3 +291,11 @@ public function handle($item) return $item->get('is_searchable') !== false; } } + +class CustomTermsScope extends Scope +{ + public function apply($query, $params) + { + $query->where('is_searchable', '!=', false); + } +} diff --git a/tests/Search/Searchables/UsersTest.php b/tests/Search/Searchables/UsersTest.php index 975cedbb183..a44fecae972 100644 --- a/tests/Search/Searchables/UsersTest.php +++ b/tests/Search/Searchables/UsersTest.php @@ -5,6 +5,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use Statamic\Facades\User; +use Statamic\Query\Scopes\Scope; use Statamic\Search\Searchables\Users; use Tests\PreventSavingStacheItemsToDisk; use Tests\TestCase; @@ -22,18 +23,18 @@ public function it_gets_users($locale, $config, $expected) 'fr' => ['url' => '/fr/', 'locale' => 'fr'], ]); - User::make()->email('alfa@test.com')->save(); - User::make()->email('bravo@test.com')->save(); + User::make()->id('alfa')->email('alfa@test.com')->save(); + User::make()->id('bravo')->email('bravo@test.com')->save(); $provider = $this->makeProvider($locale, $config); // Check if it provides the expected users. - $this->assertEquals($expected, $provider->provide()->map->email()->all()); + $this->assertEquals($expected, $provider->provide()->all()); // Check if the users are contained by the provider or not. foreach (User::all() as $user) { $this->assertEquals( - $shouldBeIn = in_array($user->email(), $expected), + $shouldBeIn = in_array($user->reference(), $expected), $provider->contains($user), "User {$user->email()} should ".($shouldBeIn ? '' : 'not ').'be contained in the provider.' ); @@ -46,34 +47,34 @@ public static function usersProvider() 'all' => [ null, ['searchables' => 'all'], - ['alfa@test.com', 'bravo@test.com'], + ['user::alfa', 'user::bravo'], ], 'all users' => [ null, ['searchables' => ['users']], - ['alfa@test.com', 'bravo@test.com'], + ['user::alfa', 'user::bravo'], ], 'all, english' => [ 'en', ['searchables' => 'all'], - ['alfa@test.com', 'bravo@test.com'], + ['user::alfa', 'user::bravo'], ], 'all users, english' => [ 'en', ['searchables' => ['users']], - ['alfa@test.com', 'bravo@test.com'], + ['user::alfa', 'user::bravo'], ], 'all, french' => [ 'fr', ['searchables' => 'all'], - ['alfa@test.com', 'bravo@test.com'], + ['user::alfa', 'user::bravo'], ], 'all users, french' => [ 'fr', ['searchables' => ['users']], - ['alfa@test.com', 'bravo@test.com'], + ['user::alfa', 'user::bravo'], ], ]; } @@ -82,17 +83,20 @@ public static function usersProvider() #[DataProvider('indexFilterProvider')] public function it_can_use_a_custom_filter($filter) { - $a = tap(User::make()->email('a@test.com'))->save(); - $b = tap(User::make()->email('b@test.com')->set('is_searchable', false))->save(); - $c = tap(User::make()->email('c@test.com')->set('is_searchable', true))->save(); - $d = tap(User::make()->email('d@test.com'))->save(); + $a = tap(User::make()->id('a')->email('a@test.com'))->save(); + $b = tap(User::make()->id('b')->email('b@test.com')->set('is_searchable', false))->save(); + $c = tap(User::make()->id('c')->email('c@test.com')->set('is_searchable', true))->save(); + $d = tap(User::make()->id('d')->email('d@test.com'))->save(); $provider = $this->makeProvider(null, [ 'searchables' => 'all', 'filter' => $filter, ]); - $this->assertEquals(['a@test.com', 'c@test.com', 'd@test.com'], $provider->provide()->map->email()->all()); + $this->assertEquals( + ['user::a', 'user::c', 'user::d'], + $provider->provide()->all() + ); $this->assertTrue($provider->contains($a)); $this->assertFalse($provider->contains($b)); @@ -112,6 +116,32 @@ function ($entry) { ]; } + #[Test] + public function it_can_use_a_query_scope() + { + CustomUsersScope::register(); + + $a = tap(User::make()->id('a')->email('a@test.com'))->save(); + $b = tap(User::make()->id('b')->email('b@test.com')->set('is_searchable', false))->save(); + $c = tap(User::make()->id('c')->email('c@test.com')->set('is_searchable', true))->save(); + $d = tap(User::make()->id('d')->email('d@test.com'))->save(); + + $provider = $this->makeProvider(null, [ + 'searchables' => 'all', + 'query_scope' => 'custom_users_scope', + ]); + + $this->assertEquals( + ['user::a', 'user::c', 'user::d'], + $provider->provide()->all() + ); + + $this->assertTrue($provider->contains($a)); + $this->assertFalse($provider->contains($b)); + $this->assertTrue($provider->contains($c)); + $this->assertTrue($provider->contains($d)); + } + private function makeProvider($locale, $config) { $index = $this->makeIndex($locale, $config); @@ -148,3 +178,11 @@ public function handle($item) return $item->get('is_searchable') !== false; } } + +class CustomUsersScope extends Scope +{ + public function apply($query, $params) + { + $query->where('is_searchable', '!=', false); + } +}