Skip to content

Commit

Permalink
Implement enum array casts
Browse files Browse the repository at this point in the history
  • Loading branch information
ralphjsmit committed Jan 12, 2023
1 parent c0affdd commit da40aa8
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 3 deletions.
96 changes: 93 additions & 3 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt
$attributes[$key] = isset($attributes[$key]) ? $this->getStorableEnumValue($attributes[$key]) : null;
}

if ($this->isEnumArrayCastable($key)) {
$attributes[$key] = isset($attributes[$key]) ? $this->getStorableEnumArrayValue($attributes[$key]) : null;
}

if ($attributes[$key] instanceof Arrayable) {
$attributes[$key] = $attributes[$key]->toArray();
}
Expand Down Expand Up @@ -789,6 +793,10 @@ protected function castAttribute($key, $value)
return $this->getEnumCastableAttributeValue($key, $value);
}

if ($this->isEnumArrayCastable($key)) {
return $this->getEnumArrayCastableAttributeValue($key, $value);
}

if ($this->isClassCastable($key)) {
return $this->getClassCastableAttributeValue($key, $value);
}
Expand Down Expand Up @@ -846,6 +854,26 @@ protected function getEnumCastableAttributeValue($key, $value)
return $this->getEnumCaseFromValue($castType, $value);
}

/**
* Cast the given attribute to an array of enums.
*
* @param string $key
* @param mixed $value
* @return mixed
*/
protected function getEnumArrayCastableAttributeValue($key, $value)
{
if (is_null($value)) {
return;
}

$castType = Str::before($this->getCasts()[$key], ':array');

$value = is_array($value) ? $value : $this->fromJson($value);

return array_map(fn (string $value) => $this->getEnumCaseFromValue($castType, $value), $value);
}

/**
* Get the type of cast for a model attribute.
*
Expand Down Expand Up @@ -968,6 +996,12 @@ public function setAttribute($key, $value)
return $this;
}

if ($this->isEnumArrayCastable($key)) {
$this->setEnumArrayCastableAttribute($key, $value);

return $this;
}

if ($this->isClassCastable($key)) {
$this->setClassCastableAttribute($key, $value);

Expand Down Expand Up @@ -1155,6 +1189,25 @@ protected function setEnumCastableAttribute($key, $value)
} else {
$this->attributes[$key] = $this->getStorableEnumValue(
$this->getEnumCaseFromValue($enumClass, $value)

/**
* Set the value of an enum array castable attribute.
*
* @param string $key
* @param \UnitEnum|string|int $value
* @return void
*/
protected function setEnumArrayCastableAttribute($key, $value)
{
$enumClass = Str::before($this->getCasts()[$key], ':array');

if (! isset($value)) {
$this->attributes[$key] = null;
} else {
$value = is_array($value) ? $value : $this->fromJson($value);

$this->attributes[$key] = $this->getStorableEnumArrayValue(
array_map(fn ($value) => is_object($value) ? $value : $this->getEnumCaseFromValue($enumClass, $value), $value)
);
}
}
Expand Down Expand Up @@ -1184,6 +1237,15 @@ protected function getStorableEnumValue($value)
return $value instanceof BackedEnum
? $value->value
: $value->name;
/**
* Get the storable value from the given enum.
*
* @param array $value
* @return string
*/
protected function getStorableEnumArrayValue($value)
{
return json_encode(array_map(fn ($value) => $this->getStorableEnumValue($value), $value));
}

/**
Expand Down Expand Up @@ -1620,6 +1682,33 @@ protected function isEnumCastable($key)
}
}

/**
* Determine if the given key is cast using an array of enums.
*
* @param string $key
* @return bool
*/
protected function isEnumArrayCastable($key)
{
$casts = $this->getCasts();

if (! array_key_exists($key, $casts)) {
return false;
}

$castType = $casts[$key];

if (in_array($castType, static::$primitiveCastTypes)) {
return false;
}

if (! Str::endsWith($castType, ':array')) {
return false;
}

return function_exists('enum_exists') && enum_exists(Str::before($castType, ':array'));
}

/**
* Determine if the key is deviable using a custom class.
*
Expand Down Expand Up @@ -1649,9 +1738,10 @@ protected function isClassDeviable($key)
*/
protected function isClassSerializable($key)
{
return ! $this->isEnumCastable($key) &&
$this->isClassCastable($key) &&
method_exists($this->resolveCasterClass($key), 'serialize');
return ! $this->isEnumCastable($key)
&& ! $this->isEnumArrayCastable($key)
&& $this->isClassCastable($key)
&& method_exists($this->resolveCasterClass($key), 'serialize');
}

