From ed628a6d829c04fd74eec068d5c45b6e94471efd Mon Sep 17 00:00:00 2001 From: Jesper Noordsij Date: Wed, 14 Jul 2021 14:35:43 +0200 Subject: [PATCH 1/5] Pass null to custom cast set method when value is null --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index e70378c6b078..363dd2461b57 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -839,7 +839,7 @@ protected function setClassCastableAttribute($key, $value) function () { }, $this->normalizeCastClassResponse($key, $caster->set( - $this, $key, $this->{$key}, $this->attributes + $this, $key, $value, $this->attributes )) )); } else { From 41685e7058361454a99487de4d7617fd0403f458 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij Date: Fri, 16 Jul 2021 15:16:48 +0200 Subject: [PATCH 2/5] Add integration test for custom casts on Eloquent Model --- .../EloquentModelCustomCastingTest.php | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 tests/Integration/Database/EloquentModelCustomCastingTest.php diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php new file mode 100644 index 000000000000..0494bd54f023 --- /dev/null +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -0,0 +1,222 @@ +addConnection([ + 'driver' => 'sqlite', + 'database' => ':memory:', + ]); + + $db->bootEloquent(); + $db->setAsGlobal(); + + $this->createSchema(); + } + + /** + * Setup the database schema. + * + * @return void + */ + public function createSchema() + { + $this->schema()->create('casting_table', function (Blueprint $table) { + $table->increments('id'); + $table->string('address_line_one'); + $table->string('address_line_two'); + $table->timestamps(); + }); + } + + /** + * Tear down the database schema. + * + * @return void + */ + protected function tearDown(): void + { + $this->schema()->drop('casting_table'); + } + + /** + * Tests... + */ + public function testSavingCastedAttributesToDatabase() + { + /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + ]); + + $this->assertSame('address_line_one_value', $model->getOriginal('address_line_one')); + $this->assertSame('address_line_one_value', $model->getAttribute('address_line_one')); + + $this->assertSame('address_line_two_value', $model->getOriginal('address_line_two')); + $this->assertSame('address_line_two_value', $model->getAttribute('address_line_two')); + + /** @var \Illuminate\Tests\Integration\Database\CustomCasts $another_model */ + $another_model = CustomCasts::create([ + 'address_line_one' => 'address_line_one_value', + 'address_line_two' => 'address_line_two_value', + ]); + + $this->assertInstanceOf(AddressModel::class, $another_model->address); + + $this->assertSame('address_line_one_value', $model->address->lineOne); + $this->assertSame('address_line_two_value', $model->address->lineTwo); + } + + public function testInvalidArgumentExceptionOnInvalidValue() + { + /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The given value is not an Address instance.'); + $model->address = 'single_string'; + + // Ensure model values remain unchanged + $this->assertSame('address_line_one_value', $model->address->lineOne); + $this->assertSame('address_line_two_value', $model->address->lineTwo); + } + + public function testInvalidArgumentExceptionOnNull() + { + /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ + $model = CustomCasts::create([ + 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The given value is not an Address instance.'); + $model->address = null; + + // Ensure model values remain unchanged + $this->assertSame('address_line_one_value', $model->address->lineOne); + $this->assertSame('address_line_two_value', $model->address->lineTwo); + } + + /** + * Get a database connection instance. + * + * @return \Illuminate\Database\Connection + */ + protected function connection() + { + return Eloquent::getConnectionResolver()->connection(); + } + + /** + * Get a schema builder instance. + * + * @return \Illuminate\Database\Schema\Builder + */ + protected function schema() + { + return $this->connection()->getSchemaBuilder(); + } +} + +/** + * Eloquent Casts... + */ +class Address implements CastsAttributes +{ + /** + * Cast the given value. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param mixed $value + * @param array $attributes + * @return AddressModel + */ + public function get($model, $key, $value, $attributes) + { + return new AddressModel( + $attributes['address_line_one'], + $attributes['address_line_two'], + ); + } + + /** + * Prepare the given value for storage. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param AddressModel $value + * @param array $attributes + * @return array + */ + public function set($model, $key, $value, $attributes) + { + if (! $value instanceof AddressModel) { + throw new InvalidArgumentException('The given value is not an Address instance.'); + } + + return [ + 'address_line_one' => $value->lineOne, + 'address_line_two' => $value->lineTwo, + ]; + } +} + +/** + * Eloquent Models... + */ +class CustomCasts extends Eloquent +{ + /** + * @var string + */ + protected $table = 'casting_table'; + + /** + * @var string[] + */ + protected $guarded = []; + + /** + * @var array + */ + protected $casts = [ + 'address' => Address::class, + ]; +} + +class AddressModel +{ + /** + * @var string + */ + public $lineOne; + + /** + * @var string + */ + public $lineTwo; + + public function __construct($address_line_one, $address_line_two) + { + $this->lineOne = $address_line_one; + $this->lineTwo = $address_line_two; + } +} From e5e1e37d809d2cd877f89e2265b7e9fd95c64739 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij Date: Fri, 16 Jul 2021 15:19:51 +0200 Subject: [PATCH 3/5] Rename Address class to AddressCast in EloquentModelCustomCastingTest Prevents a duplicate naming conflict --- tests/Integration/Database/EloquentModelCustomCastingTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php index 0494bd54f023..dbfc47980d99 100644 --- a/tests/Integration/Database/EloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -138,7 +138,7 @@ protected function schema() /** * Eloquent Casts... */ -class Address implements CastsAttributes +class AddressCast implements CastsAttributes { /** * Cast the given value. @@ -198,7 +198,7 @@ class CustomCasts extends Eloquent * @var array */ protected $casts = [ - 'address' => Address::class, + 'address' => AddressCast::class, ]; } From 407d6ae09c0de2557aae79f7d689a0d2e05adb60 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij Date: Fri, 16 Jul 2021 15:48:26 +0200 Subject: [PATCH 4/5] Allow for proper null value handling in custom CastsAttributes implementations --- .../Eloquent/Concerns/HasAttributes.php | 22 +++------- .../EloquentModelCustomCastingTest.php | 41 +++++++++++++++++++ 2 files changed, 47 insertions(+), 16 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 363dd2461b57..ff89a5dffbc5 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -834,22 +834,12 @@ protected function setClassCastableAttribute($key, $value) { $caster = $this->resolveCasterClass($key); - if (is_null($value)) { - $this->attributes = array_merge($this->attributes, array_map( - function () { - }, - $this->normalizeCastClassResponse($key, $caster->set( - $this, $key, $value, $this->attributes - )) - )); - } else { - $this->attributes = array_merge( - $this->attributes, - $this->normalizeCastClassResponse($key, $caster->set( - $this, $key, $value, $this->attributes - )) - ); - } + $this->attributes = array_merge( + $this->attributes, + $this->normalizeCastClassResponse($key, $caster->set( + $this, $key, $value, $this->attributes + )) + ); if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { unset($this->classCastCache[$key]); diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php index dbfc47980d99..01e3625ff2cf 100644 --- a/tests/Integration/Database/EloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -40,6 +40,7 @@ public function createSchema() $table->increments('id'); $table->string('address_line_one'); $table->string('address_line_two'); + $table->string('string_field'); $table->timestamps(); }); } @@ -62,6 +63,7 @@ public function testSavingCastedAttributesToDatabase() /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ $model = CustomCasts::create([ 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'string_field' => null, ]); $this->assertSame('address_line_one_value', $model->getOriginal('address_line_one')); @@ -70,10 +72,15 @@ public function testSavingCastedAttributesToDatabase() $this->assertSame('address_line_two_value', $model->getOriginal('address_line_two')); $this->assertSame('address_line_two_value', $model->getAttribute('address_line_two')); + $this->assertSame(null, $model->getOriginal('string_field')); + $this->assertSame(null, $model->getAttribute('string_field')); + $this->assertSame('', $model->getRawOriginal('string_field')); + /** @var \Illuminate\Tests\Integration\Database\CustomCasts $another_model */ $another_model = CustomCasts::create([ 'address_line_one' => 'address_line_one_value', 'address_line_two' => 'address_line_two_value', + 'string_field' => 'string_value', ]); $this->assertInstanceOf(AddressModel::class, $another_model->address); @@ -87,6 +94,7 @@ public function testInvalidArgumentExceptionOnInvalidValue() /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ $model = CustomCasts::create([ 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'string_field' => 'string_value', ]); $this->expectException(InvalidArgumentException::class); @@ -103,6 +111,7 @@ public function testInvalidArgumentExceptionOnNull() /** @var \Illuminate\Tests\Integration\Database\CustomCasts $model */ $model = CustomCasts::create([ 'address' => new AddressModel('address_line_one_value', 'address_line_two_value'), + 'string_field' => 'string_value', ]); $this->expectException(InvalidArgumentException::class); @@ -179,6 +188,37 @@ public function set($model, $key, $value, $attributes) } } +class NonNullableString implements CastsAttributes +{ + /** + * Cast the given value. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param string $value + * @param array $attributes + * @return string|null + */ + public function get($model, $key, $value, $attributes) + { + return ($value <> '') ? $value : null; + } + + /** + * Prepare the given value for storage. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @param string $key + * @param string|null $value + * @param array $attributes + * @return string + */ + public function set($model, $key, $value, $attributes) + { + return $value ?? ''; + } +} + /** * Eloquent Models... */ @@ -199,6 +239,7 @@ class CustomCasts extends Eloquent */ protected $casts = [ 'address' => AddressCast::class, + 'string_field' => NonNullableString::class, ]; } From 52fac1ae107f57eae336e8fc8d7ea3656dd41190 Mon Sep 17 00:00:00 2001 From: Jesper Noordsij <45041769+jnoordsij@users.noreply.github.com> Date: Thu, 29 Jul 2021 17:22:59 +0200 Subject: [PATCH 5/5] Fix codestyle issue in EloquentModelCustomCastingTest.php --- tests/Integration/Database/EloquentModelCustomCastingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Integration/Database/EloquentModelCustomCastingTest.php b/tests/Integration/Database/EloquentModelCustomCastingTest.php index 01e3625ff2cf..39cf55454fff 100644 --- a/tests/Integration/Database/EloquentModelCustomCastingTest.php +++ b/tests/Integration/Database/EloquentModelCustomCastingTest.php @@ -201,7 +201,7 @@ class NonNullableString implements CastsAttributes */ public function get($model, $key, $value, $attributes) { - return ($value <> '') ? $value : null; + return ($value != '') ? $value : null; } /**