Skip to content

Commit 80a01b5

Browse files
committed
Make and re-index command + tests
1 parent 430d6d5 commit 80a01b5

6 files changed

Lines changed: 371 additions & 33 deletions

File tree

src/Commands/MakeCommand.php

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,41 @@
44

55
namespace PDPhilip\Elasticsearch\Commands;
66

7-
use Illuminate\Console\GeneratorCommand;
7+
use Illuminate\Console\Command;
8+
use Illuminate\Support\Facades\File;
89
use Illuminate\Support\Str;
910
use OmniTerm\HasOmniTerm;
1011

11-
class MakeCommand extends GeneratorCommand
12+
class MakeCommand extends Command
1213
{
1314
use HasOmniTerm;
1415

1516
protected $signature = 'elastic:make {name : The name of the Elasticsearch model}';
1617

1718
protected $description = 'Create a new Elasticsearch model';
1819

19-
protected $type = 'Model';
20-
2120
public function handle(): int
2221
{
2322
$name = Str::studly($this->argument('name'));
24-
2523
$namespace = $this->resolveNamespace($name);
2624
$class = class_basename(str_replace('/', '\\', $name));
2725
$fullClass = $namespace.'\\'.$class;
2826
$path = $this->getModelPath($name);
2927

30-
if (file_exists($path)) {
28+
if (File::exists($path)) {
3129
$this->newLine();
3230
$this->omni->statusError('Model already exists', $fullClass);
3331
$this->newLine();
3432

3533
return self::FAILURE;
3634
}
3735

38-
$this->makeDirectory($path);
36+
$directory = dirname($path);
37+
if (! File::isDirectory($directory)) {
38+
File::makeDirectory($directory, 0755, true);
39+
}
3940

40-
$stub = $this->buildStub($namespace, $class);
41-
$this->files->put($path, $stub);
41+
File::put($path, $this->buildFromStub($namespace, $class));
4242

4343
$this->newLine();
4444
$this->omni->statusSuccess('Model created', $fullClass);
@@ -48,11 +48,25 @@ public function handle(): int
4848
return self::SUCCESS;
4949
}
5050

51-
protected function getStub(): string
51+
// ======================================================================
52+
// Code Generation
53+
// ======================================================================
54+
55+
private function buildFromStub(string $namespace, string $class): string
5256
{
53-
return __DIR__.'/../../resources/stubs/ElasticModel.php.stub';
57+
$stub = File::get(__DIR__.'/../../resources/stubs/ElasticModel.php.stub');
58+
59+
return str_replace(
60+
['{{ namespace }}', '{{ class }}'],
61+
[$namespace, $class],
62+
$stub
63+
);
5464
}
5565

66+
// ======================================================================
67+
// Helpers
68+
// ======================================================================
69+
5670
private function resolveNamespace(string $name): string
5771
{
5872
$parts = explode('/', str_replace('\\', '/', $name));
@@ -71,15 +85,4 @@ private function getModelPath(string $name): string
7185

7286
return app_path('Models/'.$name.'.php');
7387
}
74-
75-
private function buildStub(string $namespace, string $class): string
76-
{
77-
$stub = $this->files->get($this->getStub());
78-
79-
return str_replace(
80-
['{{ namespace }}', '{{ class }}'],
81-
[$namespace, $class],
82-
$stub
83-
);
84-
}
8588
}

src/Commands/ReIndexCommand.php

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use PDPhilip\Elasticsearch\Eloquent\Model;
1515
use PDPhilip\Elasticsearch\Schema\Blueprint;
1616
use PDPhilip\Elasticsearch\Schema\Builder as SchemaBuilder;
17+
use PDPhilip\Elasticsearch\Utils\Helpers;
1718

1819
class ReIndexCommand extends Command
1920
{
@@ -29,6 +30,8 @@ class ReIndexCommand extends Command
2930

3031
private string $connectionName;
3132

33+
private Connection $connection;
34+
3235
private SchemaBuilder $schema;
3336

3437
private Closure $mappingDefinition;
@@ -248,8 +251,8 @@ private function setupFromModel(Model $model): void
248251
$this->connectionName = $model->getConnectionName();
249252

250253
/** @var Connection $connection */
251-
$connection = DB::connection($this->connectionName);
252-
$this->schema = $connection->getSchemaBuilder();
254+
$this->connection = DB::connection($this->connectionName);
255+
$this->schema = $this->connection->getSchemaBuilder();
253256

254257
$this->mappingDefinition = fn (Blueprint $index) => $model::mappingDefinition($index);
255258
}
@@ -406,7 +409,8 @@ private function copyToTemp(): bool
406409
return false;
407410
}
408411

