diff --git a/src/Console/RebuildIndex.php b/src/Console/RebuildIndex.php index 9be5a55..271c317 100644 --- a/src/Console/RebuildIndex.php +++ b/src/Console/RebuildIndex.php @@ -9,40 +9,24 @@ /** * Class RebuildIndex * - * This Artisan command rebuilds the encrypted search index for a given Eloquent model. - * It is designed for maintenance operations where the search index may be outdated, - * corrupted, or needs regeneration after schema or normalization changes. + * Artisan command that rebuilds the encrypted search index for a given Eloquent model. + * It now supports short model names (e.g. "Client") and automatically resolves them + * under the `App\Models` namespace if not fully qualified. * - * The command iterates over all records of the specified model and regenerates - * both "exact" and "prefix" tokens as defined by the model’s - * `HasEncryptedSearchIndex` trait configuration. - * - * Usage example: + * Example: + * php artisan encryption:index-rebuild Client * php artisan encryption:index-rebuild "App\Models\Client" - * - * Options: - * --chunk=100 Number of records processed per batch (default: 100) - * - * Implementation details: - * - Before rebuilding, all existing search index entries for the given model - * are removed. - * - Records are then reprocessed in chunks to prevent memory exhaustion. - * - For each model instance, `updateSearchIndex()` is called to regenerate - * the normalized token rows. - * - * This ensures a clean and consistent index aligned with the current model data. */ class RebuildIndex extends Command { /** * The name and signature of the console command. * - * {model} The fully qualified class name (FQCN) of the Eloquent model. - * {--chunk=100} The number of model records to process per batch. - * * @var string */ - protected $signature = 'encryption:index-rebuild {model : FQCN of the Eloquent model} {--chunk=100}'; + protected $signature = 'encryption:index-rebuild + {model : Model name or FQCN of the Eloquent model} + {--chunk=100 : Number of records processed per batch}'; /** * The console command description. @@ -54,49 +38,54 @@ class RebuildIndex extends Command /** * Execute the console command. * - * This method performs the following steps: - * 1. Validates the provided model class. - * 2. Deletes all existing search index entries for that model. - * 3. Iterates through all model records in configurable chunks. - * 4. Calls `updateSearchIndex()` for each record to regenerate tokens. - * 5. Displays progress and a final summary of processed records. - * - * @return int Command exit code (0 on success, 1 on failure). + * @return int */ public function handle(): int { + $input = trim($this->argument('model')); + + // Automatically resolve models under App\Models namespace if not fully qualified + if (! class_exists($input)) { + $guessed = "App\\Models\\{$input}"; + if (class_exists($guessed)) { + $input = $guessed; + } + } + /** @var class-string $class */ - $class = $this->argument('model'); + $class = $input; if (! class_exists($class)) { - $this->error("Model class not found: {$class}"); + $this->error("Model class not found: {$this->argument('model')}"); return self::FAILURE; } $chunk = (int) $this->option('chunk'); + $this->info("Rebuilding encrypted search index for: {$class}"); + $this->line("Processing in chunks of {$chunk}..."); // Remove all existing search tokens for this model SearchIndex::where('model_type', $class)->delete(); - /** @var \Illuminate\Database\Eloquent\Builder $q */ - $q = $class::query(); + /** @var \Illuminate\Database\Eloquent\Builder $query */ + $query = $class::query(); $count = 0; - // Process model data in chunks to minimize memory usage - $q->chunk($chunk, function ($rows) use (&$count, $class) { - foreach ($rows as $model) { - if (method_exists($class, 'updateSearchIndex')) { - $class::updateSearchIndex($model); + $query->chunk($chunk, function ($models) use (&$count) { + foreach ($models as $model) { + if (method_exists($model, 'updateSearchIndex')) { + $model->updateSearchIndex(); // <-- FIXED } $count++; } + // Write a dot to indicate progress $this->output->write('.'); }); $this->newLine(); - $this->info("Rebuilt index for {$count} records of {$class}."); + $this->info("✅ Rebuilt index for {$count} records of {$class}."); return self::SUCCESS; } diff --git a/src/Observers/SearchIndexObserver.php b/src/Observers/SearchIndexObserver.php index 7d3c8c6..3284337 100644 --- a/src/Observers/SearchIndexObserver.php +++ b/src/Observers/SearchIndexObserver.php @@ -95,7 +95,7 @@ protected function rebuildIndex(Model $model): void { if (method_exists($model, 'updateSearchIndex')) { // @phpstan-ignore-next-line - $model::updateSearchIndex($model); + $model::updateSearchIndex(); } } @@ -113,7 +113,7 @@ protected function removeIndex(Model $model): void { if (method_exists($model, 'removeSearchIndex')) { // @phpstan-ignore-next-line - $model::removeSearchIndex($model); + $model::removeSearchIndex(); } } } diff --git a/src/Traits/HasEncryptedSearchIndex.php b/src/Traits/HasEncryptedSearchIndex.php index b81ceb2..3a09d0f 100644 --- a/src/Traits/HasEncryptedSearchIndex.php +++ b/src/Traits/HasEncryptedSearchIndex.php @@ -47,23 +47,6 @@ */ trait HasEncryptedSearchIndex { - /** - * Defines which fields should be included in the encrypted search index. - * - * Example: - * ```php - * protected array $encryptedSearch = [ - * 'first_names' => ['exact' => true, 'prefix' => true], - * 'last_names' => ['exact' => true], - * ]; - * ``` - * - * Each entry specifies whether an exact or prefix index (or both) - * should be generated for that field. - * - * @var array - */ - protected array $encryptedSearch = []; /** * Boot logic for the trait. @@ -98,37 +81,34 @@ public static function bootHasEncryptedSearchIndex(): void * @param \Illuminate\Database\Eloquent\Model $model * @return void */ - protected static function updateSearchIndex(Model $model): void + public function updateSearchIndex(): void { - if (empty($model->encryptedSearch)) { + $config = $this->getEncryptedSearchConfiguration(); + + if (empty($config)) { return; } $pepper = (string) config('encrypted-search.search_pepper', ''); $max = (int) config('encrypted-search.max_prefix_depth', 6); - // Remove previous entries for this model record - SearchIndex::where('model_type', get_class($model)) - ->where('model_id', $model->getKey()) + SearchIndex::where('model_type', static::class) + ->where('model_id', $this->getKey()) ->delete(); - // Generate new tokens $rows = []; - foreach ($model->encryptedSearch as $field => $modes) { - $raw = (string) $model->getAttribute($field); - if ($raw === '') { - continue; - } + + foreach ($config as $field => $modes) { + $raw = (string) $this->getAttribute($field); + if ($raw === '') continue; $norm = Normalizer::normalize($raw); - if (! $norm) { - continue; - } + if (! $norm) continue; if (! empty($modes['exact'])) { $rows[] = [ - 'model_type' => get_class($model), - 'model_id' => $model->getKey(), + 'model_type' => static::class, + 'model_id' => $this->getKey(), 'field' => $field, 'type' => 'exact', 'token' => Tokens::exact($norm, $pepper), @@ -140,8 +120,8 @@ protected static function updateSearchIndex(Model $model): void if (! empty($modes['prefix'])) { foreach (Tokens::prefixes($norm, $max, $pepper) as $t) { $rows[] = [ - 'model_type' => get_class($model), - 'model_id' => $model->getKey(), + 'model_type' => static::class, + 'model_id' => $this->getKey(), 'field' => $field, 'type' => 'prefix', 'token' => $t, @@ -236,4 +216,18 @@ public function scopeEncryptedPrefix(Builder $query, string $field, string $term ->whereIn('token', $tokens); }); } + + /** + * Get encrypted search configuration from the model. + */ + protected function getEncryptedSearchConfiguration(): array + { + // Laat modellen hun eigen configuratie bepalen + if (method_exists($this, 'getEncryptedSearchFields')) { + return $this->getEncryptedSearchFields(); + } + + // Fallback: gebruik property als die er nog is + return property_exists($this, 'encryptedSearch') ? $this->encryptedSearch : []; + } }