diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php new file mode 100644 index 000000000000..3ba60a951aa9 --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumArrayObject.php @@ -0,0 +1,82 @@ +arguments = $arguments; + } + + public function get($model, $key, $value, $attributes) + { + if (! isset($attributes[$key]) || is_null($attributes[$key])) { + return; + } + + $data = json_decode($attributes[$key], true); + + if (! is_array($data)) { + return; + } + + $enumClass = $this->arguments[0]; + + return new ArrayObject((new Collection($data))->map(function ($value) use ($enumClass) { + return is_subclass_of($enumClass, BackedEnum::class) + ? $enumClass::from($value) + : constant($enumClass.'::'.$value); + })->toArray()); + } + + public function set($model, $key, $value, $attributes) + { + if ($value === null) { + return [$key => null]; + } + + $storable = []; + + foreach ($value as $enum) { + $storable[] = $this->getStorableEnumValue($enum); + } + + return [$key => json_encode($storable)]; + } + + public function serialize($model, string $key, $value, array $attributes) + { + return (new Collection($value->getArrayCopy()))->map(function ($enum) { + return $this->getStorableEnumValue($enum); + })->toArray(); + } + + protected function getStorableEnumValue($enum) + { + if (is_string($enum) || is_int($enum)) { + return $enum; + } + + return $enum instanceof BackedEnum ? $enum->value : $enum->name; + } + }; + } +} diff --git a/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php new file mode 100644 index 000000000000..0d49606e585b --- /dev/null +++ b/src/Illuminate/Database/Eloquent/Casts/AsEnumCollection.php @@ -0,0 +1,77 @@ +arguments = $arguments; + } + + public function get($model, $key, $value, $attributes) + { + if (! isset($attributes[$key]) || is_null($attributes[$key])) { + return; + } + + $data = json_decode($attributes[$key], true); + + if (! is_array($data)) { + return; + } + + $enumClass = $this->arguments[0]; + + return (new Collection($data))->map(function ($value) use ($enumClass) { + return is_subclass_of($enumClass, BackedEnum::class) + ? $enumClass::from($value) + : constant($enumClass.'::'.$value); + }); + } + + public function set($model, $key, $value, $attributes) + { + $value = $value !== null + ? (new Collection($value))->map(function ($enum) { + return $this->getStorableEnumValue($enum); + })->toJson() + : null; + + return [$key => $value]; + } + + public function serialize($model, string $key, $value, array $attributes) + { + return (new Collection($value))->map(function ($enum) { + return $this->getStorableEnumValue($enum); + })->toArray(); + } + + protected function getStorableEnumValue($enum) + { + if (is_string($enum) || is_int($enum)) { + return $enum; + } + + return $enum instanceof BackedEnum ? $enum->value : $enum->name; + } + }; + } +} diff --git a/tests/Database/DatabaseEloquentModelTest.php b/tests/Database/DatabaseEloquentModelTest.php index 75fe19c41aac..16bf86ac32a9 100755 --- a/tests/Database/DatabaseEloquentModelTest.php +++ b/tests/Database/DatabaseEloquentModelTest.php @@ -11,14 +11,16 @@ use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Contracts\Events\Dispatcher; use Illuminate\Database\Connection; -use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\ConnectionResolverInterface as Resolver; +use Illuminate\Database\ConnectionResolverInterface; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\ArrayObject; use Illuminate\Database\Eloquent\Casts\AsArrayObject; use Illuminate\Database\Eloquent\Casts\AsCollection; use Illuminate\Database\Eloquent\Casts\AsEncryptedArrayObject; use Illuminate\Database\Eloquent\Casts\AsEncryptedCollection; +use Illuminate\Database\Eloquent\Casts\AsEnumArrayObject; +use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Casts\AsStringable; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\JsonEncodingException; @@ -43,6 +45,10 @@ use ReflectionClass; use stdClass; +if (PHP_VERSION_ID >= 80100) { + include 'Enums.php'; +} + class DatabaseEloquentModelTest extends TestCase { use InteractsWithTime; @@ -301,6 +307,48 @@ public function testDirtyOnCastedEncryptedArrayObject() $this->assertTrue($model->isDirty('asEncryptedArrayObjectAttribute')); } + /** + * @requires PHP >= 8.1 + */ + public function testDirtyOnEnumCollectionObject() + { + $model = new EloquentModelCastingStub; + $model->setRawAttributes([ + 'asEnumCollectionAttribute' => json_encode(['draft', 'pending']), + ]); + $model->syncOriginal(); + + $this->assertInstanceOf(BaseCollection::class, $model->asEnumCollectionAttribute); + $this->assertFalse($model->isDirty('asEnumCollectionAttribute')); + + $model->asEnumCollectionAttribute = ['draft', 'pending']; + $this->assertFalse($model->isDirty('asEnumCollectionAttribute')); + + $model->asEnumCollectionAttribute = ['draft', 'done']; + $this->assertTrue($model->isDirty('asEnumCollectionAttribute')); + } + + /** + * @requires PHP >= 8.1 + */ + public function testDirtyOnEnumArrayObject() + { + $model = new EloquentModelCastingStub; + $model->setRawAttributes([ + 'asEnumArrayObjectAttribute' => json_encode(['draft', 'pending']), + ]); + $model->syncOriginal(); + + $this->assertInstanceOf(ArrayObject::class, $model->asEnumArrayObjectAttribute); + $this->assertFalse($model->isDirty('asEnumArrayObjectAttribute')); + + $model->asEnumArrayObjectAttribute = ['draft', 'pending']; + $this->assertFalse($model->isDirty('asEnumArrayObjectAttribute')); + + $model->asEnumArrayObjectAttribute = ['draft', 'done']; + $this->assertTrue($model->isDirty('asEnumArrayObjectAttribute')); + } + public function testCleanAttributes() { $model = new EloquentModelStub(['foo' => '1', 'bar' => 2, 'baz' => 3]); @@ -2990,6 +3038,8 @@ class EloquentModelCastingStub extends Model 'asStringableAttribute' => AsStringable::class, 'asEncryptedCollectionAttribute' => AsEncryptedCollection::class, 'asEncryptedArrayObjectAttribute' => AsEncryptedArrayObject::class, + 'asEnumCollectionAttribute' => AsEnumCollection::class.':'.StringStatus::class, + 'asEnumArrayObjectAttribute' => AsEnumArrayObject::class.':'.StringStatus::class, ]; public function jsonAttributeValue() diff --git a/tests/Database/Enums.php b/tests/Database/Enums.php new file mode 100644 index 000000000000..2dbcb98d776d --- /dev/null +++ b/tests/Database/Enums.php @@ -0,0 +1,42 @@ + 'pending status description', + self::done => 'done status description' + }; + } + + public function toArray() + { + return [ + 'name' => $this->name, + 'value' => $this->value, + 'description' => $this->description(), + ]; + } +} diff --git a/tests/Integration/Database/EloquentModelEnumCastingTest.php b/tests/Integration/Database/EloquentModelEnumCastingTest.php index 3eb72aad6828..e342e09fef34 100644 --- a/tests/Integration/Database/EloquentModelEnumCastingTest.php +++ b/tests/Integration/Database/EloquentModelEnumCastingTest.php @@ -2,6 +2,8 @@ namespace Illuminate\Tests\Integration\Database; +use Illuminate\Database\Eloquent\Casts\AsEnumArrayObject; +use Illuminate\Database\Eloquent\Casts\AsEnumCollection; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\DB; @@ -21,7 +23,11 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed() Schema::create('enum_casts', function (Blueprint $table) { $table->increments('id'); $table->string('string_status', 100)->nullable(); + $table->json('string_status_collection')->nullable(); + $table->json('string_status_array')->nullable(); $table->integer('integer_status')->nullable(); + $table->json('integer_status_collection')->nullable(); + $table->json('integer_status_array')->nullable(); $table->string('arrayable_status')->nullable(); }); } @@ -30,14 +36,22 @@ public function testEnumsAreCastable() { DB::table('enum_casts')->insert([ 'string_status' => 'pending', + 'string_status_collection' => json_encode(['pending', 'done']), + 'string_status_array' => json_encode(['pending', 'done']), 'integer_status' => 1, + 'integer_status_collection' => json_encode([1, 2]), + 'integer_status_array' => json_encode([1, 2]), 'arrayable_status' => 'pending', ]); $model = EloquentModelEnumCastingTestModel::first(); $this->assertEquals(StringStatus::pending, $model->string_status); + $this->assertEquals([StringStatus::pending, StringStatus::done], $model->string_status_collection->all()); + $this->assertEquals([StringStatus::pending, StringStatus::done], $model->string_status_array->toArray()); $this->assertEquals(IntegerStatus::pending, $model->integer_status); + $this->assertEquals([IntegerStatus::pending, IntegerStatus::done], $model->integer_status_collection->all()); + $this->assertEquals([IntegerStatus::pending, IntegerStatus::done], $model->integer_status_array->toArray()); $this->assertEquals(ArrayableStatus::pending, $model->arrayable_status); } @@ -45,14 +59,22 @@ public function testEnumsReturnNullWhenNull() { DB::table('enum_casts')->insert([ 'string_status' => null, + 'string_status_collection' => null, + 'string_status_array' => null, 'integer_status' => null, + 'integer_status_collection' => null, + 'integer_status_array' => null, 'arrayable_status' => null, ]); $model = EloquentModelEnumCastingTestModel::first(); $this->assertEquals(null, $model->string_status); + $this->assertEquals(null, $model->string_status_collection); + $this->assertEquals(null, $model->string_status_array); $this->assertEquals(null, $model->integer_status); + $this->assertEquals(null, $model->integer_status_collection); + $this->assertEquals(null, $model->integer_status_array); $this->assertEquals(null, $model->arrayable_status); } @@ -60,13 +82,21 @@ public function testEnumsAreCastableToArray() { $model = new EloquentModelEnumCastingTestModel([ 'string_status' => StringStatus::pending, + 'string_status_collection' => [StringStatus::pending, StringStatus::done], + 'string_status_array' => [StringStatus::pending, StringStatus::done], 'integer_status' => IntegerStatus::pending, + 'integer_status_collection' => [IntegerStatus::pending, IntegerStatus::done], + 'integer_status_array' => [IntegerStatus::pending, IntegerStatus::done], 'arrayable_status' => ArrayableStatus::pending, ]); $this->assertEquals([ 'string_status' => 'pending', + 'string_status_collection' => ['pending', 'done'], + 'string_status_array' => ['pending', 'done'], 'integer_status' => 1, + 'integer_status_collection' => [1, 2], + 'integer_status_array' => [1, 2], 'arrayable_status' => [ 'name' => 'pending', 'value' => 'pending', @@ -79,13 +109,21 @@ public function testEnumsAreCastableToArrayWhenNull() { $model = new EloquentModelEnumCastingTestModel([ 'string_status' => null, + 'string_status_collection' => null, + 'string_status_array' => null, 'integer_status' => null, + 'integer_status_collection' => null, + 'integer_status_array' => null, 'arrayable_status' => null, ]); $this->assertEquals([ 'string_status' => null, + 'string_status_collection' => null, + 'string_status_array' => null, 'integer_status' => null, + 'integer_status_collection' => null, + 'integer_status_array' => null, 'arrayable_status' => null, ], $model->toArray()); } @@ -94,25 +132,67 @@ public function testEnumsAreConvertedOnSave() { $model = new EloquentModelEnumCastingTestModel([ 'string_status' => StringStatus::pending, + 'string_status_collection' => [StringStatus::pending, StringStatus::done], + 'string_status_array' => [StringStatus::pending, StringStatus::done], 'integer_status' => IntegerStatus::pending, + 'integer_status_collection' => [IntegerStatus::pending, IntegerStatus::done], + 'integer_status_array' => [IntegerStatus::pending, IntegerStatus::done], 'arrayable_status' => ArrayableStatus::pending, ]); $model->save(); - $this->assertEquals((object) [ + $this->assertEquals([ 'id' => $model->id, 'string_status' => 'pending', + 'string_status_collection' => json_encode(['pending', 'done']), + 'string_status_array' => json_encode(['pending', 'done']), 'integer_status' => 1, + 'integer_status_collection' => json_encode([1, 2]), + 'integer_status_array' => json_encode([1, 2]), 'arrayable_status' => 'pending', - ], DB::table('enum_casts')->where('id', $model->id)->first()); + ], collect(DB::table('enum_casts')->where('id', $model->id)->first())->map(function ($value) { + return str_replace(', ', ',', $value); + })->all()); + } + + public function testEnumsAreNotConvertedOnSaveWhenAlreadyCorrect() + { + $model = new EloquentModelEnumCastingTestModel([ + 'string_status' => 'pending', + 'string_status_collection' => ['pending', 'done'], + 'string_status_array' => ['pending', 'done'], + 'integer_status' => 1, + 'integer_status_collection' => [1, 2], + 'integer_status_array' => [1, 2], + 'arrayable_status' => 'pending', + ]); + + $model->save(); + + $this->assertEquals([ + 'id' => $model->id, + 'string_status' => 'pending', + 'string_status_collection' => json_encode(['pending', 'done']), + 'string_status_array' => json_encode(['pending', 'done']), + 'integer_status' => 1, + 'integer_status_collection' => json_encode([1, 2]), + 'integer_status_array' => json_encode([1, 2]), + 'arrayable_status' => 'pending', + ], collect(DB::table('enum_casts')->where('id', $model->id)->first())->map(function ($value) { + return str_replace(', ', ',', $value); + })->all()); } public function testEnumsAcceptNullOnSave() { $model = new EloquentModelEnumCastingTestModel([ 'string_status' => null, + 'string_status_collection' => null, + 'string_status_array' => null, 'integer_status' => null, + 'integer_status_collection' => null, + 'integer_status_array' => null, 'arrayable_status' => null, ]); @@ -121,7 +201,11 @@ public function testEnumsAcceptNullOnSave() $this->assertEquals((object) [ 'id' => $model->id, 'string_status' => null, + 'string_status_collection' => null, + 'string_status_array' => null, 'integer_status' => null, + 'integer_status_collection' => null, + 'integer_status_array' => null, 'arrayable_status' => null, ], DB::table('enum_casts')->where('id', $model->id)->first()); } @@ -195,7 +279,11 @@ class EloquentModelEnumCastingTestModel extends Model public $casts = [ 'string_status' => StringStatus::class, + 'string_status_collection' => AsEnumCollection::class.':'.StringStatus::class, + 'string_status_array' => AsEnumArrayObject::class.':'.StringStatus::class, 'integer_status' => IntegerStatus::class, + 'integer_status_collection' => AsEnumCollection::class.':'.IntegerStatus::class, + 'integer_status_array' => AsEnumArrayObject::class.':'.IntegerStatus::class, 'arrayable_status' => ArrayableStatus::class, ]; } diff --git a/tests/Integration/Database/Enums.php b/tests/Integration/Database/Enums.php index fc466716533a..f3bf199a16dc 100644 --- a/tests/Integration/Database/Enums.php +++ b/tests/Integration/Database/Enums.php @@ -6,12 +6,14 @@ enum StringStatus: string { + case draft = 'draft'; case pending = 'pending'; case done = 'done'; } enum IntegerStatus: int { + case draft = 0; case pending = 1; case done = 2; }