409-
$this->omni->success('Copy complete — '.($result['created'] ?? 0).' docs created');
412+
$this->tempCount = (int) ($result['created'] ?? 0);
413+
$this->omni->success('Copy complete — '.$this->tempCount.' docs created');
410414

411415
return true;
412416
} catch (Exception $e) {
@@ -425,8 +429,6 @@ private function verifyTemp(): bool
425429
{
426430
$this->omni->divider('Phase 4: Verify Temp');
427431

428-
$this->tempCount = $this->countDocs($this->tempIndexName);
429-
430432
$this->omni->tableHeader('Metric', 'Value');
431433
$this->omni->tableRow('Original count', number_format($this->originalCount));
432434
$this->omni->tableRow('Temp count', number_format($this->tempCount));
@@ -516,7 +518,8 @@ private function copyBack(): bool
516518

517519
$failures = $result['failures'] ?? [];
518520
if (empty($failures)) {
519-
$this->omni->success('Copy back complete — '.($result['created'] ?? 0).' docs');
521+
$this->finalCount = (int) ($result['created'] ?? 0);
522+
$this->omni->success('Copy back complete — '.$this->finalCount.' docs');
520523

521524
return true;
522525
}
@@ -544,8 +547,6 @@ private function verifyFinal(): bool
544547
{
545548
$this->omni->divider('Phase 8: Verify Final');
546549

547-
$this->finalCount = $this->countDocs($this->indexName);
548-
549550
$this->omni->tableHeader('Metric', 'Value');
550551
$this->omni->tableRow('Original snapshot', number_format($this->originalCount));
551552
$this->omni->tableRow('Temp count', number_format($this->tempCount));
@@ -561,8 +562,8 @@ private function verifyFinal(): bool
561562
$this->omni->warning('Final count mismatch ('.$this->finalCount.' vs '.$this->tempCount.') — running catch-up');
562563

563564
try {
564-
$this->schema->reindex($this->tempIndexName, $this->indexName);
565-
$this->finalCount = $this->countDocs($this->indexName);
565+
$result = $this->schema->reindex($this->tempIndexName, $this->indexName);
566+
$this->finalCount = (int) ($result['created'] ?? 0) + (int) ($result['updated'] ?? 0);
566567

567568
if ($this->countsMatch($this->tempCount, $this->finalCount)) {
568569
$this->omni->success('Catch-up resolved the mismatch');
@@ -668,7 +669,9 @@ private function mappingMismatches(): array
668669
{
669670
$currentMapping = $this->schema->getFieldsMapping($this->indexName);
670671

671-
$blueprint = new Blueprint($this->indexName);
672+
$blueprint = Helpers::getLaravelCompatabilityVersion() >= 12
673+
? new Blueprint($this->connection, $this->indexName)
674+
: new Blueprint($this->indexName);
672675
($this->mappingDefinition)($blueprint);
673676

674677
$mismatches = [];

tests/MakeCommandTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Illuminate\Support\Facades\File;
6+
7+
beforeEach(function () {
8+
$this->modelsPath = app_path('Models');
9+
10+
if (! File::isDirectory($this->modelsPath)) {
11+
File::makeDirectory($this->modelsPath, 0755, true);
12+
}
13+
});
14+
15+
afterEach(function () {
16+
File::cleanDirectory($this->modelsPath);
17+
});
18+
19+
it('creates a model file', function () {
20+
$this->artisan('elastic:make', ['name' => 'Product'])
21+
->assertSuccessful();
22+
23+
$path = app_path('Models/Product.php');
24+
expect(File::exists($path))->toBeTrue();
25+
26+
$content = File::get($path);
27+
expect($content)
28+
->toContain('namespace App\Models;')
29+
->toContain('class Product extends Eloquent')
30+
->toContain("protected \$connection = 'elasticsearch';");
31+
});
32+
33+
it('fails when the model already exists', function () {
34+
$path = app_path('Models/Product.php');
35+
File::put($path, '<?php // existing');
36+
37+
$this->artisan('elastic:make', ['name' => 'Product'])
38+
->assertFailed();
39+
});
40+
41+
it('creates a model in a subdirectory', function () {
42+
$this->artisan('elastic:make', ['name' => 'Elastic/SearchLog'])
43+
->assertSuccessful();
44+
45+
$path = app_path('Models/Elastic/SearchLog.php');
46+
expect(File::exists($path))->toBeTrue();
47+
48+
$content = File::get($path);
49+
expect($content)
50+
->toContain('namespace App\Models\Elastic;')
51+
->toContain('class SearchLog extends Eloquent');
52+
});
53+
54+
it('converts the name to StudlyCase', function () {
55+
$this->artisan('elastic:make', ['name' => 'audit_log'])
56+
->assertSuccessful();
57+
58+
$path = app_path('Models/AuditLog.php');
59+
expect(File::exists($path))->toBeTrue();
60+
61+
$content = File::get($path);
62+
expect($content)->toContain('class AuditLog extends Eloquent');
63+
});
64+
65+
it('includes the mapping definition from the stub', function () {
66+
$this->artisan('elastic:make', ['name' => 'Event'])
67+
->assertSuccessful();
68+
69+
$content = File::get(app_path('Models/Event.php'));
70+
expect($content)
71+
->toContain('implements HasMappingDefinition')
72+
->toContain('public static function mappingDefinition(Blueprint $index): void')
73+
->toContain("\$index->date('created_at');")
74+
->toContain("\$index->date('updated_at');");
75+
});
76+
77+
it('includes the docblock from the stub', function () {
78+
$this->artisan('elastic:make', ['name' => 'Metric'])
79+
->assertSuccessful();
80+
81+
$content = File::get(app_path('Models/Metric.php'));
82+
expect($content)
83+
->toContain('App\Models\Metric')
84+
->toContain('@property string $id')
85+
->toContain('@property Carbon $created_at')
86+
->toContain('@property Carbon $updated_at');
87+
});
88+
89+
it('creates nested subdirectories', function () {
90+
$this->artisan('elastic:make', ['name' => 'Analytics/Events/ClickEvent'])
91+
->assertSuccessful();
92+
93+
$path = app_path('Models/Analytics/Events/ClickEvent.php');
94+
expect(File::exists($path))->toBeTrue();
95+
96+
$content = File::get($path);
97+
expect($content)
98+
->toContain('namespace App\Models\Analytics\Events;')
99+
->toContain('class ClickEvent extends Eloquent');
100+
});

tests/Models/ReIndexTarget.php

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 PDPhilip\Elasticsearch\Tests\Models;
6+
7+
use PDPhilip\Elasticsearch\Eloquent\HasMappingDefinition;
8+
use PDPhilip\Elasticsearch\Eloquent\Model;
9+
use PDPhilip\Elasticsearch\Schema\Blueprint;
10+
use PDPhilip\Elasticsearch\Tests\Concerns\TestsWithIdStrategies;
11+
12+
class ReIndexTarget extends Model implements HasMappingDefinition
13+
{
14+
use TestsWithIdStrategies;
15+
16+
protected $connection = 'elasticsearch';
17+
18+
protected $table = 're_index_targets';
19+
20+
protected static $unguarded = true;
21+
22+
public static function mappingDefinition(Blueprint $index): void
23+
{
24+
$index->keyword('status');
25+
$index->text('name');
26+
$index->date('created_at');
27+
$index->date('updated_at');
28+
}
29+
}

tests/QueryElasticsearchSpecificTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,13 @@
127127

128128
});
129129

130+
it('counts nested queries', function () {
131+
$count = Post::where('status',2)->whereNestedObject('comments', function (Builder $query) {
132+
$query->where('country', 'USA');
133+
})->count();
134+
expect($count)->toBe(2);
135+
});
136+
130137
it('can search with field boosting', function () {
131138
$users = User::search('John', 'best_fields', ['name' => 5, 'description' => 1])->get();
132139
expect($users)->toHaveCount(2)

0 commit comments

Comments
 (0)