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;
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 */
4433trait 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