Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Allow model attributes to be casted to/from an Enum #39315

Merged
merged 13 commits into from
Oct 29, 2021
73 changes: 72 additions & 1 deletion src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
*/
themsaid marked this conversation as resolved.
Show resolved Hide resolved
protected function getEnumCastableAttributeValue($key, $value)
{
$castType = $this->getCasts()[$key];

return $castType::from($value);
taylorotwell marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Get the type of cast for a model attribute.
*
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -885,6 +913,21 @@ function () {
}
}

/**
* Set the value of a enum castable attribute.
*
* @param string $key
* @param mixed $value
themsaid marked this conversation as resolved.
Show resolved Hide resolved
* @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.
*
Expand Down Expand Up @@ -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;
}
themsaid marked this conversation as resolved.
Show resolved Hide resolved

throw new InvalidCastException($this->getModel(), $key, $castType);
}

/**
* Determine if the key is deviable using a custom class.
*
Expand All @@ -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');
}

Expand Down
100 changes: 100 additions & 0 deletions tests/Integration/Database/EloquentModelEnumCastingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<?php

namespace Illuminate\Tests\Integration\Database;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;

if (strpos(PHP_VERSION, '8.1') === 0) {
driesvints marked this conversation as resolved.
Show resolved Hide resolved
include 'Enums.php';
}

/**
* @group integration
*/
class EloquentModelEnumCastingTest extends DatabaseTestCase
{
protected function setUp(): void
{
parent::setUp();

Schema::create('enum_casts', function (Blueprint $table) {
$table->increments('id');
$table->string('string_status', 100)->nullable();
$table->integer('integer_status')->nullable();
});
}

/**
* @requires PHP 8.1
*/
driesvints marked this conversation as resolved.
Show resolved Hide resolved
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);

}

/**
* @requires PHP 8.1
*/
public function testEnumsAreCastableToArray()
{
$model = new EloquentModelEnumCastingTestModel([
'string_status' => StringStatus::pending,
'integer_status' => IntegerStatus::pending,
]);

$this->assertEquals([
'string_status' => 'pending',
'integer_status' => 1,
], $model->toArray());
}

/**
* @requires PHP 8.1
*/
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
*/
themsaid marked this conversation as resolved.
Show resolved Hide resolved
class EloquentModelEnumCastingTestModel extends Model
{
public $timestamps = false;
protected $guarded = [];
protected $table = 'enum_casts';

public $casts = [
'string_status' => StringStatus::class,
'integer_status' => IntegerStatus::class,
];
}
15 changes: 15 additions & 0 deletions tests/Integration/Database/Enums.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Illuminate\Tests\Integration\Database;

enum StringStatus: string
{
case pending = 'pending';
case done = 'done';
}

enum IntegerStatus: int
{
case pending = 1;
case done = 2;
}