Skip to content

Commit 7802ba3

Browse files
Merge pull request #9 from ginkelsoft-development/develop
dev into main
2 parents 0ebf2e7 + f962a47 commit 7802ba3

File tree

5 files changed

+86
-91
lines changed

5 files changed

+86
-91
lines changed

.env.example

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
SEARCH_PEPPER=your-long-random-string-here
2+
ENCRYPTED_SEARCH_DRIVER=elasticsearch
3+
ELASTICSEARCH_HOSTS=https://search.example.com

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,13 @@ The detached index structure scales linearly and supports millions of records ef
173173

174174
## Framework Compatibility
175175

176-
| Laravel Version | PHP Version(s) Supported |
177-
| --------------- | ------------------------ |
178-
| 8.x | 8.0 – 8.1 |
179-
| 9.x | 8.1 – 8.2 |
180-
| 10.x | 8.1 – 8.3 |
181-
| 11.x | 8.2 – 8.3 |
182-
| 12.x | 8.3+ |
176+
| Laravel Version | Supported PHP Versions |
177+
|-----------------|------------------------|
178+
| **8.x** | 8.0 – 8.1 |
179+
| **9.x** | 8.1 – 8.2 |
180+
| **10.x** | 8.1 – 8.3 |
181+
| **11.x** | 8.2 – 8.3 |
182+
| **12.x** | 8.3 and higher |
183183

184184
The package is continuously tested across all supported combinations using GitHub Actions.
185185

src/Contracts/SearchDriver.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
namespace Ginkelsoft\EncryptedSearch\Contracts;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
7+
interface SearchDriver
8+
{
9+
public function index(Model $model): void;
10+
public function delete(Model $model): void;
11+
public function search(string $field, string $term, string $mode = 'exact'): array;
12+
}

src/EncryptedSearchServiceProvider.php

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,38 @@
33
namespace Ginkelsoft\EncryptedSearch;
44

55
use Illuminate\Support\Facades\Event;
6-
use Ginkelsoft\EncryptedSearch\Observers\SearchIndexObserver;
76
use Illuminate\Support\ServiceProvider;
7+
use Ginkelsoft\EncryptedSearch\Observers\SearchIndexObserver;
88

99
/**
1010
* Class EncryptedSearchServiceProvider
1111
*
1212
* Registers and bootstraps the Laravel Encrypted Search Index package.
1313
*
14-
* This service provider integrates the package into a Laravel application by:
15-
* - Registering configuration (`config/encrypted-search.php`)
16-
* - Publishing database migrations for the `encrypted_search_index` table
17-
* - Registering console commands (e.g., index rebuilding)
18-
*
19-
* The provider ensures that the encrypted search system is fully
20-
* self-contained and can be seamlessly installed into any Laravel project.
14+
* Responsibilities:
15+
* - Merge and publish configuration.
16+
* - Publish database migration for the `encrypted_search_index` table.
17+
* - Register console commands for index rebuilding.
18+
* - Attach a global observer that synchronizes encrypted search indexes
19+
* with Eloquent model lifecycle events.
2120
*
2221
* Typical installation:
23-
* ```bash
22+
*
2423
* composer require ginkelsoft/laravel-encrypted-search-index
2524
* php artisan vendor:publish --tag=config
2625
* php artisan vendor:publish --tag=migrations
2726
* php artisan migrate
28-
* ```
29-
*
30-
* After registration, Eloquent models can use the
31-
* `HasEncryptedSearchIndex` trait to automatically build searchable,
32-
* privacy-preserving index entries.
3327
*
34-
* @see \Ginkelsoft\EncryptedSearch\Traits\HasEncryptedSearchIndex
35-
* @see \Ginkelsoft\EncryptedSearch\Console\RebuildIndex
28+
* After installation, any model using the
29+
* {@see \Ginkelsoft\EncryptedSearch\Traits\HasEncryptedSearchIndex}
30+
* trait will automatically maintain a privacy-preserving search index.
3631
*/
3732
class EncryptedSearchServiceProvider extends ServiceProvider
3833
{
3934
/**
4035
* Register bindings and configuration.
4136
*
42-
* This merges the package configuration into the application’s
43-
* global config namespace under the key `encrypted-search`.
37+
* Merges the package configuration into the application's config repository.
4438
*
4539
* @return void
4640
*/
@@ -53,36 +47,32 @@ public function register(): void
5347
}
5448

