From 4a8031266f068cebba7dfec9c4777faf19e9efba Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Sat, 6 Apr 2024 20:46:01 +0800 Subject: [PATCH 1/5] Don't break with `Model::shouldBeStrict()`` enabled --- .gitignore | 1 + src/VirtualColumn.php | 9 ++++++++- tests/VirtualColumnTest.php | 14 ++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7b37592..998f123 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ composer.lock .phpunit.result.cache .php-cs-fixer.cache +.idea/ \ No newline at end of file diff --git a/src/VirtualColumn.php b/src/VirtualColumn.php index e3cfec6..bbe2d7d 100644 --- a/src/VirtualColumn.php +++ b/src/VirtualColumn.php @@ -5,6 +5,7 @@ namespace Stancl\VirtualColumn; use Illuminate\Contracts\Encryption\DecryptException; +use Illuminate\Database\Eloquent\MissingAttributeException; use Illuminate\Support\Facades\Crypt; /** @@ -42,7 +43,13 @@ protected function decodeVirtualColumn(): void ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object'], // Default encrypted castables ); - foreach ($this->getAttribute(static::getDataColumn()) ?? [] as $key => $value) { + try { + $data = $this->getAttribute(static::getDataColumn()) ?? []; + } catch (MissingAttributeException) { + return; + } + + foreach ($data as $key => $value) { $attributeHasEncryptedCastable = in_array(data_get($this->getCasts(), $key), $encryptedCastables); if ($attributeHasEncryptedCastable && $this->valueEncrypted($value)) { diff --git a/tests/VirtualColumnTest.php b/tests/VirtualColumnTest.php index bb33458..9290b34 100644 --- a/tests/VirtualColumnTest.php +++ b/tests/VirtualColumnTest.php @@ -128,6 +128,20 @@ public function models_extending_a_parent_model_using_virtualcolumn_get_encoded_ $this->assertSame($encodedBar->bar, 'bar'); } + /** @test */ + public function decoding_works_with_strict_mode_enabled() { + FooModel::shouldBeStrict(); + + FooModel::create([ + 'id' => 1, + 'foo' => 'bar' + ]); + + $id = FooModel::query()->pluck('id')->first(); + + $this->assertSame(1, $id); + } + // maybe add an explicit test that the saving() and updating() listeners don't run twice? /** @test */ From 2d61396e541587ce3ce1229b0c29070ee428a153 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Samuel=20=C5=A0tancl?= Date: Sat, 6 Apr 2024 17:49:13 +0200 Subject: [PATCH 2/5] add missing EOL --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 998f123..61721f3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ composer.lock .phpunit.result.cache .php-cs-fixer.cache -.idea/ \ No newline at end of file +.idea/ From 09bdf523e0169d0355a48996950a5561cbd06276 Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 9 Apr 2024 06:38:08 +0800 Subject: [PATCH 3/5] Early return in the listener if data column isn't required --- src/VirtualColumn.php | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/VirtualColumn.php b/src/VirtualColumn.php index bbe2d7d..ba3a78b 100644 --- a/src/VirtualColumn.php +++ b/src/VirtualColumn.php @@ -43,13 +43,7 @@ protected function decodeVirtualColumn(): void ['encrypted', 'encrypted:array', 'encrypted:collection', 'encrypted:json', 'encrypted:object'], // Default encrypted castables ); - try { - $data = $this->getAttribute(static::getDataColumn()) ?? []; - } catch (MissingAttributeException) { - return; - } - - foreach ($data as $key => $value) { + foreach ($this->getAttribute(static::getDataColumn()) ?? [] as $key => $value) { $attributeHasEncryptedCastable = in_array(data_get($this->getCasts(), $key), $encryptedCastables); if ($attributeHasEncryptedCastable && $this->valueEncrypted($value)) { @@ -114,6 +108,10 @@ protected function getAfterListeners(): array return [ 'retrieved' => [ function () { + if (($this->attributes[static::getDataColumn()] ?? null) === null) { + return; + } + // Always decode after model retrieval $this->dataEncoded = true; From 9cda7d498cb186a7f920fae352ac5133ab8fd085 Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 9 Apr 2024 06:38:08 +0800 Subject: [PATCH 4/5] Early return in the listener if data column isn't required --- src/VirtualColumn.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/VirtualColumn.php b/src/VirtualColumn.php index ba3a78b..a829973 100644 --- a/src/VirtualColumn.php +++ b/src/VirtualColumn.php @@ -5,7 +5,6 @@ namespace Stancl\VirtualColumn; use Illuminate\Contracts\Encryption\DecryptException; -use Illuminate\Database\Eloquent\MissingAttributeException; use Illuminate\Support\Facades\Crypt; /** From 5426e41a5c46de03dcf7475473556177998e9564 Mon Sep 17 00:00:00 2001 From: Nick Potts Date: Tue, 9 Apr 2024 07:18:43 +0800 Subject: [PATCH 5/5] Throw exception if a user is trying to overwrite the data without first loading it --- src/VirtualColumn.php | 13 +++++++++++-- tests/VirtualColumnTest.php | 14 ++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/VirtualColumn.php b/src/VirtualColumn.php index a829973..4dfaa2b 100644 --- a/src/VirtualColumn.php +++ b/src/VirtualColumn.php @@ -31,9 +31,11 @@ trait VirtualColumn */ public bool $dataEncoded = false; + public bool $dataNotLoaded = false; + protected function decodeVirtualColumn(): void { - if (! $this->dataEncoded) { + if (! $this->dataEncoded || ! $this->dataNotLoaded) { return; } @@ -65,6 +67,10 @@ protected function encodeAttributes(): void return; } + if ($this->dataNotLoaded) { + throw new \Exception('Data column was not loaded from the database. Make sure the data column is selected in the query.'); + } + $dataColumn = static::getDataColumn(); $customColumns = static::getCustomColumns(); $attributes = array_filter($this->getAttributes(), fn ($key) => ! in_array($key, $customColumns), ARRAY_FILTER_USE_KEY); @@ -107,11 +113,14 @@ protected function getAfterListeners(): array return [ 'retrieved' => [ function () { + // If the data column wasn't retrieved, don't decode it if (($this->attributes[static::getDataColumn()] ?? null) === null) { + $this->dataNotLoaded = true; + return; } - // Always decode after model retrieval + // Mark the data as encoded so that it doesn't get encoded again $this->dataEncoded = true; $this->decodeVirtualColumn(); diff --git a/tests/VirtualColumnTest.php b/tests/VirtualColumnTest.php index 9290b34..9089a4b 100644 --- a/tests/VirtualColumnTest.php +++ b/tests/VirtualColumnTest.php @@ -128,6 +128,20 @@ public function models_extending_a_parent_model_using_virtualcolumn_get_encoded_ $this->assertSame($encodedBar->bar, 'bar'); } + /** @test */ + public function model_doesnt_overwrite_when_selectively_fetching() { + $this->expectExceptionMessage('Data column was not loaded from the database. Make sure the data column is selected in the query.'); + + FooModel::create([ + 'id' => 1, + 'foo' => 'bar' + ]); + + $foo = FooModel::query()->first(['id']); + $foo->bar = 'baz'; + $foo->save(); + } + /** @test */ public function decoding_works_with_strict_mode_enabled() { FooModel::shouldBeStrict();