Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 49 additions & 20 deletions src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Database\Eloquent\Concerns;

use Carbon\CarbonImmutable;
use Carbon\CarbonInterface;
use DateTimeInterface;
use Illuminate\Contracts\Database\Eloquent\Castable;
Expand Down Expand Up @@ -78,6 +79,9 @@ trait HasAttributes
'encrypted:json',
'encrypted:object',
'float',
'immutable_date',
'immutable_datetime',
'immutable_custom_datetime',
'int',
'integer',
'json',
Expand Down Expand Up @@ -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]);
}

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand All @@ -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]);
Expand All @@ -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';
}
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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;
}
Expand Down Expand Up @@ -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]
);
}

Expand Down Expand Up @@ -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();
}

/**
Expand Down Expand Up @@ -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');
}

/**
Expand Down Expand Up @@ -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];
}

/**
Expand All @@ -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))
);
}
}
Expand Down Expand Up @@ -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);
Expand All @@ -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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Illuminate\Tests\Integration\Database\EloquentModelDateCastingTest;

use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Tests\Integration\Database\DatabaseTestCase;

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

Schema::create('test_model_immutable', function (Blueprint $table) {
$table->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',
];
}