5549
/**
56-
* Bootstrap package resources (config, migrations, and commands).
57-
*
58-
* This method is executed after all other service providers have
59-
* been registered and is responsible for publishing configuration,
60-
* registering migrations, and exposing console commands.
50+
* Bootstrap the package resources, commands, and event listeners.
6151
*
6252
* @return void
6353
*/
6454
public function boot(): void
6555
{
66-
// Publish configuration file
56+
// Publish configuration
6757
$this->publishes([
6858
__DIR__ . '/../config/encrypted-search.php' => config_path('encrypted-search.php'),
6959
], 'config');
7060

71-
// Publish migration with timestamped filename
61+
// Publish migration file
7262
$timestamp = date('Y_m_d_His');
7363
$this->publishes([
7464
__DIR__ . '/../database/migrations/create_encrypted_search_index_table.php'
7565
=> database_path("migrations/{$timestamp}_create_encrypted_search_index_table.php"),
7666
], 'migrations');
7767

78-
// Register CLI commands only in console context
68+
// Register Artisan commands
7969
if ($this->app->runningInConsole()) {
8070
$this->commands([
8171
Console\RebuildIndex::class,
8272
]);
8373
}
8474

85-
// 🔹 Register the observer for all Eloquent events
75+
// Listen for all Eloquent model events and route them through the observer
8676
Event::listen('eloquent.*: *', SearchIndexObserver::class);
8777
}
8878
}

src/Traits/HasEncryptedSearchIndex.php

Lines changed: 48 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,14 @@
1212
/**
1313
* Trait HasEncryptedSearchIndex
1414
*
15-
* Provides automatic encrypted search indexing for Eloquent models.
15+
* Adds encrypted search indexing capabilities to Eloquent models.
1616
*
17-
* When attached to a model, this trait builds and maintains a companion index
18-
* table (`encrypted_search_index`) that stores deterministic, non-reversible
19-
* search tokens derived from model attributes.
17+
* When applied, the model automatically generates deterministic, non-reversible
18+
* search tokens for configured fields and stores them in the
19+
* `encrypted_search_index` table. These tokens support privacy-preserving
20+
* search queries while keeping plaintext data out of the database.
2021
*
21-
* These tokens enable privacy-preserving search queries (exact or prefix-based)
22-
* without revealing plaintext values in the database.
23-
*
24-
* Example usage:
22+
* Example:
2523
*
2624
* class Client extends Model {
2725
* use HasEncryptedSearchIndex;
@@ -31,47 +29,38 @@
3129
* 'last_names' => ['exact' => true, 'prefix' => true],
3230
* ];
3331
* }
34-
*
35-
* On each save or delete:
36-
* - Tokens are (re)generated and stored in `encrypted_search_index`.
37-
* - Old entries for the record are automatically replaced or removed.
38-
*
39-
* Search queries:
40-
*
41-
* Client::encryptedExact('last_names', 'vermeer')->get();
42-
* Client::encryptedPrefix('first_names', 'wie')->get();
4332
*/
4433
trait HasEncryptedSearchIndex
4534
{
4635
/**
47-
* Boot logic for the trait.
36+
* Boot logic for this trait.
4837
*
49-
* Automatically updates or removes encrypted search tokens whenever
50-
* a model instance is saved, updated, deleted, or restored.
38+
* Automatically rebuilds or removes search index entries on
39+
* create, update, save, delete, restore, and force-delete events.
5140
*
5241
* @return void
5342
*/
5443
public static function bootHasEncryptedSearchIndex(): void
5544
{
56-
// Rebuild index when model is created, updated or saved
45+
// Rebuild index on save-related events
5746
foreach (['created', 'updated', 'saved'] as $event) {
5847
static::$event(function (Model $model) {
5948
$model->updateSearchIndex();
6049
});
6150
}
6251

63-
// Always remove tokens when model is deleted
52+
// Remove index entries when deleted
6453
static::deleted(fn(Model $m) => $m->removeSearchIndex());
6554

66-
// Register forceDeleted/restored events only if the model uses SoftDeletes
55+
// Register SoftDelete-specific hooks only if supported
6756
if (in_array(SoftDeletes::class, class_uses_recursive(static::class), true)) {
6857
static::forceDeleted(fn(Model $m) => $m->removeSearchIndex());
6958
static::restored(fn(Model $m) => $m->updateSearchIndex());
7059
}
7160
}
7261

7362
/**
74-
* Create or refresh all search index entries for this model instance.
63+
* Create or refresh all encrypted search entries for this model instance.
7564
*
7665
* @return void
7766
*/
@@ -86,7 +75,7 @@ public function updateSearchIndex(): void
8675
$pepper = (string) config('encrypted-search.search_pepper', '');
8776
$max = (int) config('encrypted-search.max_prefix_depth', 6);
8877

89-
// Remove existing entries
78+
// Remove existing entries for this model
9079
SearchIndex::where('model_type', static::class)
9180
->where('model_id', $this->getKey())
9281
->delete();
@@ -99,45 +88,47 @@ public function updateSearchIndex(): void
9988
continue;
10089
}
10190

