From 12bdab0b193ef405cb1afd918c937c34ab2d121b Mon Sep 17 00:00:00 2001 From: Adam Campbell Date: Sun, 1 Aug 2021 09:31:52 -0500 Subject: [PATCH 1/3] Add tests --- .../Eloquent/Concerns/HasAttributes.php | 117 +++++++++++------- .../EloquentModelImmutableDateCastingTest.php | 76 ++++++++++++ 2 files changed, 148 insertions(+), 45 deletions(-) create mode 100644 tests/Integration/Database/EloquentModelImmutableDateCastingTest.php diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index e70378c6b078..3c1f599f4afb 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', @@ -175,7 +179,7 @@ public function attributesToArray() protected function addDateAttributesToArray(array $attributes) { foreach ($this->getDates() as $key) { - if (! isset($attributes[$key])) { + if (!isset($attributes[$key])) { continue; } @@ -200,7 +204,7 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated // We want to spin through all the mutated attributes for this model and call // the mutator for the attribute. We cache off every mutated attributes so // we don't have to constantly check on attributes that actually change. - if (! array_key_exists($key, $attributes)) { + if (!array_key_exists($key, $attributes)) { continue; } @@ -225,7 +229,7 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) { foreach ($this->getCasts() as $key => $value) { - if (! array_key_exists($key, $attributes) || + if (!array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) { continue; } @@ -241,11 +245,11 @@ 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]); } @@ -283,7 +287,7 @@ protected function getArrayableAttributes() */ protected function getArrayableAppends() { - if (! count($this->appends)) { + if (!count($this->appends)) { return []; } @@ -373,7 +377,7 @@ protected function getArrayableItems(array $values) */ public function getAttribute($key) { - if (! $key) { + if (!$key) { return; } @@ -434,7 +438,7 @@ public function getRelationValue($key) return $this->relations[$key]; } - if (! $this->isRelation($key)) { + if (!$this->isRelation($key)) { return; } @@ -487,7 +491,7 @@ protected function getRelationshipFromMethod($method) { $relation = $this->$method(); - if (! $relation instanceof Relation) { + if (!$relation instanceof Relation) { if (is_null($relation)) { throw new LogicException(sprintf( '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method @@ -537,8 +541,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; } @@ -583,7 +587,7 @@ protected function castAttribute($key, $value) switch ($castType) { case 'int': case 'integer': - return (int) $value; + return (int)$value; case 'real': case 'float': case 'double': @@ -591,10 +595,10 @@ protected function castAttribute($key, $value) case 'decimal': return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]); case 'string': - return (string) $value; + return (string)$value; case 'bool': case 'boolean': - return (bool) $value; + return (bool)$value; case 'object': return $this->fromJson($value, true); case 'array': @@ -607,6 +611,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,10 +642,10 @@ 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)) { + if ($caster instanceof CastsInboundAttributes || !is_object($value)) { unset($this->classCastCache[$key]); } else { $this->classCastCache[$key] = $value; @@ -658,6 +667,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 +719,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. * @@ -746,7 +771,7 @@ public function setAttribute($key, $value) return $this; } - if (! is_null($value) && $this->isJsonCastable($key)) { + if (!is_null($value) && $this->isJsonCastable($key)) { $value = $this->castAttributeAsJson($key, $value); } @@ -757,7 +782,7 @@ public function setAttribute($key, $value) return $this->fillJsonAttribute($key, $value); } - if (! is_null($value) && $this->isEncryptedCastable($key)) { + if (!is_null($value) && $this->isEncryptedCastable($key)) { $value = $this->castAttributeAsEncryptedString($key, $value); } @@ -798,7 +823,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 +842,8 @@ public function fillJsonAttribute($key, $value) )); $this->attributes[$key] = $this->isEncryptedCastable($key) - ? $this->castAttributeAsEncryptedString($key, $value) - : $value; + ? $this->castAttributeAsEncryptedString($key, $value) + : $value; return $this; } @@ -851,7 +876,7 @@ function () { ); } - if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { + if ($caster instanceof CastsInboundAttributes || !is_object($value)) { unset($this->classCastCache[$key]); } else { $this->classCastCache[$key] = $value; @@ -881,14 +906,14 @@ protected function getArrayAttributeWithValue($path, $key, $value) */ protected function getArrayAttributeByKey($key) { - if (! isset($this->attributes[$key])) { + if (!isset($this->attributes[$key])) { return []; } return $this->fromJson( $this->isEncryptedCastable($key) - ? $this->fromEncryptedString($this->attributes[$key]) - : $this->attributes[$key] + ? $this->fromEncryptedString($this->attributes[$key]) + : $this->attributes[$key] ); } @@ -932,7 +957,7 @@ protected function asJson($value) */ public function fromJson($value, $asObject = false) { - return json_decode($value, ! $asObject); + return json_decode($value, !$asObject); } /** @@ -977,7 +1002,7 @@ public static function encryptUsing($encrypter) */ public function fromFloat($value) { - switch ((string) $value) { + switch ((string)$value) { case 'Infinity': return INF; case '-Infinity': @@ -985,7 +1010,7 @@ public function fromFloat($value) case 'NaN': return NAN; default: - return (float) $value; + return (float)$value; } } @@ -1107,7 +1132,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(); } /** @@ -1117,7 +1144,7 @@ protected function serializeDate(DateTimeInterface $date) */ public function getDates() { - if (! $this->usesTimestamps()) { + if (!$this->usesTimestamps()) { return $this->dates; } @@ -1162,7 +1189,7 @@ public function setDateFormat($format) public function hasCast($key, $types = null) { if (array_key_exists($key, $this->getCasts())) { - return $types ? in_array($this->getCastType($key), (array) $types, true) : true; + return $types ? in_array($this->getCastType($key), (array)$types, true) : true; } return false; @@ -1225,7 +1252,7 @@ protected function isEncryptedCastable($key) */ protected function isClassCastable($key) { - if (! array_key_exists($key, $this->getCasts())) { + if (!array_key_exists($key, $this->getCasts())) { return false; } @@ -1268,7 +1295,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 +1337,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 +1354,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)) ); } } @@ -1525,7 +1552,7 @@ public function isDirty($attributes = null) */ public function isClean($attributes = null) { - return ! $this->isDirty(...func_get_args()); + return !$this->isDirty(...func_get_args()); } /** @@ -1579,7 +1606,7 @@ public function getDirty() $dirty = []; foreach ($this->getAttributes() as $key => $value) { - if (! $this->originalIsEquivalent($key)) { + if (!$this->originalIsEquivalent($key)) { $dirty[$key] = $value; } } @@ -1605,7 +1632,7 @@ public function getChanges() */ public function originalIsEquivalent($key) { - if (! array_key_exists($key, $this->original)) { + if (!array_key_exists($key, $this->original)) { return false; } @@ -1618,7 +1645,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 +1657,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; } /** @@ -1719,7 +1746,7 @@ public function getMutatedAttributes() { $class = static::class; - if (! isset(static::$mutatorCache[$class])) { + if (!isset(static::$mutatorCache[$class])) { static::cacheMutatedAttributes($class); } diff --git a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php new file mode 100644 index 000000000000..c16987d2dd13 --- /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', + ]; +} \ No newline at end of file From 25023cb21ad44fb1852d6039931cd640f142cf60 Mon Sep 17 00:00:00 2001 From: Adam Campbell Date: Sun, 1 Aug 2021 09:38:42 -0500 Subject: [PATCH 2/3] Fix styling --- .../Eloquent/Concerns/HasAttributes.php | 52 +++++++++---------- .../EloquentModelImmutableDateCastingTest.php | 2 +- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index 3c1f599f4afb..a7e4e22000d4 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -179,7 +179,7 @@ public function attributesToArray() protected function addDateAttributesToArray(array $attributes) { foreach ($this->getDates() as $key) { - if (!isset($attributes[$key])) { + if (! isset($attributes[$key])) { continue; } @@ -204,7 +204,7 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated // We want to spin through all the mutated attributes for this model and call // the mutator for the attribute. We cache off every mutated attributes so // we don't have to constantly check on attributes that actually change. - if (!array_key_exists($key, $attributes)) { + if (! array_key_exists($key, $attributes)) { continue; } @@ -229,7 +229,7 @@ protected function addMutatedAttributesToArray(array $attributes, array $mutated protected function addCastAttributesToArray(array $attributes, array $mutatedAttributes) { foreach ($this->getCasts() as $key => $value) { - if (!array_key_exists($key, $attributes) || + if (! array_key_exists($key, $attributes) || in_array($key, $mutatedAttributes)) { continue; } @@ -287,7 +287,7 @@ protected function getArrayableAttributes() */ protected function getArrayableAppends() { - if (!count($this->appends)) { + if (! count($this->appends)) { return []; } @@ -377,7 +377,7 @@ protected function getArrayableItems(array $values) */ public function getAttribute($key) { - if (!$key) { + if (! $key) { return; } @@ -438,7 +438,7 @@ public function getRelationValue($key) return $this->relations[$key]; } - if (!$this->isRelation($key)) { + if (! $this->isRelation($key)) { return; } @@ -491,7 +491,7 @@ protected function getRelationshipFromMethod($method) { $relation = $this->$method(); - if (!$relation instanceof Relation) { + if (! $relation instanceof Relation) { if (is_null($relation)) { throw new LogicException(sprintf( '%s::%s must return a relationship instance, but "null" was returned. Was the "return" keyword used?', static::class, $method @@ -587,7 +587,7 @@ protected function castAttribute($key, $value) switch ($castType) { case 'int': case 'integer': - return (int)$value; + return (int) $value; case 'real': case 'float': case 'double': @@ -595,10 +595,10 @@ protected function castAttribute($key, $value) case 'decimal': return $this->asDecimal($value, explode(':', $this->getCasts()[$key], 2)[1]); case 'string': - return (string)$value; + return (string) $value; case 'bool': case 'boolean': - return (bool)$value; + return (bool) $value; case 'object': return $this->fromJson($value, true); case 'array': @@ -645,7 +645,7 @@ protected function getClassCastableAttributeValue($key, $value) ? $value : $caster->get($this, $key, $value, $this->attributes); - if ($caster instanceof CastsInboundAttributes || !is_object($value)) { + if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { unset($this->classCastCache[$key]); } else { $this->classCastCache[$key] = $value; @@ -771,7 +771,7 @@ public function setAttribute($key, $value) return $this; } - if (!is_null($value) && $this->isJsonCastable($key)) { + if (! is_null($value) && $this->isJsonCastable($key)) { $value = $this->castAttributeAsJson($key, $value); } @@ -782,7 +782,7 @@ public function setAttribute($key, $value) return $this->fillJsonAttribute($key, $value); } - if (!is_null($value) && $this->isEncryptedCastable($key)) { + if (! is_null($value) && $this->isEncryptedCastable($key)) { $value = $this->castAttributeAsEncryptedString($key, $value); } @@ -876,7 +876,7 @@ function () { ); } - if ($caster instanceof CastsInboundAttributes || !is_object($value)) { + if ($caster instanceof CastsInboundAttributes || ! is_object($value)) { unset($this->classCastCache[$key]); } else { $this->classCastCache[$key] = $value; @@ -906,7 +906,7 @@ protected function getArrayAttributeWithValue($path, $key, $value) */ protected function getArrayAttributeByKey($key) { - if (!isset($this->attributes[$key])) { + if (! isset($this->attributes[$key])) { return []; } @@ -957,7 +957,7 @@ protected function asJson($value) */ public function fromJson($value, $asObject = false) { - return json_decode($value, !$asObject); + return json_decode($value, ! $asObject); } /** @@ -1002,7 +1002,7 @@ public static function encryptUsing($encrypter) */ public function fromFloat($value) { - switch ((string)$value) { + switch ((string) $value) { case 'Infinity': return INF; case '-Infinity': @@ -1010,7 +1010,7 @@ public function fromFloat($value) case 'NaN': return NAN; default: - return (float)$value; + return (float) $value; } } @@ -1144,7 +1144,7 @@ protected function serializeDate(DateTimeInterface $date) */ public function getDates() { - if (!$this->usesTimestamps()) { + if (! $this->usesTimestamps()) { return $this->dates; } @@ -1189,7 +1189,7 @@ public function setDateFormat($format) public function hasCast($key, $types = null) { if (array_key_exists($key, $this->getCasts())) { - return $types ? in_array($this->getCastType($key), (array)$types, true) : true; + return $types ? in_array($this->getCastType($key), (array) $types, true) : true; } return false; @@ -1252,7 +1252,7 @@ protected function isEncryptedCastable($key) */ protected function isClassCastable($key) { - if (!array_key_exists($key, $this->getCasts())) { + if (! array_key_exists($key, $this->getCasts())) { return false; } @@ -1552,7 +1552,7 @@ public function isDirty($attributes = null) */ public function isClean($attributes = null) { - return !$this->isDirty(...func_get_args()); + return ! $this->isDirty(...func_get_args()); } /** @@ -1606,7 +1606,7 @@ public function getDirty() $dirty = []; foreach ($this->getAttributes() as $key => $value) { - if (!$this->originalIsEquivalent($key)) { + if (! $this->originalIsEquivalent($key)) { $dirty[$key] = $value; } } @@ -1632,7 +1632,7 @@ public function getChanges() */ public function originalIsEquivalent($key) { - if (!array_key_exists($key, $this->original)) { + if (! array_key_exists($key, $this->original)) { return false; } @@ -1661,7 +1661,7 @@ public function originalIsEquivalent($key) } return is_numeric($attribute) && is_numeric($original) - && strcmp((string)$attribute, (string)$original) === 0; + && strcmp((string) $attribute, (string) $original) === 0; } /** @@ -1746,7 +1746,7 @@ public function getMutatedAttributes() { $class = static::class; - if (!isset(static::$mutatorCache[$class])) { + if (! isset(static::$mutatorCache[$class])) { static::cacheMutatedAttributes($class); } diff --git a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php index c16987d2dd13..4568c043b473 100644 --- a/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php +++ b/tests/Integration/Database/EloquentModelImmutableDateCastingTest.php @@ -73,4 +73,4 @@ class TestModelCustomImmutable extends Model 'date_field' => 'immutable_date:Y-m', 'datetime_field' => 'immutable_datetime:Y-m H:i', ]; -} \ No newline at end of file +} From ca2a73c3c5a860a360de7b2ff6d70a31f0f2d93b Mon Sep 17 00:00:00 2001 From: Taylor Otwell Date: Sun, 1 Aug 2021 10:16:13 -0500 Subject: [PATCH 3/3] formatting --- src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php index a7e4e22000d4..dabcb7209968 100644 --- a/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/src/Illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -245,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 === 'immutable_date' || $value === 'immutable_datetime')) { + ($value === 'date' || $value === 'datetime' || + $value === 'immutable_date' || $value === 'immutable_datetime')) { $attributes[$key] = $this->serializeDate($attributes[$key]); } - if ($attributes[$key] && ($this->isCustomDateTimeCast($value) || $this->isImmutableCustomDateTimeCast($value))) { + if ($attributes[$key] && ($this->isCustomDateTimeCast($value) || + $this->isImmutableCustomDateTimeCast($value))) { $attributes[$key] = $attributes[$key]->format(explode(':', $value, 2)[1]); }