Skip to content

Commit 8a7dac4

Browse files
authored
Merge pull request #14 from carsdotcom/safe-attribute-mutator
fix: Attribute mutators could throw exceptions, that also should be safe
2 parents 78d9eb6 + d562e91 commit 8a7dac4

File tree

2 files changed

+42
-6
lines changed

2 files changed

+42
-6
lines changed

app/JsonModel.php

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -445,13 +445,13 @@ public function safeUpdateRecursive(array $attributes, bool $isRootOfChange = tr
445445
$wasSet = isset($this->{$key});
446446
$previousValue = $this->{$key}; // __get will fill null even if it wasn't null
447447

448-
if ($this->{$key} instanceof JsonModel) {
449-
$this->{$key}->safeUpdateRecursive($updatedAttribute, false, $caughtExceptions);
450-
} else {
451-
$this->{$key} = $updatedAttribute;
452-
}
453-
454448
try {
449+
if ($this->{$key} instanceof JsonModel) {
450+
$this->{$key}->safeUpdateRecursive($updatedAttribute, false, $caughtExceptions);
451+
} else {
452+
$this->{$key} = $updatedAttribute;
453+
}
454+
455455
$canSave = $this->preSave();
456456
if (!$canSave) {
457457
throw new \DomainException("A saving handler on " . (new FriendlyClassName())($this) . " returned false but provided no reason.");

tests/Unit/JsonModelTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -881,6 +881,28 @@ public function testSafeUpdateRecursiveIncludesSavingHandlers(): void
881881
"A saving handler on Custom Saving Handler returned false but provided no reason."
882882
], array_map(fn ($e) => ($e instanceof JsonSchemaValidationException) ? $e->errorsAsMultilineString() : $e->getMessage(), $caughtExceptions));
883883
}
884+
885+
public function testSafeUpdateRecursiveIncludesAttributeMutators(): void
886+
{
887+
$model = $this->makeMockModel();
888+
$jsonModel = new CustomAttributeSetter($model, 'data');
889+
$caughtExceptions = [];
890+
$jsonModel->safeUpdateRecursive(['shirt' => 'red', 'bestCaptain' => 'Solo'], caughtExceptions: $caughtExceptions);
891+
self::assertCanonicallySame(['shirt' => 'red'], $jsonModel);
892+
self::assertCanonicallySame(["Sorry, Solo is not a valid choice for Best Star Trek Captain."], array_map(fn ($e) => $e->getMessage(), $caughtExceptions));
893+
894+
// can mix schema and attribute mutator problems
895+
$caughtExceptions = [];
896+
$jsonModel->safeUpdateRecursive(['shirt' => 'orange', 'bestCaptain' => 'Starbuck', 'tribbles' => 14], caughtExceptions: $caughtExceptions);
897+
self::assertCanonicallySame([
898+
'shirt' => 'red', // orange is invalid in the schema, reverted
899+
'tribbles' => 14
900+
], $jsonModel);
901+
self::assertCanonicallySame([
902+
"The properties must match schema: shirt\nThe data should match one item from enum",
903+
"Sorry, Starbuck is not a valid choice for Best Star Trek Captain."
904+
], array_map(fn ($e) => ($e instanceof JsonSchemaValidationException) ? $e->errorsAsMultilineString() : $e->getMessage(), $caughtExceptions));
905+
}
884906
}
885907

886908
/**
@@ -911,6 +933,20 @@ protected static function boot()
911933
}
912934
}
913935

936+
class CustomAttributeSetter extends JsonModel
937+
{
938+
public const SCHEMA = '{"properties":{"shirt":{"enum":["red", "gold"]}}}';
939+
public function setBestCaptainAttribute($value): void
940+
{
941+
if($value === 'Saru' ) {
942+
$this->attributes['bestCaptain'] = $value;
943+
} else {
944+
throw new DomainException("Sorry, {$value} is not a valid choice for Best Star Trek Captain.");
945+
}
946+
}
947+
}
948+
949+
914950
class DownstreamModel extends JsonModel
915951
{
916952
use HasJsonModelAttributes;

0 commit comments

Comments
 (0)