/**
Expand Down
50 changes: 50 additions & 0 deletions tests/Integration/Database/EloquentModelEnumCastingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ protected function defineDatabaseMigrationsAfterDatabaseRefreshed()
Schema::create('enum_casts', function (Blueprint $table) {
$table->increments('id');
$table->string('string_status', 100)->nullable();
$table->json('string_status_array', 100)->nullable();
$table->integer('integer_status')->nullable();
$table->json('integer_status_array')->nullable();
$table->string('arrayable_status')->nullable();
});
}
Expand All @@ -30,43 +32,55 @@ public function testEnumsAreCastable()
{
DB::table('enum_casts')->insert([
'string_status' => 'pending',
'string_status_array' => json_encode(['pending', 'done']),
'integer_status' => 1,
'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_array);
$this->assertEquals(IntegerStatus::pending, $model->integer_status);
$this->assertEquals([IntegerStatus::pending, IntegerStatus::done], $model->integer_status_array);
$this->assertEquals(ArrayableStatus::pending, $model->arrayable_status);
}

public function testEnumsReturnNullWhenNull()
{
DB::table('enum_casts')->insert([
'string_status' => null,
'string_status_array' => null,
'integer_status' => null,
'integer_status_array' => null,
'arrayable_status' => null,
]);

$model = EloquentModelEnumCastingTestModel::first();

$this->assertEquals(null, $model->string_status);
$this->assertEquals(null, $model->string_status_array);
$this->assertEquals(null, $model->integer_status);
$this->assertEquals(null, $model->integer_status_array);
$this->assertEquals(null, $model->arrayable_status);
}

public function testEnumsAreCastableToArray()
{
$model = new EloquentModelEnumCastingTestModel([
'string_status' => StringStatus::pending,
'string_status_array' => [StringStatus::pending, StringStatus::done],
'integer_status' => IntegerStatus::pending,
'integer_status_array' => [IntegerStatus::pending, IntegerStatus::done],
'arrayable_status' => ArrayableStatus::pending,
]);

$this->assertEquals([
'string_status' => 'pending',
'string_status_array' => json_encode(['pending', 'done']),
'integer_status' => 1,
'integer_status_array' => json_encode([1, 2]),
'arrayable_status' => [
'name' => 'pending',
'value' => 'pending',
Expand All @@ -79,13 +93,17 @@ public function testEnumsAreCastableToArrayWhenNull()
{
$model = new EloquentModelEnumCastingTestModel([
'string_status' => null,
'string_status_array' => null,
'integer_status' => null,
'integer_status_array' => null,
'arrayable_status' => null,
]);

$this->assertEquals([
'string_status' => null,
'string_status_array' => null,
'integer_status' => null,
'integer_status_array' => null,
'arrayable_status' => null,
], $model->toArray());
}
Expand All @@ -94,7 +112,9 @@ public function testEnumsAreConvertedOnSave()
{
$model = new EloquentModelEnumCastingTestModel([
'string_status' => StringStatus::pending,
'string_status_array' => [StringStatus::pending, StringStatus::done],
'integer_status' => IntegerStatus::pending,
'integer_status_array' => [IntegerStatus::pending, IntegerStatus::done],
'arrayable_status' => ArrayableStatus::pending,
]);

Expand All @@ -103,7 +123,31 @@ public function testEnumsAreConvertedOnSave()
$this->assertEquals((object) [
'id' => $model->id,
'string_status' => 'pending',
'string_status_array' => json_encode(['pending', 'done']),
'integer_status' => 1,
'integer_status_array' => json_encode([1, 2]),
'arrayable_status' => 'pending',
], DB::table('enum_casts')->where('id', $model->id)->first());
}

public function testEnumsAreNotConvertedOnSaveWhenAlreadyCorrect()
{
$model = new EloquentModelEnumCastingTestModel([
'string_status' => 'pending',
'string_status_array' => ['pending', 'done'],
'integer_status' => 1,
'integer_status_array' => [1, 2],
'arrayable_status' => 'pending',
]);

$model->save();

$this->assertEquals((object) [
'id' => $model->id,
'string_status' => 'pending',
'string_status_array' => json_encode(['pending', 'done']),
'integer_status' => 1,
'integer_status_array' => json_encode([1, 2]),
'arrayable_status' => 'pending',
], DB::table('enum_casts')->where('id', $model->id)->first());
}
Expand All @@ -112,7 +156,9 @@ public function testEnumsAcceptNullOnSave()
{
$model = new EloquentModelEnumCastingTestModel([
'string_status' => null,
'string_status_array' => null,
'integer_status' => null,
'integer_status_array' => null,
'arrayable_status' => null,
]);

Expand All @@ -121,7 +167,9 @@ public function testEnumsAcceptNullOnSave()
$this->assertEquals((object) [
'id' => $model->id,
'string_status' => null,
'string_status_array' => null,
'integer_status' => null,
'integer_status_array' => null,
'arrayable_status' => null,
], DB::table('enum_casts')->where('id', $model->id)->first());
}
Expand Down Expand Up @@ -195,7 +243,9 @@ class EloquentModelEnumCastingTestModel extends Model

public $casts = [
'string_status' => StringStatus::class,
'string_status_array' => StringStatus::class.':array',
'integer_status' => IntegerStatus::class,
'integer_status_array' => IntegerStatus::class.':array',
'arrayable_status' => ArrayableStatus::class,
];
}
2 changes: 2 additions & 0 deletions tests/Integration/Database/Enums.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down

0 comments on commit da40aa8

Please sign in to comment.