Skip to content

Commit c0bb0b1

Browse files
[8.x] Allow model attributes to be casted to/from an Enum (#39315)
* php8.1 enums support in eloquent * include the test in php8.1 only * fix * fix tests * exclude EloquentModelEnumCastingTest from styleci * exclude EloquentModelEnumCastingTest from styleci * fix style * exclude for styleci * wip * formatting * return null for null values * handle null on casting to enum Co-authored-by: Taylor Otwell <[email protected]>
1 parent 92cfdfb commit c0bb0b1

File tree

4 files changed

+213
-1
lines changed

4 files changed

+213
-1
lines changed

.styleci.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
php:
22
preset: laravel
33
version: 8
4+
finder:
5+
not-name:
6+
- Enums.php
47
js:
58
finder:
69
not-name:

src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,10 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
264264
$attributes[$key] = $this->serializeClassCastableAttribute($key, $attributes[$key]);
265265
}
266266

267+
if ($this->isEnumCastable($key)) {
268+
$attributes[$key] = isset($attributes[$key] ) ? $attributes[$key]->value : null;
269+
}
270+
267271
if ($attributes[$key] instanceof Arrayable) {
268272
$attributes[$key] = $attributes[$key]->toArray();
269273
}
@@ -622,6 +626,10 @@ protected function castAttribute($key, $value)
622626
return $this->asTimestamp($value);
623627
}
624628

629+
if ($this->isEnumCastable($key)) {
630+
return $this->getEnumCastableAttributeValue($key, $value);
631+
}
632+
625633
if ($this->isClassCastable($key)) {
626634
return $this->getClassCastableAttributeValue($key, $value);
627635
}
@@ -657,6 +665,24 @@ protected function getClassCastableAttributeValue($key, $value)
657665
}
658666
}
659667