102-
$norm = Normalizer::normalize($raw);
103-
if (! $norm) {
91+
$normalized = Normalizer::normalize($raw);
92+
if (! $normalized) {
10493
continue;
10594
}
10695

96+
// Exact matches
10797
if (! empty($modes['exact'])) {
10898
$rows[] = [
10999
'model_type' => static::class,
110100
'model_id' => $this->getKey(),
111101
'field' => $field,
112102
'type' => 'exact',
113-
'token' => Tokens::exact($norm, $pepper),
103+
'token' => Tokens::exact($normalized, $pepper),
114104
'created_at' => now(),
115105
'updated_at' => now(),
116106
];
117107
}
118108

109+
// Prefix matches
119110
if (! empty($modes['prefix'])) {
120-
foreach (Tokens::prefixes($norm, $max, $pepper) as $t) {
111+
foreach (Tokens::prefixes($normalized, $max, $pepper) as $token) {
121112
$rows[] = [
122113
'model_type' => static::class,
123114
'model_id' => $this->getKey(),
124115
'field' => $field,
125116
'type' => 'prefix',
126-
'token' => $t,
117+
'token' => $token,
127118
'created_at' => now(),
128119
'updated_at' => now(),
129120
];
130121
}
131122
}
132123
}
133124

134-
if ($rows) {
125+
if (! empty($rows)) {
135126
SearchIndex::insert($rows);
136127
}
137128
}
138129

139130
/**
140-
* Remove all index entries for this model instance.
131+
* Delete all encrypted search entries related to this model instance.
141132
*
142133
* @return void
143134
*/
@@ -149,26 +140,23 @@ public function removeSearchIndex(): void
149140
}
150141

151142
/**
152-
* Query scope: find models by exact match on an indexed field.
153-
*
154-
* Example:
155-
* Client::encryptedExact('last_names', 'vermeer')->get();
143+
* Scope: query models by exact encrypted token match.
156144
*
157-
* @param \Illuminate\Database\Eloquent\Builder $query
158-
* @param string $field
159-
* @param string $term
160-
* @return \Illuminate\Database\Eloquent\Builder
145+
* @param Builder $query
146+
* @param string $field
147+
* @param string $term
148+
* @return Builder
161149
*/
162150
public function scopeEncryptedExact(Builder $query, string $field, string $term): Builder
163151
{
164152
$pepper = (string) config('encrypted-search.search_pepper', '');
165-
$norm = Normalizer::normalize($term);
153+
$normalized = Normalizer::normalize($term);
166154

167-
if (! $norm) {
155+
if (! $normalized) {
168156
return $query->whereRaw('1=0');
169157
}
170158

171-
$token = Tokens::exact($norm, $pepper);
159+
$token = Tokens::exact($normalized, $pepper);
172160

173161
return $query->whereIn($this->getQualifiedKeyName(), function ($sub) use ($field, $token) {
174162
$sub->select('model_id')
@@ -181,26 +169,27 @@ public function scopeEncryptedExact(Builder $query, string $field, string $term)
181169
}
182170

183171
/**
184-
* Query scope: find models by prefix match on an indexed field.
185-
*
186-
* Example:
187-
* Client::encryptedPrefix('first_names', 'wi')->get();
172+
* Scope: query models by prefix-based encrypted token match.
188173
*
189-
* @param \Illuminate\Database\Eloquent\Builder $query
190-
* @param string $field
191-
* @param string $term
192-
* @return \Illuminate\Database\Eloquent\Builder
174+
* @param Builder $query
175+
* @param string $field
176+
* @param string $term
177+
* @return Builder
193178
*/
194179
public function scopeEncryptedPrefix(Builder $query, string $field, string $term): Builder
195180
{
196181
$pepper = (string) config('encrypted-search.search_pepper', '');
197-
$norm = Normalizer::normalize($term);
182+
$normalized = Normalizer::normalize($term);
198183

199-
if (! $norm) {
184+
if (! $normalized) {
200185
return $query->whereRaw('1=0');
201186
}
202187

203-
$tokens = Tokens::prefixes($norm, (int) config('encrypted-search.max_prefix_depth', 6), $pepper);
188+
$tokens = Tokens::prefixes(
189+
$normalized,
190+
(int) config('encrypted-search.max_prefix_depth', 6),
191+
$pepper
192+
);
204193

205194
return $query->whereIn($this->getQualifiedKeyName(), function ($sub) use ($field, $tokens) {
206195
$sub->select('model_id')
@@ -213,9 +202,9 @@ public function scopeEncryptedPrefix(Builder $query, string $field, string $term
213202
}
214203

215204
/**
216-
* Resolve the encrypted search configuration for this model.
205+
* Retrieve the encrypted search field configuration.
217206
*
218-
* This allows models to define searchable fields either via a
207+
* Models may define configuration either via a
219208
* `getEncryptedSearchFields()` method or a `$encryptedSearch` property.
220209
*
221210
* @return array<string, array<string,bool>>
@@ -226,6 +215,8 @@ protected function getEncryptedSearchConfiguration(): array
226215
return $this->getEncryptedSearchFields();
227216
}
228217

229-
return property_exists($this, 'encryptedSearch') ? $this->encryptedSearch : [];
218+
return property_exists($this, 'encryptedSearch')
219+
? $this->encryptedSearch
220+
: [];
230221
}
231222
}

0 commit comments

Comments
 (0)