From 88547aeb1ea9637ec8cc6b6f8b3a36a5dcf0e623 Mon Sep 17 00:00:00 2001 From: eugenstranz Date: Thu, 28 Mar 2024 11:38:37 +0800 Subject: [PATCH 1/3] Feature: add ability to store eloquent casts as an encrypted string --- .../EloquentCasts/DataCollectionEloquentCast.php | 13 ++++++++++++- src/Support/EloquentCasts/DataEloquentCast.php | 13 ++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Support/EloquentCasts/DataCollectionEloquentCast.php b/src/Support/EloquentCasts/DataCollectionEloquentCast.php index 4a0af6489..a6ebe7c29 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; @@ -21,6 +22,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 = '[]'; } @@ -66,6 +71,12 @@ 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; } } diff --git a/src/Support/EloquentCasts/DataEloquentCast.php b/src/Support/EloquentCasts/DataEloquentCast.php index bc14fb5fc..e0e170d4e 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 From 34ec24778df24786d85d7b312ecdef3e43e4083d Mon Sep 17 00:00:00 2001 From: eugenstranz Date: Mon, 3 Jun 2024 19:07:46 +0800 Subject: [PATCH 2/3] provide tests and documentation for encrypted data objects --- docs/advanced-usage/eloquent-casting.md | 28 +++++++++++++++ .../Models/DummyModelWithEncryptedCasts.php | 20 +++++++++++ .../EloquentCasts/DataEloquentCastTest.php | 36 +++++++++++++++++++ 3 files changed, 84 insertions(+) create mode 100644 tests/Fakes/Models/DummyModelWithEncryptedCasts.php diff --git a/docs/advanced-usage/eloquent-casting.md b/docs/advanced-usage/eloquent-casting.md index 328573302..47138e078 100644 --- a/docs/advanced-usage/eloquent-casting.md +++ b/docs/advanced-usage/eloquent-casting.md @@ -115,6 +115,19 @@ The child data object value of the model will be stored in the database as a JSO When retrieving the model, the data object will be instantiated based on the `type` key in the JSON string. +#### Abstract data object with collection + +You can use with collection. + +```php +class Record extends Model +{ + protected $casts = [ + 'configs' => DataCollection::class . ':' . RecordConfig::class, + ]; +} +``` + #### Abstract data class morphs By default, the `type` key in the JSON string will be the fully qualified class name of the child data object. This can break your application quite easily when you refactor your code. To prevent this, you can add a morph map like with [Eloquent models](https://laravel.com/docs/eloquent-relationships#polymorphic-relationships). Within your `AppServiceProvivder` you can add the following mapping: @@ -216,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/tests/Fakes/Models/DummyModelWithEncryptedCasts.php b/tests/Fakes/Models/DummyModelWithEncryptedCasts.php new file mode 100644 index 000000000..e22b37aff --- /dev/null +++ b/tests/Fakes/Models/DummyModelWithEncryptedCasts.php @@ -0,0 +1,20 @@ + 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 c28d4e2d5..232be5574 100644 --- a/tests/Support/EloquentCasts/DataEloquentCastTest.php +++ b/tests/Support/EloquentCasts/DataEloquentCastTest.php @@ -1,14 +1,18 @@ 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')); +}); From de5d11bf6599fb220f89abb9c4e52ef65a3f2087 Mon Sep 17 00:00:00 2001 From: eugenstranz Date: Mon, 3 Jun 2024 19:11:55 +0800 Subject: [PATCH 3/3] refactor --- tests/Support/EloquentCasts/DataEloquentCastTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Support/EloquentCasts/DataEloquentCastTest.php b/tests/Support/EloquentCasts/DataEloquentCastTest.php index 232be5574..72215fd2a 100644 --- a/tests/Support/EloquentCasts/DataEloquentCastTest.php +++ b/tests/Support/EloquentCasts/DataEloquentCastTest.php @@ -7,7 +7,6 @@ use function Pest\Laravel\assertDatabaseHas; use Spatie\LaravelData\Support\DataConfig; - use Spatie\LaravelData\Tests\Fakes\AbstractData\AbstractDataA; use Spatie\LaravelData\Tests\Fakes\AbstractData\AbstractDataB; use Spatie\LaravelData\Tests\Fakes\Models\DummyModelWithCasts;