Skip to content

Commit 6cfa4db

Browse files
authored
Merge pull request #6 from carsdotcom/issue-5
feat: Add a way for Cast classes to express when an attribute should be unset (removed)
2 parents 8a7dac4 + 7a99a71 commit 6cfa4db

File tree

6 files changed

+168
-0
lines changed

6 files changed

+168
-0
lines changed

app/Casts/CastFloatOrUnset.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Carsdotcom\LaravelJsonModel\Casts;
4+
5+
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
6+
7+
class CastFloatOrUnset implements ShouldUnset, CastsInboundAttributes
8+
{
9+
public static function shouldUnset(mixed $value): bool
10+
{
11+
return ($value === null);
12+
}
13+
14+
public function set($model, string $key, $value, array $attributes)
15+
{
16+
if ($this->shouldUnset($value)) {
17+
return null;
18+
}
19+
return (float)$value;
20+
}
21+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Carsdotcom\LaravelJsonModel\Casts;
4+
5+
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
6+
7+
class CastNonEmptyStringOrUnset implements ShouldUnset, CastsInboundAttributes
8+
{
9+
public static function shouldUnset(mixed $value): bool
10+
{
11+
return ($value === null || $value === '');
12+
}
13+
14+
public function set($model, string $key, $value, array $attributes)
15+
{
16+
if ($this->shouldUnset($value)) {
17+
return null;
18+
}
19+
return (string)$value;
20+
}
21+
}

app/Casts/CastStringOrUnset.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Carsdotcom\LaravelJsonModel\Casts;
4+
5+
use Illuminate\Contracts\Database\Eloquent\CastsInboundAttributes;
6+
7+
class CastStringOrUnset implements ShouldUnset, CastsInboundAttributes
8+
{
9+
public static function shouldUnset(mixed $value): bool
10+
{
11+
return ($value === null);
12+
}
13+
14+
public function set($model, string $key, $value, array $attributes)
15+
{
16+
if ($this->shouldUnset($value)) {
17+
return null;
18+
}
19+
return (string)$value;
20+
}
21+
}

app/Casts/ShouldUnset.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?php
2+
3+
namespace Carsdotcom\LaravelJsonModel\Casts;
4+
5+
interface ShouldUnset
6+
{
7+
static function shouldUnset(mixed $value): bool;
8+
}

app/JsonModel.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Carsdotcom\JsonSchemaValidation\Exceptions\JsonSchemaValidationException;
2222
use Carsdotcom\JsonSchemaValidation\Helpers\FriendlyClassName;
2323
use Carsdotcom\JsonSchemaValidation\Traits\ValidatesWithJsonSchema;
24+
use Carsdotcom\LaravelJsonModel\Casts\ShouldUnset;
2425
use Carsdotcom\LaravelJsonModel\Contracts\CanCascadeEvents;
2526
use Carsdotcom\JsonSchemaValidation\Contracts\CanValidate;
2627
use Carsdotcom\LaravelJsonModel\Helpers\ClassUsesTrait;
@@ -693,4 +694,24 @@ public static function preventsAccessingMissingAttributes()
693694
{
694695
return static::$modelsShouldPreventAccessingMissingAttributes;
695696
}
697+
698+
/**
699+
* Extends setAttribute to cooperate with casts that implement ShouldUnset.
700+
* If the cast wishes, we'll *remove* the attribute from the Json representation.
701+
* This lets us implement things like PATCH {"zip":null} to mean "remove ZIP, return to default behavior"
702+
*/
703+
use HasAttributes {
704+
setAttribute as protected eloquentSetAttribute;
705+
}
706+
707+
public function setAttribute($key, $value) {
708+
$castType = $this->hasCast($key) ? $this->getCastType($key) : null;
709+
if (is_a($castType, ShouldUnset::class, true) && $castType::shouldUnset($value)) {
710+
unset($this->attributes[$key]);
711+
return $this;
712+
}
713+
714+
$this->eloquentSetAttribute($key, $value);
715+
return $this;
716+
}
696717
}

tests/Unit/JsonModelTest.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
namespace Tests\Unit;
1010

