diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index e70378c6b078..dabcb7209968 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2,6 +2,7 @@ namespace Illuminate\Database\Eloquent\Concerns; +use Carbon\CarbonImmutable; use Carbon\CarbonInterface; use DateTimeInterface; use Illuminate\Contracts\Database\Eloquent\Castable; @@ -78,6 +79,9 @@ trait HasAttributes 'encrypted:json', 'encrypted:object', 'float', + 'immutable_date', + 'immutable_datetime', + 'immutable_custom_datetime', 'int', 'integer', 'json', @@ -241,11 +245,13 @@ protected function addCastAttributesToArray(array $attributes, array $mutatedAtt // a string. This allows the developers to customize how dates are serialized // into an array without affecting how they are persisted into the storage. if ($attributes[$key] && - ($value === 'date' || $value === 'datetime')) { + ($value === 'date' || $value === 'datetime' || + $value === 'immutable_date' || $value === 'immutable_datetime')) { $attributes[$key] = $this->serializeDate($attributes[$key]); } - if ($attributes[$key] && $this->isCustomDateTimeCast($value)) { + if ($attributes[$key] && ($this->isCustomDateTimeCast($value) || + $this->isImmutableCustomDateTimeCast($value))) { $attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]); } @@ -537,8 +543,8 @@ protected function mutateAttribute($key, $value) protected function mutateAttributeForArray($key, $value) { $value = $this->isClassCastable($key) - ? $this->getClassCastableAttributeValue($key, $value) - : $this->mutateAttribute($key, $value); + ? $this->getClassCastableAttributeValue($key, $value) + : $this->mutateAttribute($key, $value); return $value instanceof Arrayable ? $value->toArray() : $value; } @@ -607,6 +613,11 @@ protected function castAttribute($key, $value) case 'datetime': case 'custom_datetime': return $this->asDateTime($value); + case 'immutable_date': + return $this->asDate($value)->toImmutable(); + case 'immutable_custom_datetime': + case 'immutable_datetime': + return $this->asDateTime($value)->toImmutable(); case 'timestamp': return $this->asTimestamp($value); } @@ -633,8 +644,8 @@ protected function getClassCastableAttributeValue($key, $value) $caster = $this->resolveCasterClass($key); $value = $caster instanceof CastsInboundAttributes - ? $value - : $caster->get($this, $key, $value, $this->attributes); + ? $value + : $caster->get($this, $key, $value, $this->attributes); if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { unset($this->classCastCache[$key]); @@ -658,6 +669,10 @@ protected function getCastType($key) return 'custom_datetime'; } + if ($this->isImmutableCustomDateTimeCast($this->getCasts()[$key])) { + return 'immutable_custom_datetime'; + } + if ($this->isDecimalCast($this->getCasts()[$key])) { return 'decimal'; } @@ -706,6 +721,18 @@ protected function isCustomDateTimeCast($cast) strncmp($cast, 'datetime:', 9) === 0; } + /** + * Determine if the cast type is an immutable custom date time cast. + * + * @param string $cast + * @return bool + */ + protected function isImmutableCustomDateTimeCast($cast) + { + return strncmp($cast, 'immutable_date:', 15) === 0 || + strncmp($cast, 'immutable_datetime:', 19) === 0; + } + /** * Determine if the cast type is a decimal cast. * @@ -798,7 +825,7 @@ protected function setMutatedAttributeValue($key, $value) protected function isDateAttribute($key) { return in_array($key, $this->getDates(), true) || - $this->isDateCastable($key); + $this->isDateCastable($key); } /** @@ -817,8 +844,8 @@ public function fillJsonAttribute($key, $value) )); $this->attributes[$key] = $this->isEncryptedCastable($key) - ? $this->castAttributeAsEncryptedString($key, $value) - : $value; + ? $this->castAttributeAsEncryptedString($key, $value) + : $value; return $this; } @@ -887,8 +914,8 @@ protected function getArrayAttributeByKey($key) return $this->fromJson( $this->isEncryptedCastable($key) - ? $this->fromEncryptedString($this->attributes[$key]) - : $this->attributes[$key] + ? $this->fromEncryptedString($this->attributes[$key]) + : $this->attributes[$key] ); } @@ -1107,7 +1134,9 @@ protected function asTimestamp($value) */ protected function serializeDate(DateTimeInterface $date) { - return Carbon::instance($date)->toJSON(); + return $date instanceof \DateTimeImmutable ? + CarbonImmutable::instance($date)->toJSON() : + Carbon::instance($date)->toJSON(); } /** @@ -1268,7 +1297,7 @@ protected function isClassDeviable($key) protected function isClassSerializable($key) { return $this->isClassCastable($key) && - method_exists($this->parseCasterClass($this->getCasts()[$key]), 'serialize'); + method_exists($this->parseCasterClass($this->getCasts()[$key]), 'serialize'); } /** @@ -1310,8 +1339,8 @@ protected function resolveCasterClass($key) protected function parseCasterClass($class) { return strpos($class, ':') === false - ? $class - : explode(':', $class, 2)[0]; + ? $class + : explode(':', $class, 2)[0]; } /** @@ -1327,8 +1356,8 @@ protected function mergeAttributesFromClassCasts() $this->attributes = array_merge( $this->attributes, $caster instanceof CastsInboundAttributes - ? [$key => $value] - : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes)) + ? [$key => $value] + : $this->normalizeCastClassResponse($key, $caster->set($this, $key, $value, $this->attributes)) ); } } @@ -1618,7 +1647,7 @@ public function originalIsEquivalent($key) return false; } elseif ($this->isDateAttribute($key)) { return $this->fromDateTime($attribute) === - $this->fromDateTime($original); + $this->fromDateTime($original); } elseif ($this->hasCast($key, ['object', 'collection'])) { return $this->castAttribute($key, $attribute) == $this->castAttribute($key, $original); @@ -1630,11 +1659,11 @@ public function originalIsEquivalent($key) return abs($this->castAttribute($key, $attribute) - $this->castAttribute($key, $original)) < PHP_FLOAT_EPSILON * 4; } elseif ($this->hasCast($key, static::$primitiveCastTypes)) { return $this->castAttribute($key, $attribute) === - $this->castAttribute($key, $original); + $this->castAttribute($key, $original); } return is_numeric($attribute) && is_numeric($original) - && strcmp((string) $attribute, (string) $original) === 0; + && strcmp((string) $attribute, (string) $original) === 0; } /** diff --git a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php new file mode 100644 index 000000000000..4568c043b473 --- /dev/null +++ b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php @@ -0,0 +1,76 @@ +increments('id'); + $table->date('date_field')->nullable(); + $table->datetime('datetime_field')->nullable(); + }); + } + + public function testDatesAreImmutableCastable() + { + $model = TestModelImmutable::create([ + 'date_field' => '2019-10-01', + 'datetime_field' => '2019-10-01 10:15:20', + ]); + + $this->assertSame('2019-10-01T00:00:00.000000Z', $model->toArray()['date_field']); + $this->assertSame('2019-10-01T10:15:20.000000Z', $model->toArray()['datetime_field']); + $this->assertInstanceOf(CarbonImmutable::class, $model->date_field); + $this->assertInstanceOf(CarbonImmutable::class, $model->datetime_field); + } + + public function testDatesAreImmutableAndCustomCastable() + { + $model = TestModelCustomImmutable::create([ + 'date_field' => '2019-10-01', + 'datetime_field' => '2019-10-01 10:15:20', + ]); + + $this->assertSame('2019-10', $model->toArray()['date_field']); + $this->assertSame('2019-10 10:15', $model->toArray()['datetime_field']); + $this->assertInstanceOf(CarbonImmutable::class, $model->date_field); + $this->assertInstanceOf(CarbonImmutable::class, $model->datetime_field); + } +} + +class TestModelImmutable extends Model +{ + public $table = 'test_model_immutable'; + public $timestamps = false; + protected $guarded = []; + + public $casts = [ + 'date_field' => 'immutable_date', + 'datetime_field' => 'immutable_datetime', + ]; +} + +class TestModelCustomImmutable extends Model +{ + public $table = 'test_model_immutable'; + public $timestamps = false; + protected $guarded = []; + + public $casts = [ + 'date_field' => 'immutable_date:Y-m', + 'datetime_field' => 'immutable_datetime:Y-m H:i', + ]; +}