Skip to content

Commit 8e1ce0a

Browse files
Merge pull request #2 from ginkelsoft-development/develop
-Update README.md
2 parents ac97e09 + f7c195a commit 8e1ce0a

File tree

5 files changed

+150
-29
lines changed

5 files changed

+150
-29
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ This design follows a **defense-in-depth** model: encrypted data stays secure, w
9393

9494
```bash
9595
composer require ginkelsoft/laravel-encrypted-search-index
96-
php artisan vendor:publish --tag=config
96+
php artisan vendor:publish --provider="Ginkelsoft\EncryptedSearch\EncryptedSearchServiceProvider" --tag=config
97+
php artisan vendor:publish --provider="Ginkelsoft\EncryptedSearch\EncryptedSearchServiceProvider" --tag=migrations
9798
php artisan migrate
9899
```
99100

composer.json

Lines changed: 10 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,29 @@
11
{
22
"name": "ginkelsoft/laravel-encrypted-search-index",
3-
"description": "Encrypted and privacy-preserving search indexing for Laravel models.",
3+
"description": "Encrypted and searchable index for Laravel models with deterministic hashing and prefix tokens.",
4+
"keywords": ["laravel", "encryption", "search", "privacy", "gdpr", "secure-index"],
45
"type": "library",
56
"license": "MIT",
6-
"authors": [
7-
{
8-
"name": "Wietse van Ginkel",
9-
"email": "info@ginkelsoft.com"
10-
}
11-
],
12-
"require": {
13-
"php": "^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3",
14-
"illuminate/support": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0",
15-
"illuminate/database": "^8.0 || ^9.0 || ^10.0 || ^11.0 || ^12.0"
16-
},
17-
"require-dev": {
18-
"orchestra/testbench": "^6.0 || ^7.0 || ^8.0 || ^9.0 || ^10.0",
19-
"phpunit/phpunit": "^9.6 || ^10.0 || ^11.0"
20-
},
217
"autoload": {
228
"psr-4": {
239
"Ginkelsoft\\EncryptedSearch\\": "src/"
2410
}
2511
},
26-
"autoload-dev": {
27-
"psr-4": {
28-
"Ginkelsoft\\EncryptedSearch\\Tests\\": "tests/",
29-
"Tests\\": "tests/"
30-
}
31-
},
3212
"extra": {
3313
"laravel": {
3414
"providers": [
3515
"Ginkelsoft\\EncryptedSearch\\EncryptedSearchServiceProvider"
3616
]
3717
}
3818
},
19+
"require": {
20+
"php": "^8.1",
21+
"illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0"
22+
},
23+
"require-dev": {
24+
"orchestra/testbench": "^7.0|^8.0|^9.0",
25+
"phpunit/phpunit": "^10.0|^11.0"
26+
},
3927
"minimum-stability": "stable",
4028
"prefer-stable": true
4129
}

src/EncryptedSearchServiceProvider.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Ginkelsoft\EncryptedSearch;
44

5+
use Illuminate\Support\Facades\Event;
6+
use Ginkelsoft\EncryptedSearch\Observers\SearchIndexObserver;
57
use Illuminate\Support\ServiceProvider;
68