668+
/**
669+
* Cast the given attribute to an enum.
670+
*
671+
* @param string $key
672+
* @param mixed $value
673+
* @return mixed
674+
*/
675+
protected function getEnumCastableAttributeValue($key, $value)
676+
{
677+
if (is_null($value)) {
678+
return;
679+
}
680+
681+
$castType = $this->getCasts()[$key];
682+
683+
return $castType::from($value);
684+
}
685+
660686
/**
661687
* Get the type of cast for a model attribute.
662688
*
@@ -767,6 +793,12 @@ public function setAttribute($key, $value)
767793
$value = $this->fromDateTime($value);
768794
}
769795

796+
if ($this->isEnumCastable($key)) {
797+
$this->setEnumCastableAttribute($key, $value);
798+
799+
return $this;
800+
}
801+
770802
if ($this->isClassCastable($key)) {
771803
$this->setClassCastableAttribute($key, $value);
772804

@@ -885,6 +917,18 @@ function () {
885917
}
886918
}
887919

920+
/**
921+
* Set the value of an enum castable attribute.
922+
*
923+
* @param string $key
924+
* @param \BackedEnum $value
925+
* @return void
926+
*/
927+
protected function setEnumCastableAttribute($key, $value)
928+
{
929+
$this->attributes[$key] = isset($value) ? $value->value : null;
930+
}
931+
888932
/**
889933
* Get an array attribute with the given key and value set.
890934
*
@@ -1282,6 +1326,29 @@ protected function isClassCastable($key)
12821326
throw new InvalidCastException($this->getModel(), $key, $castType);
12831327
}
12841328

1329+
/**
1330+
* Determine if the given key is cast using an enum.
1331+
*
1332+
* @param string $key
1333+
* @return bool
1334+
*/
1335+
protected function isEnumCastable($key)
1336+
{
1337+
if (! array_key_exists($key, $this->getCasts())) {
1338+
return false;
1339+
}
1340+
1341+
$castType = $this->getCasts()[$key];
1342+
1343+
if (in_array($castType, static::$primitiveCastTypes)) {
1344+
return false;
1345+
}
1346+
1347+
if (function_exists('enum_exists') && enum_exists($castType)) {
1348+
return true;
1349+
}
1350+
}
1351+
12851352
/**
12861353
* Determine if the key is deviable using a custom class.
12871354
*
@@ -1307,7 +1374,8 @@ protected function isClassDeviable($key)
13071374
*/
13081375
protected function isClassSerializable($key)
13091376
{
1310-
return $this->isClassCastable($key) &&
1377+
return ! $this->isEnumCastable($key) &&
1378+
$this->isClassCastable($key) &&
13111379
method_exists($this->resolveCasterClass($key), 'serialize');
13121380
}
13131381

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database;
4+
5+
use Illuminate\Database\Eloquent\Model;
6+
use Illuminate\Database\Schema\Blueprint;
7+
use Illuminate\Support\Facades\DB;
8+
use Illuminate\Support\Facades\Schema;
9+
10+
if (PHP_VERSION_ID >= 80100) {
11+
include 'Enums.php';
12+
}
13+
14+
/**
15+
* @requires PHP 8.1
16+
* @group integration
17+
*/
18+
class EloquentModelEnumCastingTest extends DatabaseTestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
Schema::create('enum_casts', function (Blueprint $table) {
25+
$table->increments('id');
26+
$table->string('string_status', 100)->nullable();
27+
$table->integer('integer_status')->nullable();
28+
});
29+
}
30+
31+
public function testEnumsAreCastable()
32+
{
33+
DB::table('enum_casts')->insert([
34+
'string_status' => 'pending',
35+
'integer_status' => 1,
36+
]);
37+
38+
$model = EloquentModelEnumCastingTestModel::first();
39+
40+
$this->assertEquals(StringStatus::pending, $model->string_status);
41+
$this->assertEquals(IntegerStatus::pending, $model->integer_status);
42+
}
43+
44+
public function testEnumsReturnNullWhenNull()
45+
{
46+
DB::table('enum_casts')->insert([
47+
'string_status' => null,
48+
'integer_status' => null,
49+
]);
50+
51+
$model = EloquentModelEnumCastingTestModel::first();
52+
53+
$this->assertEquals(null, $model->string_status);
54+
$this->assertEquals(null, $model->integer_status);
55+
}
56+
57+
public function testEnumsAreCastableToArray()
58+
{
59+
$model = new EloquentModelEnumCastingTestModel([
60+
'string_status' => StringStatus::pending,
61+
'integer_status' => IntegerStatus::pending,
62+
]);
63+
64+
$this->assertEquals([
65+
'string_status' => 'pending',
66+
'integer_status' => 1,
67+
], $model->toArray());
68+
}
69+
70+
public function testEnumsAreCastableToArrayWhenNull()
71+
{
72+
$model = new EloquentModelEnumCastingTestModel([
73+
'string_status' => null,
74+
'integer_status' => null,
75+
]);
76+
77+
$this->assertEquals([
78+
'string_status' => null,
79+
'integer_status' => null,
80+
], $model->toArray());
81+
}
82+
83+
public function testEnumsAreConvertedOnSave()
84+
{
85+
$model = new EloquentModelEnumCastingTestModel([
86+
'string_status' => StringStatus::pending,
87+
'integer_status' => IntegerStatus::pending,
88+
]);
89+
90+
$model->save();
91+
92+
$this->assertEquals((object) [
93+
'id' => $model->id,
94+
'string_status' => 'pending',
95+
'integer_status' => 1,
96+
], DB::table('enum_casts')->where('id', $model->id)->first());
97+
}
98+
99+
public function testEnumsAcceptNullOnSave()
100+
{
101+
$model = new EloquentModelEnumCastingTestModel([
102+
'string_status' => null,
103+
'integer_status' => null,
104+
]);
105+
106+
$model->save();
107+
108+
$this->assertEquals((object) [
109+
'id' => $model->id,
110+
'string_status' => null,
111+
'integer_status' => null,
112+
], DB::table('enum_casts')->where('id', $model->id)->first());
113+
}
114+
}
115+
116+
class EloquentModelEnumCastingTestModel extends Model
117+
{
118+
public $timestamps = false;
119+
protected $guarded = [];
120+
protected $table = 'enum_casts';
121+
122+
public $casts = [
123+
'string_status' => StringStatus::class,
124+
'integer_status' => IntegerStatus::class,
125+
];
126+
}

tests/Integration/Database/Enums.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
3+
namespace Illuminate\Tests\Integration\Database;
4+
5+
enum StringStatus: string
6+
{
7+
case pending = 'pending';
8+
case done = 'done';
9+
}
10+
11+
enum IntegerStatus: int
12+
{
13+
case pending = 1;
14+
case done = 2;
15+
}

0 commit comments

Comments
 (0)