1111
use Carsdotcom\JsonSchemaValidation\Exceptions\JsonSchemaValidationException;
12+
use Carsdotcom\LaravelJsonModel\Casts\CastFloatOrUnset;
13+
use Carsdotcom\LaravelJsonModel\Casts\CastNonEmptyStringOrUnset;
14+
use Carsdotcom\LaravelJsonModel\Casts\CastStringOrUnset;
1215
use Carsdotcom\LaravelJsonModel\CollectionOfJsonModels;
1316
use Carsdotcom\JsonSchemaValidation\SchemaValidator as SchemaValidator;
1417
use Carsdotcom\LaravelJsonModel\JsonModel;
@@ -903,6 +906,79 @@ public function testSafeUpdateRecursiveIncludesAttributeMutators(): void
903906
"Sorry, Starbuck is not a valid choice for Best Star Trek Captain."
904907
], array_map(fn ($e) => ($e instanceof JsonSchemaValidationException) ? $e->errorsAsMultilineString() : $e->getMessage(), $caughtExceptions));
905908
}
909+
910+
public function testCastFloatOrUnset(): void
911+
{
912+
$jsonModel = new class extends JsonModel{
913+
protected $casts = [
914+
'fou' => CastFloatOrUnset::class
915+
];
916+
};
917+
918+
self::assertFalse(isset($jsonModel->fou), "Initializes unset");
919+
$jsonModel->fou = null;
920+
self::assertFalse(isset($jsonModel->fou), "Stays unset on initialize to null");
921+
$jsonModel->fou = 6.9;
922+
self::assertSame(6.9, $jsonModel->fou, "Float is unchanged");
923+
$jsonModel->fou = '6';
924+
self::assertSame(6.0, $jsonModel->fou, "String is cast");
925+
$jsonModel->fou = 9;
926+
self::assertSame(9.0, $jsonModel->fou, "int is cast");
927+
self::assertStringContainsString('"fou":9', $jsonModel->toJson());
928+
$jsonModel->fou = null;
929+
self::assertFalse(isset($jsonModel->fou), "Set to null unsets attribute");
930+
self::assertStringNotContainsString('fou', $jsonModel->toJson());
931+
}
932+
933+
public function testCastStringOrUnset(): void
934+
{
935+
$jsonModel = new class extends JsonModel{
936+
protected $casts = [
937+
'sou' => CastStringOrUnset::class
938+
];
939+
};
940+
941+
self::assertFalse(isset($jsonModel->sou), "Initializes unset");
942+
$jsonModel->sou = null;
943+
self::assertFalse(isset($jsonModel->sou), "Stays unset on initialize to null");
944+
$jsonModel->sou = 'happy happy';
945+
self::assertSame('happy happy', $jsonModel->sou, "String is unchanged");
946+
$jsonModel->sou = 6;
947+
self::assertSame('6', $jsonModel->sou, "int is cast");
948+
$jsonModel->sou = false;
949+
self::assertSame('', $jsonModel->sou, "bool is cast");
950+
self::assertStringContainsString('"sou":""', $jsonModel->toJson());
951+
$jsonModel->sou = null;
952+
self::assertFalse(isset($jsonModel->sou), "Set to null unsets attribute");
953+
self::assertStringNotContainsString('sou', $jsonModel->toJson());
954+
}
955+
956+
public function testCastNonEmptyStringOrUnset(): void
957+
{
958+
$jsonModel = new class extends JsonModel{
959+
protected $casts = [
960+
'sou' => CastNonEmptyStringOrUnset::class
961+
];
962+
};
963+
964+
self::assertFalse(isset($jsonModel->sou), "Initializes unset");
965+
$jsonModel->sou = null;
966+
self::assertFalse(isset($jsonModel->sou), "Stays unset on initialize to null");
967+
$jsonModel->sou = 'happy happy';
968+
self::assertSame('happy happy', $jsonModel->sou, "String is unchanged");
969+
970+
$jsonModel->sou = '';
971+
self::assertFalse(isset($jsonModel->sou), "Set to empty string unsets attribute");
972+
self::assertStringNotContainsString('sou', $jsonModel->toJson());
973+
974+
$jsonModel->sou = 6;
975+
self::assertSame('6', $jsonModel->sou, "int is cast");
976+
self::assertStringContainsString('"sou":"6"', $jsonModel->toJson());
977+
978+
$jsonModel->sou = null;
979+
self::assertFalse(isset($jsonModel->sou), "Set to null unsets attribute");
980+
self::assertStringNotContainsString('sou', $jsonModel->toJson());
981+
}
906982
}
907983

908984
/**

0 commit comments

Comments
 (0)