From 2fb79cd69e7de7d4971ff07e88dec0dbd182d3c8 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 15:37:15 +0200 Subject: [PATCH 01/12] php8.1 enums support in eloquent --- phpunit.xml.dist | 1 + .../Eloquent/Concerns/HasAttributes.php | 73 ++++++++++++- .../Database/EloquentModelEnumCastingTest.php | 101 ++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tests/Integration/Database/EloquentModelEnumCastingTest.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 05294e99751a..34af147fff60 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,7 @@ ./tests + ./tests/Integration/Database/EloquentModelEnumCastingTest.php diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 22dde6e6f9d0..46b523272746 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -264,6 +264,10 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt $attributes[$key] = $this->serializeClassCastableAttribute($key, $attributes[$key]); } + if ($this->isEnumCastable($key)) { + $attributes[$key] = $attributes[$key]->value; + } + if ($attributes[$key] instanceof Arrayable) { $attributes[$key] = $attributes[$key]->toArray(); } @@ -622,6 +626,10 @@ protected function castAttribute($key, $value) return $this->asTimestamp($value); } + if ($this->isEnumCastable($key)) { + return $this->getEnumCastableAttributeValue($key, $value); + } + if ($this->isClassCastable($key)) { return $this->getClassCastableAttributeValue($key, $value); } @@ -657,6 +665,20 @@ protected function getClassCastableAttributeValue($key, $value) } } + /** + * Cast the given attribute using a custom cast class. + * + * @param string $key + * @param mixed $value + * @return mixed + */ + protected function getEnumCastableAttributeValue($key, $value) + { + $castType = $this->getCasts()[$key]; + + return $castType::from($value); + } + /** * Get the type of cast for a model attribute. * @@ -767,6 +789,12 @@ public function setAttribute($key, $value) $value = $this->fromDateTime($value); } + if ($this->isEnumCastable($key)) { + $this->setEnumCastableAttribute($key, $value); + + return $this; + } + if ($this->isClassCastable($key)) { $this->setClassCastableAttribute($key, $value); @@ -885,6 +913,21 @@ function () { } } + /** + * Set the value of a enum castable attribute. + * + * @param string $key + * @param mixed $value + * @return void + */ + protected function setEnumCastableAttribute($key, $value) + { + $this->attributes = array_merge( + $this->attributes, + [$key => $value->value] + ); + } + /** * Get an array attribute with the given key and value set. * @@ -1282,6 +1325,33 @@ protected function isClassCastable($key) throw new InvalidCastException($this->getModel(), $key, $castType); } + /** + * Determine if the given key is cast using an enum. + * + * @param string $key + * @return bool + * + * @throws \Illuminate\Database\Eloquent\InvalidCastException + */ + protected function isEnumCastable($key) + { + if (! array_key_exists($key, $this->getCasts())) { + return false; + } + + $castType = $this->getCasts()[$key]; + + if (in_array($castType, static::$primitiveCastTypes)) { + return false; + } + + if (function_exists('enum_exists') && enum_exists($castType)) { + return true; + } + + throw new InvalidCastException($this->getModel(), $key, $castType); + } + /** * Determine if the key is deviable using a custom class. * @@ -1307,7 +1377,8 @@ protected function isClassDeviable($key) */ protected function isClassSerializable($key) { - return $this->isClassCastable($key) && + return ! $this->isEnumCastable($key) && + $this->isClassCastable($key) && method_exists($this->resolveCasterClass($key), 'serialize'); } diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php new file mode 100644 index 000000000000..50965b014069 --- /dev/null +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -0,0 +1,101 @@ +increments('id'); + $table->string('string_status', 100)->nullable(); + $table->integer('integer_status')->nullable(); + }); + } + + public function testEnumsAreCastable() + { + DB::table('enum_casts')->insert([ + 'string_status' => 'pending', + 'integer_status' => 1, + ]); + + $model = EloquentModelEnumCastingTestModel::first(); + + $this->assertEquals(StringStatus::pending, $model->string_status); + $this->assertEquals(IntegerStatus::pending, $model->integer_status); + + } + + public function testEnumsAreCastableToArray() + { + $model = new EloquentModelEnumCastingTestModel([ + 'string_status' => StringStatus::pending, + 'integer_status' => IntegerStatus::pending, + ]); + + $this->assertEquals([ + 'string_status' => 'pending', + 'integer_status' => 1, + ], $model->toArray()); + } + + public function testEnumsAreConvertedOnSave() + { + $model = new EloquentModelEnumCastingTestModel([ + 'string_status' => StringStatus::pending, + 'integer_status' => IntegerStatus::pending, + ]); + + $model->save(); + + $this->assertEquals((object) [ + 'id' => $model->id, + 'string_status' => 'pending', + 'integer_status' => 1, + ], DB::table('enum_casts')->where('id', $model->id)->first()); + } +} + +/** + * @property $secret + * @property $secret_array + * @property $secret_json + * @property $secret_object + * @property $secret_collection + */ +class EloquentModelEnumCastingTestModel extends Model +{ + public $timestamps = false; + protected $guarded = []; + protected $table = 'enum_casts'; + + public $casts = [ + 'string_status' => StringStatus::class, + 'integer_status' => IntegerStatus::class, + ]; +} + +enum StringStatus : string { + case pending = 'pending'; + case done = 'done'; +} + +enum IntegerStatus : int { + case pending = 1; + case done = 2; +} From 53af6c90fcd56809884079e39c2b8a4eb9cb30cd Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 15:50:44 +0200 Subject: [PATCH 02/12] include the test in php8.1 only --- phpunit.xml.dist | 1 - .../Database/EloquentModelEnumCastingTest.php | 27 +++++++++---------- tests/Integration/Database/Enums.php | 15 +++++++++++ 3 files changed, 28 insertions(+), 15 deletions(-) create mode 100644 tests/Integration/Database/Enums.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index bc5549ff092e..70f89b954c3c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,7 +15,6 @@ ./tests - ./tests/Integration/Database/EloquentModelEnumCastingTest.php diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index 50965b014069..4239e869e5d2 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -2,14 +2,14 @@ namespace Illuminate\Tests\Integration\Database; -use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\Collection; -use Illuminate\Support\Facades\Crypt; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -use stdClass; + +if (strpos(PHP_VERSION, '8.1') === 0) { + include 'Enums.php'; +} /** * @group integration @@ -27,6 +27,9 @@ protected function setUp(): void }); } + /** + * @requires PHP 8.1 + */ public function testEnumsAreCastable() { DB::table('enum_casts')->insert([ @@ -41,6 +44,9 @@ public function testEnumsAreCastable() } + /** + * @requires PHP 8.1 + */ public function testEnumsAreCastableToArray() { $model = new EloquentModelEnumCastingTestModel([ @@ -54,6 +60,9 @@ public function testEnumsAreCastableToArray() ], $model->toArray()); } + /** + * @requires PHP 8.1 + */ public function testEnumsAreConvertedOnSave() { $model = new EloquentModelEnumCastingTestModel([ @@ -89,13 +98,3 @@ class EloquentModelEnumCastingTestModel extends Model 'integer_status' => IntegerStatus::class, ]; } - -enum StringStatus : string { - case pending = 'pending'; - case done = 'done'; -} - -enum IntegerStatus : int { - case pending = 1; - case done = 2; -} diff --git a/tests/Integration/Database/Enums.php b/tests/Integration/Database/Enums.php new file mode 100644 index 000000000000..7b03302aa2f5 --- /dev/null +++ b/tests/Integration/Database/Enums.php @@ -0,0 +1,15 @@ + Date: Fri, 22 Oct 2021 15:57:32 +0200 Subject: [PATCH 03/12] fix --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 46b523272746..f8ccd6f3726f 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1330,8 +1330,6 @@ protected function isClassCastable($key) * * @param string $key * @return bool - * - * @throws \Illuminate\Database\Eloquent\InvalidCastException */ protected function isEnumCastable($key) { @@ -1348,8 +1346,6 @@ protected function isEnumCastable($key) if (function_exists('enum_exists') && enum_exists($castType)) { return true; } - - throw new InvalidCastException($this->getModel(), $key, $castType); } /** From 386f1d4a83b69eafb3c29351690b90bf71f239f2 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 15:59:58 +0200 Subject: [PATCH 04/12] fix tests --- .../Database/EloquentModelEnumCastingTest.php | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index 4239e869e5d2..73ccd82be169 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -7,11 +7,12 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Schema; -if (strpos(PHP_VERSION, '8.1') === 0) { +if (PHP_VERSION_ID >= 80100) { include 'Enums.php'; } /** + * @requires PHP 8.1 * @group integration */ class EloquentModelEnumCastingTest extends DatabaseTestCase @@ -27,9 +28,6 @@ protected function setUp(): void }); } - /** - * @requires PHP 8.1 - */ public function testEnumsAreCastable() { DB::table('enum_casts')->insert([ @@ -44,9 +42,6 @@ public function testEnumsAreCastable() } - /** - * @requires PHP 8.1 - */ public function testEnumsAreCastableToArray() { $model = new EloquentModelEnumCastingTestModel([ @@ -60,9 +55,6 @@ public function testEnumsAreCastableToArray() ], $model->toArray()); } - /** - * @requires PHP 8.1 - */ public function testEnumsAreConvertedOnSave() { $model = new EloquentModelEnumCastingTestModel([ From 9662a66fb2dfdf55febd8ab4c359b2c0fa5297e6 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 16:02:45 +0200 Subject: [PATCH 05/12] exclude EloquentModelEnumCastingTest from styleci --- .styleci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.styleci.yml b/.styleci.yml index 77081cc0b741..db118d510e9b 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -1,6 +1,9 @@ php: preset: laravel version: 8 + finder: + not-name: + - tests/Integration/Database/EloquentModelEnumCastingTest.php js: finder: not-name: From e33eb99d7cc38144264ba51b6569bfd73ee8a5d7 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 16:03:19 +0200 Subject: [PATCH 06/12] exclude EloquentModelEnumCastingTest from styleci --- .styleci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.styleci.yml b/.styleci.yml index db118d510e9b..23474253f476 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -3,7 +3,7 @@ php: version: 8 finder: not-name: - - tests/Integration/Database/EloquentModelEnumCastingTest.php + - tests/Integration/Database/Enums.php js: finder: not-name: From 52447312afbf5e004988c77252271db69d05e04b Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 16:04:57 +0200 Subject: [PATCH 07/12] fix style --- tests/Integration/Database/EloquentModelEnumCastingTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index 73ccd82be169..203d8dbab1de 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -39,7 +39,6 @@ public function testEnumsAreCastable() $this->assertEquals(StringStatus::pending, $model->string_status); $this->assertEquals(IntegerStatus::pending, $model->integer_status); - } public function testEnumsAreCastableToArray() From 319dde563b0eed160ed99c03a093c4da28906605 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 22 Oct 2021 16:12:54 +0200 Subject: [PATCH 08/12] exclude for styleci --- .styleci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.styleci.yml b/.styleci.yml index 23474253f476..474d04edc478 100644 --- a/.styleci.yml +++ b/.styleci.yml @@ -3,7 +3,7 @@ php: version: 8 finder: not-name: - - tests/Integration/Database/Enums.php + - Enums.php js: finder: not-name: From f2c8d314c824754d150f73444b2a0018cdc5366b Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Sat, 23 Oct 2021 08:02:56 +0200 Subject: [PATCH 09/12] wip --- .../Database/Eloquent/Concerns/HasAttributes.php | 4 ++-- .../Integration/Database/EloquentModelEnumCastingTest.php | 7 ------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index f8ccd6f3726f..c8fe278cb970 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -914,10 +914,10 @@ function () { } /** - * Set the value of a enum castable attribute. + * Set the value of an enum castable attribute. * * @param string $key - * @param mixed $value + * @param \BackedEnum $value * @return void */ protected function setEnumCastableAttribute($key, $value) diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index 203d8dbab1de..ed695e973a86 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -71,13 +71,6 @@ public function testEnumsAreConvertedOnSave() } } -/** - * @property $secret - * @property $secret_array - * @property $secret_json - * @property $secret_object - * @property $secret_collection - */ class EloquentModelEnumCastingTestModel extends Model { public $timestamps = false; From 271176874b18a74c4ed3d19fa93c732043aaabed Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Thu, 28 Oct 2021 15:31:51 -0500 Subject: [PATCH 10/12] formatting --- .../Database/Eloquent/Concerns/HasAttributes.php | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index c8fe278cb970..a80ccb520364 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -666,7 +666,7 @@ protected function getClassCastableAttributeValue($key, $value) } /** - * Cast the given attribute using a custom cast class. + * Cast the given attribute to an enum. * * @param string $key * @param mixed $value @@ -922,10 +922,7 @@ function () { */ protected function setEnumCastableAttribute($key, $value) { - $this->attributes = array_merge( - $this->attributes, - [$key => $value->value] - ); + $this->attributes[$key] = $value->value; } /** From 331322173562df6da9027fa40bfee344fe0e57ce Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 29 Oct 2021 13:43:47 +0200 Subject: [PATCH 11/12] return null for null values --- .../Database/Eloquent/Concerns/HasAttributes.php | 4 ++++ .../Database/EloquentModelEnumCastingTest.php | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index a80ccb520364..9626809a5fb4 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -674,6 +674,10 @@ protected function getClassCastableAttributeValue($key, $value) */ protected function getEnumCastableAttributeValue($key, $value) { + if (is_null($value)) { + return; + } + $castType = $this->getCasts()[$key]; return $castType::from($value); diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index ed695e973a86..a25092577346 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -41,6 +41,19 @@ public function testEnumsAreCastable() $this->assertEquals(IntegerStatus::pending, $model->integer_status); } + public function testEnumsReturnNullWhenNull() + { + DB::table('enum_casts')->insert([ + 'string_status' => null, + 'integer_status' => null, + ]); + + $model = EloquentModelEnumCastingTestModel::first(); + + $this->assertEquals(null, $model->string_status); + $this->assertEquals(null, $model->integer_status); + } + public function testEnumsAreCastableToArray() { $model = new EloquentModelEnumCastingTestModel([ From 64be8e6d28ff95795ead3ce8975f5b2212c1a165 Mon Sep 17 00:00:00 2001 From: Mohamed Said Date: Fri, 29 Oct 2021 15:46:31 +0200 Subject: [PATCH 12/12] handle null on casting to enum --- .../Eloquent/Concerns/HasAttributes.php | 4 +-- .../Database/EloquentModelEnumCastingTest.php | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 9626809a5fb4..f4d25cb44337 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -265,7 +265,7 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt } if ($this->isEnumCastable($key)) { - $attributes[$key] = $attributes[$key]->value; + $attributes[$key] = isset($attributes[$key] ) ? $attributes[$key]->value : null; } if ($attributes[$key] instanceof Arrayable) { @@ -926,7 +926,7 @@ function () { */ protected function setEnumCastableAttribute($key, $value) { - $this->attributes[$key] = $value->value; + $this->attributes[$key] = isset($value) ? $value->value : null; } /** diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index a25092577346..199e5b54e484 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -67,6 +67,19 @@ public function testEnumsAreCastableToArray() ], $model->toArray()); } + public function testEnumsAreCastableToArrayWhenNull() + { + $model = new EloquentModelEnumCastingTestModel([ + 'string_status' => null, + 'integer_status' => null, + ]); + + $this->assertEquals([ + 'string_status' => null, + 'integer_status' => null, + ], $model->toArray()); + } + public function testEnumsAreConvertedOnSave() { $model = new EloquentModelEnumCastingTestModel([ @@ -82,6 +95,22 @@ public function testEnumsAreConvertedOnSave() 'integer_status' => 1, ], DB::table('enum_casts')->where('id', $model->id)->first()); } + + public function testEnumsAcceptNullOnSave() + { + $model = new EloquentModelEnumCastingTestModel([ + 'string_status' => null, + 'integer_status' => null, + ]); + + $model->save(); + + $this->assertEquals((object) [ + 'id' => $model->id, + 'string_status' => null, + 'integer_status' => null, + ], DB::table('enum_casts')->where('id', $model->id)->first()); + } } class EloquentModelEnumCastingTestModel extends Model