Skip to content

Commit 969752c

Browse files
committed
Add DeleteAllIndexesCommand and PaginatesEloquentModels contract
- Add scout:delete-all-indexes command for deleting all search indexes - Add PaginatesEloquentModels contract for custom engine pagination - Update Builder to check for pagination contracts before fallback - Add unit tests for DeleteAllIndexesCommand - Add integration test for delete-all-indexes with Meilisearch - Register new command in ScoutServiceProvider and test cases
1 parent 43b6e75 commit 969752c

File tree

7 files changed

+222
-2
lines changed

7 files changed

+222
-2
lines changed

src/scout/src/Builder.php

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
use Closure;
88
use Hyperf\Contract\Arrayable;
99
use Hyperf\Database\Connection;
10+
use Hyperf\Contract\LengthAwarePaginatorInterface;
11+
use Hyperf\Contract\PaginatorInterface;
1012
use Hyperf\Paginator\LengthAwarePaginator;
1113
use Hyperf\Paginator\Paginator;
1214
use Hypervel\Database\Eloquent\Collection as EloquentCollection;
1315
use Hypervel\Database\Eloquent\Model;
16+
use Hypervel\Scout\Contracts\PaginatesEloquentModels;
17+
use Hypervel\Scout\Contracts\PaginatesEloquentModelsUsingDatabase;
1418
use Hypervel\Scout\Contracts\SearchableInterface;
1519
use Hypervel\Support\Collection;
1620
use Hypervel\Support\LazyCollection;
@@ -357,12 +361,20 @@ public function simplePaginate(
357361
?int $perPage = null,
358362
string $pageName = 'page',
359363
?int $page = null
360-
): Paginator {
364+
): PaginatorInterface {
361365
$engine = $this->engine();
362366

363367
$page = $page ?? Paginator::resolveCurrentPage($pageName);
364368
$perPage = $perPage ?? $this->model->getPerPage();
365369

370+
if ($engine instanceof PaginatesEloquentModels) {
371+
return $engine->simplePaginate($this, $perPage, $page)->appends('query', $this->query);
372+
}
373+
374+
if ($engine instanceof PaginatesEloquentModelsUsingDatabase) {
375+
return $engine->simplePaginateUsingDatabase($this, $perPage, $pageName, $page)->appends('query', $this->query);
376+
}
377+
366378
$rawResults = $engine->paginate($this, $perPage, $page);
367379
/** @var array<TModel> $mappedModels */
368380
$mappedModels = $engine->map(
@@ -387,12 +399,20 @@ public function paginate(
387399
?int $perPage = null,
388400
string $pageName = 'page',
389401
?int $page = null
390-
): LengthAwarePaginator {
402+
): LengthAwarePaginatorInterface {
391403
$engine = $this->engine();
392404

393405
$page = $page ?? Paginator::resolveCurrentPage($pageName);
394406
$perPage = $perPage ?? $this->model->getPerPage();
395407

408+
if ($engine instanceof PaginatesEloquentModels) {
409+
return $engine->paginate($this, $perPage, $page)->appends('query', $this->query);
410+
}
411+
412+
if ($engine instanceof PaginatesEloquentModelsUsingDatabase) {
413+
return $engine->paginateUsingDatabase($this, $perPage, $pageName, $page)->appends('query', $this->query);
414+
}
415+
396416
$rawResults = $engine->paginate($this, $perPage, $page);
397417
/** @var array<TModel> $mappedModels */
398418
$mappedModels = $engine->map(
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Scout\Console;
6+
7+
use Exception;
8+
use Hypervel\Console\Command;
9+
use Hypervel\Scout\EngineManager;
10+
11+
/**
12+
* Delete all search indexes.
13+
*/
14+
class DeleteAllIndexesCommand extends Command
15+
{
16+
/**
17+
* The name and signature of the console command.
18+
*/
19+
protected ?string $signature = 'scout:delete-all-indexes';
20+
21+
/**
22+
* The console command description.
23+
*/
24+
protected string $description = 'Delete all indexes';
25+
26+
/**
27+
* Execute the console command.
28+
*/
29+
public function handle(EngineManager $manager): int
30+
{
31+
$engine = $manager->engine();
32+
33+
if (! method_exists($engine, 'deleteAllIndexes')) {
34+
$driver = $manager->getDefaultDriver();
35+
36+
$this->error("The [{$driver}] engine does not support deleting all indexes.");
37+
38+
return self::FAILURE;
39+
}
40+
41+
try {
42+
$engine->deleteAllIndexes();
43+
44+
$this->info('All indexes deleted successfully.');
45+
46+
return self::SUCCESS;
47+
} catch (Exception $exception) {
48+
$this->error($exception->getMessage());
49+
50+
return self::FAILURE;
51+
}
52+
}
53+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Scout\Contracts;
6+
7+
use Hyperf\Contract\LengthAwarePaginatorInterface;
8+
use Hyperf\Contract\PaginatorInterface;
9+
use Hypervel\Scout\Builder;
10+
11+
/**
12+
* Contract for engines that handle pagination directly.
13+
*
14+
* Engines implementing this contract return paginators directly from search
15+
* results, bypassing the default Builder pagination logic. This is useful
16+
* for engines that can calculate total counts efficiently during search.
17+
*/
18+
interface PaginatesEloquentModels
19+
{
20+
/**
21+
* Paginate the given search on the engine.
22+
*/
23+
public function paginate(Builder $builder, int $perPage, int $page): LengthAwarePaginatorInterface;
24+
25+
/**
26+
* Paginate the given search on the engine using simple pagination.
27+
*/
28+
public function simplePaginate(Builder $builder, int $perPage, int $page): PaginatorInterface;
29+
}

src/scout/src/ScoutServiceProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Hypervel\Scout;
66

77
use Hyperf\Contract\ConfigInterface;
8+
use Hypervel\Scout\Console\DeleteAllIndexesCommand;
89
use Hypervel\Scout\Console\DeleteIndexCommand;
910
use Hypervel\Scout\Console\FlushCommand;
1011
use Hypervel\Scout\Console\ImportCommand;
@@ -71,6 +72,7 @@ protected function registerPublishing(): void
7172
protected function registerCommands(): void
7273
{
7374
$this->commands([
75+
DeleteAllIndexesCommand::class,
7476
DeleteIndexCommand::class,
7577
FlushCommand::class,
7678
ImportCommand::class,

tests/Scout/Integration/Meilisearch/MeilisearchCommandsIntegrationTest.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,4 +69,32 @@ public function testFlushCommandRemovesModels(): void
6969
$results = SearchableModel::search('')->get();
7070
$this->assertCount(0, $results);
7171
}
72+
73+
public function testDeleteAllIndexesCommandRemovesAllIndexes(): void
74+
{
75+
// Create models without triggering Scout indexing
76+
SearchableModel::withoutSyncingToSearch(function (): void {
77+
SearchableModel::create(['title' => 'First', 'body' => 'Content']);
78+
});
79+
80+
$this->artisan('scout:import', ['model' => SearchableModel::class])
81+
->assertOk();
82+
83+
$this->waitForMeilisearchTasks();
84+
85+
// Verify model is indexed
86+
$results = SearchableModel::search('')->get();
87+
$this->assertCount(1, $results);
88+
89+
// Run the delete-all-indexes command
90+
$this->artisan('scout:delete-all-indexes')
91+
->expectsOutputToContain('All indexes deleted successfully')
92+
->assertOk();
93+
94+
$this->waitForMeilisearchTasks();
95+
96+
// Searching should now fail or return empty because index is gone
97+
// After deleting the index, Meilisearch will auto-create on next search
98+
// so we just verify the command executed successfully
99+
}
72100
}

tests/Scout/Integration/Meilisearch/MeilisearchScoutIntegrationTestCase.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine;
88
use Hypervel\Foundation\Testing\RefreshDatabase;
9+
use Hypervel\Scout\Console\DeleteAllIndexesCommand;
910
use Hypervel\Scout\Console\DeleteIndexCommand;
1011
use Hypervel\Scout\Console\FlushCommand;
1112
use Hypervel\Scout\Console\ImportCommand;
@@ -66,6 +67,7 @@ protected function tearDownInCoroutine(): void
6667
protected function registerScoutCommands(): void
6768
{
6869
Artisan::getArtisan()->resolveCommands([
70+
DeleteAllIndexesCommand::class,
6971
DeleteIndexCommand::class,
7072
FlushCommand::class,
7173
ImportCommand::class,
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Hypervel\Tests\Scout\Unit\Console;
6+
7+
use Exception;
8+
use Hypervel\Scout\Console\DeleteAllIndexesCommand;
9+
use Hypervel\Scout\EngineManager;
10+
use Hypervel\Scout\Engines\CollectionEngine;
11+
use Hypervel\Scout\Engines\MeilisearchEngine;
12+
use Hypervel\Tests\TestCase;
13+
use Mockery as m;
14+
15+
/**
16+
* @internal
17+
* @coversNothing
18+
*/
19+
class DeleteAllIndexesCommandTest extends TestCase
20+
{
21+
public function testDeletesAllIndexesSuccessfully(): void
22+
{
23+
$engine = m::mock(MeilisearchEngine::class);
24+
$engine->shouldReceive('deleteAllIndexes')
25+
->once()
26+
->andReturn([]);
27+
28+
$manager = m::mock(EngineManager::class);
29+
$manager->shouldReceive('engine')
30+
->once()
31+
->andReturn($engine);
32+
33+
$command = m::mock(DeleteAllIndexesCommand::class)->makePartial();
34+
$command->shouldReceive('info')
35+
->once()
36+
->with('All indexes deleted successfully.');
37+
38+
$result = $command->handle($manager);
39+
40+
$this->assertSame(0, $result);
41+
}
42+
43+
public function testFailsWhenEngineDoesNotSupportDeleteAllIndexes(): void
44+
{
45+
$engine = new CollectionEngine();
46+
47+
$manager = m::mock(EngineManager::class);
48+
$manager->shouldReceive('engine')
49+
->once()
50+
->andReturn($engine);
51+
$manager->shouldReceive('getDefaultDriver')
52+
->once()
53+
->andReturn('collection');
54+
55+
$command = m::mock(DeleteAllIndexesCommand::class)->makePartial();
56+
$command->shouldReceive('error')
57+
->once()
58+
->with('The [collection] engine does not support deleting all indexes.');
59+
60+
$result = $command->handle($manager);
61+
62+
$this->assertSame(1, $result);
63+
}
64+
65+
public function testHandlesExceptionFromEngine(): void
66+
{
67+
$engine = m::mock(MeilisearchEngine::class);
68+
$engine->shouldReceive('deleteAllIndexes')
69+
->once()
70+
->andThrow(new Exception('Connection failed'));
71+
72+
$manager = m::mock(EngineManager::class);
73+
$manager->shouldReceive('engine')
74+
->once()
75+
->andReturn($engine);
76+
77+
$command = m::mock(DeleteAllIndexesCommand::class)->makePartial();
78+
$command->shouldReceive('error')
79+
->once()
80+
->with('Connection failed');
81+
82+
$result = $command->handle($manager);
83+
84+
$this->assertSame(1, $result);
85+
}
86+
}

0 commit comments

Comments
 (0)