From d562e918c2b9a29a508e84e92a982ea81b77cb09 Mon Sep 17 00:00:00 2001 From: Jeremy Wadhams Date: Tue, 3 Dec 2024 12:39:17 -0600 Subject: [PATCH] fix: Attribute mutators could throw exceptions, that also should be safe --- app/JsonModel.php | 12 ++++++------ tests/Unit/JsonModelTest.php | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/app/JsonModel.php b/app/JsonModel.php index 19df9f7..a7ff62c 100644 --- a/app/JsonModel.php +++ b/app/JsonModel.php @@ -445,13 +445,13 @@ public function safeUpdateRecursive(array $attributes, bool $isRootOfChange = tr $wasSet = isset($this->{$key}); $previousValue = $this->{$key}; // __get will fill null even if it wasn't null - if ($this->{$key} instanceof JsonModel) { - $this->{$key}->safeUpdateRecursive($updatedAttribute, false, $caughtExceptions); - } else { - $this->{$key} = $updatedAttribute; - } - try { + if ($this->{$key} instanceof JsonModel) { + $this->{$key}->safeUpdateRecursive($updatedAttribute, false, $caughtExceptions); + } else { + $this->{$key} = $updatedAttribute; + } + $canSave = $this->preSave(); if (!$canSave) { throw new \DomainException("A saving handler on " . (new FriendlyClassName())($this) . " returned false but provided no reason."); diff --git a/tests/Unit/JsonModelTest.php b/tests/Unit/JsonModelTest.php index 4879557..c88e7c2 100644 --- a/tests/Unit/JsonModelTest.php +++ b/tests/Unit/JsonModelTest.php @@ -881,6 +881,28 @@ public function testSafeUpdateRecursiveIncludesSavingHandlers(): void "A saving handler on Custom Saving Handler returned false but provided no reason." ], array_map(fn ($e) => ($e instanceof JsonSchemaValidationException) ? $e->errorsAsMultilineString() : $e->getMessage(), $caughtExceptions)); } + + public function testSafeUpdateRecursiveIncludesAttributeMutators(): void + { + $model = $this->makeMockModel(); + $jsonModel = new CustomAttributeSetter($model, 'data'); + $caughtExceptions = []; + $jsonModel->safeUpdateRecursive(['shirt' => 'red', 'bestCaptain' => 'Solo'], caughtExceptions: $caughtExceptions); + self::assertCanonicallySame(['shirt' => 'red'], $jsonModel); + self::assertCanonicallySame(["Sorry, Solo is not a valid choice for Best Star Trek Captain."], array_map(fn ($e) => $e->getMessage(), $caughtExceptions)); + + // can mix schema and attribute mutator problems + $caughtExceptions = []; + $jsonModel->safeUpdateRecursive(['shirt' => 'orange', 'bestCaptain' => 'Starbuck', 'tribbles' => 14], caughtExceptions: $caughtExceptions); + self::assertCanonicallySame([ + 'shirt' => 'red', // orange is invalid in the schema, reverted + 'tribbles' => 14 + ], $jsonModel); + self::assertCanonicallySame([ + "The properties must match schema: shirt\nThe data should match one item from enum", + "Sorry, Starbuck is not a valid choice for Best Star Trek Captain." + ], array_map(fn ($e) => ($e instanceof JsonSchemaValidationException) ? $e->errorsAsMultilineString() : $e->getMessage(), $caughtExceptions)); + } } /** @@ -911,6 +933,20 @@ protected static function boot() } } +class CustomAttributeSetter extends JsonModel +{ + public const SCHEMA = '{"properties":{"shirt":{"enum":["red", "gold"]}}}'; + public function setBestCaptainAttribute($value): void + { + if($value === 'Saru' ) { + $this->attributes['bestCaptain'] = $value; + } else { + throw new DomainException("Sorry, {$value} is not a valid choice for Best Star Trek Captain."); + } + } +} + + class DownstreamModel extends JsonModel { use HasJsonModelAttributes;