79
/**
@@ -79,5 +81,8 @@ public function boot(): void
7981
Console\RebuildIndex::class,
8082
]);
8183
}
84+
85+
// 🔹 Register the observer for all Eloquent events
86+
Event::listen('eloquent.*: *', SearchIndexObserver::class);
8287
}
8388
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
namespace Ginkelsoft\EncryptedSearch\Observers;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Ginkelsoft\EncryptedSearch\Traits\HasEncryptedSearchIndex;
7+
8+
/**
9+
* Class SearchIndexObserver
10+
*
11+
* A global Eloquent event listener that automatically maintains
12+
* encrypted search indexes for models using the
13+
* {@see HasEncryptedSearchIndex} trait.
14+
*
15+
* This observer listens to all Eloquent model events via the wildcard pattern:
16+
*
17+
* Event::listen('eloquent.*: *', SearchIndexObserver::class);
18+
*
19+
* It reacts to model lifecycle events such as created, updated, saved,
20+
* touched, restored, deleted, and forceDeleted.
21+
*
22+
* When a model using the trait is created, updated, or touched, the
23+
* observer rebuilds its associated search tokens. When a model is
24+
* deleted or force-deleted, the corresponding index entries are removed.
25+
*
26+
* @package Ginkelsoft\EncryptedSearch\Observers
27+
*/
28+
class SearchIndexObserver
29+
{
30+
/**
31+
* Handles all Eloquent events emitted through the wildcard listener.
32+
*
33+
* @param string $event The Eloquent event name, e.g. "eloquent.saved: App\Models\Client".
34+
* @param array $payload The event payload — typically contains the Model instance at index 0.
35+
* @return void
36+
*/
37+
public function handle(string $event, array $payload): void
38+
{
39+
if (empty($payload[0]) || ! $payload[0] instanceof Model) {
40+
return;
41+
}
42+
43+
/** @var Model $model */
44+
$model = $payload[0];
45+
46+
// Only process models that use the HasEncryptedSearchIndex trait
47+
if (! $this->usesTrait($model, HasEncryptedSearchIndex::class)) {
48+
return;
49+
}
50+
51+
$eventLower = strtolower($event);
52+
53+
// Handle deletion events (deleted or forceDeleted)
54+
if (str_contains($eventLower, 'deleted')) {
55+
$this->removeIndex($model);
56+
return;
57+
}
58+
59+
// Handle write and restore events that require index rebuilding
60+
if (
61+
str_contains($eventLower, 'saved') ||
62+
str_contains($eventLower, 'updated') ||
63+
str_contains($eventLower, 'created') ||
64+
str_contains($eventLower, 'touched') ||
65+
str_contains($eventLower, 'restored')
66+
) {
67+
$this->rebuildIndex($model);
68+
}
69+
}
70+
71+
/**
72+
* Determines whether the given model uses a specific trait.
73+
*
74+
* @param Model $model The model instance to inspect.
75+
* @param string $traitFqcn The fully-qualified trait class name to check.
76+
* @return bool
77+
*/
78+
protected function usesTrait(Model $model, string $traitFqcn): bool
79+
{
80+
$uses = class_uses_recursive($model);
81+
return in_array($traitFqcn, $uses, true);
82+
}
83+
84+
/**
85+
* Rebuilds the search index for the given model.
86+
*
87+
* If the model defines the static method `updateSearchIndex`,
88+
* it will be called directly. This method is typically defined
89+
* in the {@see HasEncryptedSearchIndex} trait.
90+
*
91+
* @param Model $model The model instance to reindex.
92+
* @return void
93+
*/
94+
protected function rebuildIndex(Model $model): void
95+
{
96+
if (method_exists($model, 'updateSearchIndex')) {
97+
// @phpstan-ignore-next-line
98+
$model::updateSearchIndex($model);
99+
}
100+
}
101+
102+
/**
103+
* Removes all index entries for the given model.
104+
*
105+
* If the model defines the static method `removeSearchIndex`,
106+
* it will be invoked to clear existing tokens associated with
107+
* the model’s primary key.
108+
*
109+
* @param Model $model The model instance to remove from the index.
110+
* @return void
111+
*/
112+
protected function removeIndex(Model $model): void
113+
{
114+
if (method_exists($model, 'removeSearchIndex')) {
115+
// @phpstan-ignore-next-line
116+
$model::removeSearchIndex($model);
117+
}
118+
}
119+
}

src/Traits/HasEncryptedSearchIndex.php

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,21 @@ trait HasEncryptedSearchIndex
7575
*/
7676
public static function bootHasEncryptedSearchIndex(): void
7777
{
78-
static::saved(function (Model $model): void {
79-
static::updateSearchIndex($model);
80-
});
78+
// Rebuild index when model is created, updated or saved.
79+
foreach (['created', 'updated', 'saved'] as $event) {
80+
static::$event(function (Model $model) {
81+
static::updateSearchIndex($model);
82+
});
83+
}
8184

82-
static::deleted(function (Model $model): void {
83-
static::removeSearchIndex($model);
84-
});
85+
// Remove tokens when model is deleted or force-deleted
86+
static::deleted(fn(Model $m) => static::removeSearchIndex($m));
87+
static::forceDeleted(fn(Model $m) => static::removeSearchIndex($m));
88+
89+
// Optional: if SoftDeletes is used, re-index on restore
90+
if (method_exists(static::class, 'restored')) {
91+
static::restored(fn(Model $m) => static::updateSearchIndex($m));
92+
}
8593
}
8694

8795
/**

0 commit comments

Comments
 (0)