diff --git a/docs/advanced-usage/eloquent-casting.md b/docs/advanced-usage/eloquent-casting.md index ba252d87..47138e07 100644 --- a/docs/advanced-usage/eloquent-casting.md +++ b/docs/advanced-usage/eloquent-casting.md @@ -229,3 +229,18 @@ $artist = Artist::create([ $artist->songs; // DataCollection $artist->songs->count();// 0 ``` + +## Using encryption with data objects and collections + +Similar to Laravel's native encrypted casts, you can also encrypt data objects and collections. + +When retrieving the model, the data object will be decrypted automatically. + +```php +class Artist extends Model +{ + protected $casts = [ + 'songs' => DataCollection::class.':'.SongData::class.',encrypted', + ]; +} +``` diff --git a/src/Support/EloquentCasts/DataCollectionEloquentCast.php b/src/Support/EloquentCasts/DataCollectionEloquentCast.php index 16c81ca3..e323aaa6 100644 --- a/src/Support/EloquentCasts/DataCollectionEloquentCast.php +++ b/src/Support/EloquentCasts/DataCollectionEloquentCast.php @@ -4,6 +4,7 @@ use Illuminate\Contracts\Database\Eloquent\CastsAttributes; use Illuminate\Contracts\Support\Arrayable; +use Illuminate\Support\Facades\Crypt; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\BaseDataCollectable; use Spatie\LaravelData\Contracts\TransformableData; @@ -25,6 +26,10 @@ public function __construct( public function get($model, string $key, $value, array $attributes): ?DataCollection { + if (is_string($value) && in_array('encrypted', $this->arguments)) { + $value = Crypt::decryptString($value); + } + if ($value === null && in_array('default', $this->arguments)) { $value = '[]'; } @@ -91,7 +96,13 @@ public function set($model, string $key, $value, array $attributes): ?string $dataCollection = new ($this->dataCollectionClass)($this->dataClass, $data); - return $dataCollection->toJson(); + $dataCollection = $dataCollection->toJson(); + + if (in_array('encrypted', $this->arguments)) { + return Crypt::encryptString($dataCollection); + } + + return $dataCollection; } protected function isAbstractClassCast(): bool diff --git a/src/Support/EloquentCasts/DataEloquentCast.php b/src/Support/EloquentCasts/DataEloquentCast.php index bc14fb5f..e0e170d4 100644 --- a/src/Support/EloquentCasts/DataEloquentCast.php +++ b/src/Support/EloquentCasts/DataEloquentCast.php @@ -3,6 +3,7 @@ namespace Spatie\LaravelData\Support\EloquentCasts; use Illuminate\Contracts\Database\Eloquent\CastsAttributes; +use Illuminate\Support\Facades\Crypt; use Spatie\LaravelData\Contracts\BaseData; use Spatie\LaravelData\Contracts\TransformableData; use Spatie\LaravelData\Exceptions\CannotCastData; @@ -23,6 +24,10 @@ public function __construct( public function get($model, string $key, $value, array $attributes): ?BaseData { + if (is_string($value) && in_array('encrypted', $this->arguments)) { + $value = Crypt::decryptString($value); + } + if (is_null($value) && in_array('default', $this->arguments)) { $value = '{}'; } @@ -70,7 +75,13 @@ public function set($model, string $key, $value, array $attributes): ?string ]); } - return $value->toJson(); + $value = $value->toJson(); + + if (in_array('encrypted', $this->arguments)) { + return Crypt::encryptString($value); + } + + return $value; } protected function isAbstractClassCast(): bool diff --git a/tests/Fakes/Models/DummyModelWithEncryptedCasts.php b/tests/Fakes/Models/DummyModelWithEncryptedCasts.php new file mode 100644 index 00000000..668afa10 --- /dev/null +++ b/tests/Fakes/Models/DummyModelWithEncryptedCasts.php @@ -0,0 +1,19 @@ + SimpleData::class.':encrypted', + 'data_collection' => SimpleDataCollection::class.':'.SimpleData::class.',encrypted', + ]; + + protected $table = 'dummy_model_with_casts'; + + public $timestamps = false; +} diff --git a/tests/Support/EloquentCasts/DataEloquentCastTest.php b/tests/Support/EloquentCasts/DataEloquentCastTest.php index c28d4e2d..72215fd2 100644 --- a/tests/Support/EloquentCasts/DataEloquentCastTest.php +++ b/tests/Support/EloquentCasts/DataEloquentCastTest.php @@ -1,5 +1,7 @@ toBeInstanceOf(AbstractDataA::class) ->a->toBe('A\A'); }); + +it('can save an encrypted data object', function () { + // Save the encrypted data to the database + DummyModelWithEncryptedCasts::create([ + 'data' => new SimpleData('Test'), + ]); + + // Retrieve the model from the database without Eloquent casts + $model = DB::table('dummy_model_with_casts') + ->first(); + + try { + Crypt::decryptString($model->data); + $isEncrypted = true; + } catch (DecryptException $e) { + $isEncrypted = false; + } + + expect($isEncrypted)->toBeTrue(); +}); + +it('can load an encrypted data object', function () { + // Save the encrypted data to the database + DummyModelWithEncryptedCasts::create([ + 'data' => new SimpleData('Test'), + ]); + + /** @var \Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCasts $model */ + $model = DummyModelWithEncryptedCasts::first(); + + expect($model->data)->toEqual(new SimpleData('Test')); +});