From 8f9745dd25307eb3caae453142252c77faa5e6c9 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 10:02:02 +0200 Subject: [PATCH 01/33] POC for https://github.com/laravel/framework/discussions/31778 --- .../Database/Console/ShowModelCommand.php | 2 +- illuminate/Database/Eloquent/Builder.php | 2 +- .../Eloquent/Concerns/HasAttributes.php | 42 +++++++++++++------ illuminate/Database/Eloquent/Model.php | 10 ++++- .../Eloquent/Relations/BelongsToMany.php | 4 +- 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/illuminate/Database/Console/ShowModelCommand.php b/illuminate/Database/Console/ShowModelCommand.php index 93606cf..afe7e3d 100644 --- a/illuminate/Database/Console/ShowModelCommand.php +++ b/illuminate/Database/Console/ShowModelCommand.php @@ -465,7 +465,7 @@ protected function getColumnType($column) */ protected function getColumnDefault($column, $model) { - $attributeDefault = $model->getAttributes()[$column->getName()] ?? null; + $attributeDefault = $model->getAttributeFromArray($column->getName()); return match (true) { $attributeDefault instanceof BackedEnum => $attributeDefault->value, diff --git a/illuminate/Database/Eloquent/Builder.php b/illuminate/Database/Eloquent/Builder.php index 38d5ccf..aaf00ab 100755 --- a/illuminate/Database/Eloquent/Builder.php +++ b/illuminate/Database/Eloquent/Builder.php @@ -1190,7 +1190,7 @@ protected function addUpdatedAtColumn(array $values) ) { $timestamp = $this->model->newInstance() ->forceFill([$column => $timestamp]) - ->getAttributes()[$column] ?? $timestamp; + ->getAttributeFromArray($column) ?? $timestamp; } $values = array_merge([$column => $timestamp], $values); diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 71cf01b..444c9d7 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -507,14 +507,18 @@ public function getAttributeValue($key) } /** - * Get an attribute from the $attributes array. + * Get an (casted) attribute from the $attributes array without transformation + * @see self::getAttributeValue * * @param string $key * @return mixed */ protected function getAttributeFromArray($key) { - return $this->getAttributes()[$key] ?? null; + $this->mergeAttributesFromClassCasts($key); + $this->mergeAttributesFromAttributeCasts($key); + + return $this->attributes[$key] ?? null; } /** @@ -1798,12 +1802,18 @@ protected function mergeAttributesFromCachedCasts() /** * Merge the cast class attributes back into the model. - * + * @param string|array $keys * @return void */ protected function mergeAttributesFromClassCasts() { - foreach ($this->classCastCache as $key => $value) { + $k = \func_get_args()[0] ?? null; + + $classCastCache = \is_string($k) || \is_array($k) ? + \array_intersect_key($this->classCastCache, \array_flip(\array_values((array)$k))) : + $this->classCastCache; + + foreach ($classCastCache as $key => $value) { $caster = $this->resolveCasterClass($key); $this->attributes = array_merge( @@ -1817,12 +1827,18 @@ protected function mergeAttributesFromClassCasts() /** * Merge the cast class attributes back into the model. - * + * @param string|array $keys * @return void */ protected function mergeAttributesFromAttributeCasts() { - foreach ($this->attributeCastCache as $key => $value) { + $k = \func_get_args()[0] ?? null; + + $attributeCastCache = \is_string($k) || \is_array($k) ? + \array_intersect_key($this->attributeCastCache, \array_flip(\array_values((array)$k))) : + $this->attributeCastCache; + + foreach ($attributeCastCache as $key => $value) { $attribute = $this->{Str::camel($key)}(); if ($attribute->get && !$attribute->set) { @@ -1857,12 +1873,14 @@ protected function normalizeCastClassResponse($key, $value) /** * Get all of the current attributes on the model. - * + * @param bool $withoutCasting * @return array */ public function getAttributes() { - $this->mergeAttributesFromCachedCasts(); + if (true !== \func_get_arg(0)) { + $this->mergeAttributesFromCachedCasts(); + } return $this->attributes; } @@ -1970,7 +1988,7 @@ public function only($attributes) */ public function syncOriginal() { - $this->original = $this->getAttributes(); + $this->original = $this->getAttributes(true); return $this; } @@ -1996,10 +2014,8 @@ public function syncOriginalAttributes($attributes) { $attributes = is_array($attributes) ? $attributes : func_get_args(); - $modelAttributes = $this->getAttributes(); - foreach ($attributes as $attribute) { - $this->original[$attribute] = $modelAttributes[$attribute]; + $this->original[$attribute] = $this->getAttributeFromArray($attributes); } return $this; @@ -2105,7 +2121,7 @@ public function getDirty() { $dirty = []; - foreach ($this->getAttributes() as $key => $value) { + foreach ($this->attributes as $key => $value) { if (!$this->originalIsEquivalent($key)) { $dirty[$key] = $value; } diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index fe2f4e8..5666df8 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1754,7 +1754,7 @@ public function replicate(?array $except = null) ])); $attributes = Arr::except( - $this->getAttributes(), + $this->getAttributes(true), $except ? array_unique(array_merge($except, $defaults)) : $defaults ); @@ -2356,7 +2356,13 @@ public function __call($method, $parameters) { $lowerMethod = \strtolower($method); - if (\in_array($lowerMethod, ['increment', 'decrement', 'incrementquietly', 'decrementquietly'], true)) { + if ( + \in_array( + $lowerMethod, + ['increment', 'decrement', 'incrementquietly', 'decrementquietly', 'getAttributeFromArray'], + true + ) + ) { return $this->$method(...$parameters); } diff --git a/illuminate/Database/Eloquent/Relations/BelongsToMany.php b/illuminate/Database/Eloquent/Relations/BelongsToMany.php index 8728177..2ca1e7b 100755 --- a/illuminate/Database/Eloquent/Relations/BelongsToMany.php +++ b/illuminate/Database/Eloquent/Relations/BelongsToMany.php @@ -1211,12 +1211,12 @@ protected function migratePivotAttributes(Model $model) { $values = []; - foreach ($model->getAttributes() as $key => $value) { + foreach (\array_keys($model->getAttributes(true)) as $key) { // To get the pivots attributes we will just take any of the attributes which // begin with "pivot_" and add those to this arrays, as well as unsetting // them from the parent's models since they exist in a different table. if (str_starts_with($key, 'pivot_')) { - $values[substr($key, 6)] = $value; + $values[substr($key, 6)] = $model->getAttributeFromArray($key); unset($model->$key); } From 3004879101f7d54dded0afa6a8b475c536e13d17 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 10:10:50 +0200 Subject: [PATCH 02/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 6 +++--- illuminate/Database/Eloquent/Model.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 444c9d7..1db35a5 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1988,7 +1988,7 @@ public function only($attributes) */ public function syncOriginal() { - $this->original = $this->getAttributes(true); + $this->original = $this->getAttributes(true); // should original contain casted values? return $this; } @@ -2015,7 +2015,7 @@ public function syncOriginalAttributes($attributes) $attributes = is_array($attributes) ? $attributes : func_get_args(); foreach ($attributes as $attribute) { - $this->original[$attribute] = $this->getAttributeFromArray($attributes); + $this->original[$attribute] = $this->getAttributeFromArray($attribute);//should $original get casted values? } return $this; @@ -2122,7 +2122,7 @@ public function getDirty() $dirty = []; foreach ($this->attributes as $key => $value) { - if (!$this->originalIsEquivalent($key)) { + if (!$this->originalIsEquivalent($key)) { // will cast $dirty[$key] = $value; } } diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 5666df8..f0623c0 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1754,7 +1754,7 @@ public function replicate(?array $except = null) ])); $attributes = Arr::except( - $this->getAttributes(true), + $this->getAttributes(true), // because setRaqAttributes is called below $except ? array_unique(array_merge($except, $defaults)) : $defaults ); From 161cb7d7cba5dbd020bf1a0fb2996477d5333d5a Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 10:39:52 +0200 Subject: [PATCH 03/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 1db35a5..52695d5 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1983,12 +1983,13 @@ public function only($attributes) /** * Sync the original attributes with the current. - * + * $original should hold raw not casted values + * @see self::discardChanges * @return $this */ public function syncOriginal() { - $this->original = $this->getAttributes(true); // should original contain casted values? + $this->original = $this->getAttributes(true); return $this; } @@ -2014,8 +2015,10 @@ public function syncOriginalAttributes($attributes) { $attributes = is_array($attributes) ? $attributes : func_get_args(); + $modelAttributes = $this->getAttributes(true); + foreach ($attributes as $attribute) { - $this->original[$attribute] = $this->getAttributeFromArray($attribute);//should $original get casted values? + $this->original[$attribute] = $modelAttributes[$attribute]; } return $this; From bfc79eac1c4a2f7dfff9bc7eea7b01710dbf97de Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 23:08:39 +0200 Subject: [PATCH 04/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 6 ++---- illuminate/Database/Eloquent/Model.php | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 52695d5..4aef2e7 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1989,7 +1989,7 @@ public function only($attributes) */ public function syncOriginal() { - $this->original = $this->getAttributes(true); + $this->original = $this->getAttributes(); return $this; } @@ -2015,10 +2015,8 @@ public function syncOriginalAttributes($attributes) { $attributes = is_array($attributes) ? $attributes : func_get_args(); - $modelAttributes = $this->getAttributes(true); - foreach ($attributes as $attribute) { - $this->original[$attribute] = $modelAttributes[$attribute]; + $this->original[$attribute] = $this->getAttributeFromArray($attribute); } return $this; diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index f0623c0..1e40fae 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1754,7 +1754,7 @@ public function replicate(?array $except = null) ])); $attributes = Arr::except( - $this->getAttributes(true), // because setRaqAttributes is called below + $this->getAttributes(), $except ? array_unique(array_merge($except, $defaults)) : $defaults ); From 7ac9d61b445efd9335ea0445c6de9596863df9dc Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 23:09:55 +0200 Subject: [PATCH 05/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 4aef2e7..621f5fc 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1983,8 +1983,7 @@ public function only($attributes) /** * Sync the original attributes with the current. - * $original should hold raw not casted values - * @see self::discardChanges + * * @return $this */ public function syncOriginal() From 6917fbd618c03dba05db16a15e6d4217bdd367fe Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 23:11:38 +0200 Subject: [PATCH 06/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 621f5fc..6efd1bf 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2121,8 +2121,8 @@ public function getDirty() { $dirty = []; - foreach ($this->attributes as $key => $value) { - if (!$this->originalIsEquivalent($key)) { // will cast + foreach ($this->getAttributes() as $key => $value) { + if (!$this->originalIsEquivalent($key)) { $dirty[$key] = $value; } } From 89725c0d0f23c161b759301370e400db1028aa72 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sat, 1 Nov 2025 23:20:27 +0200 Subject: [PATCH 07/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 6efd1bf..1f75557 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -507,7 +507,7 @@ public function getAttributeValue($key) } /** - * Get an (casted) attribute from the $attributes array without transformation + * Get an attribute from the $attributes array without transformation * @see self::getAttributeValue * * @param string $key From b2fc1466894f3018a450aece000e73c1d53a2c79 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 00:29:40 +0200 Subject: [PATCH 08/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 1e40fae..c849304 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -2359,7 +2359,7 @@ public function __call($method, $parameters) if ( \in_array( $lowerMethod, - ['increment', 'decrement', 'incrementquietly', 'decrementquietly', 'getAttributeFromArray'], + ['increment', 'decrement', 'incrementquietly', 'decrementquietly', 'getattributefromarray'], true ) ) { From aeeb541305e48eaea891a17758866c405a2c9513 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 00:46:44 +0200 Subject: [PATCH 09/33] POC for https://github.com/laravel/framework/discussions/31778 --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 1f75557..23d04a8 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1878,7 +1878,7 @@ protected function normalizeCastClassResponse($key, $value) */ public function getAttributes() { - if (true !== \func_get_arg(0)) { + if (true !== (\func_get_args()[0] ?? null)) { $this->mergeAttributesFromCachedCasts(); } From fd6a5b25f29217a2a3f11230530a9c92daea71d7 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 10:34:42 +0200 Subject: [PATCH 10/33] POC for https://github.com/laravel/framework/discussions/31778 improve getDirty calls https://github.com/laravel/framework/pull/57627 --- .../Eloquent/Concerns/HasAttributes.php | 28 +++++++++++++++---- illuminate/Database/Eloquent/Model.php | 8 ++++-- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 23d04a8..26cb280 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -64,6 +64,12 @@ trait HasAttributes */ protected $changes = []; + /** + * Temporary cache to avoid multiple getDirty calls generating multiple set calls for + * sync/merge casted attributes to objects to persist the possible changes made to those objects + */ + protected ?array $tmpDirtyCache = null; + /** * The attributes that should be cast. * @@ -2041,10 +2047,7 @@ public function syncChanges() */ public function isDirty($attributes = null) { - return $this->hasChanges( - $this->getDirty(), - is_array($attributes) ? $attributes : func_get_args() - ); + return [] !== $this->getDirty(\is_array($attributes) ? $attributes : \func_get_args()); } /** @@ -2114,14 +2117,27 @@ protected function hasChanges($changes, $attributes = null) /** * Get the attributes that have been changed since the last sync. - * + * @param string|array $attributes * @return array */ public function getDirty() { + if (isset($this->tmpDirtyCache)) { + if ([] !== $args = \func_get_args()) { + return \array_intersect_key($this->tmpDirtyCache, \array_flip((array)$args[0])); + } + + return $this->tmpDirtyCache; + } + + $attributes = (array)(\func_get_args()[0] ?? \array_keys($this->attributes)); + $dirty = []; - foreach ($this->getAttributes() as $key => $value) { + foreach ($attributes as $key) { + // this will merge/sync before the if condition + $value = $this->getAttributeFromArray($key); + if (!$this->originalIsEquivalent($key)) { $dirty[$key] = $value; } diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index c849304..14caf48 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -995,7 +995,7 @@ function () use ($column) { $this->fireModelEvent('updated', false); - $this->syncOriginalAttribute($column); + $this->syncOriginalAttributes(\array_keys($this->changes)); } ); } @@ -1232,12 +1232,14 @@ protected function performUpdate(Builder $query) // Once we have run the update operation, we will fire the "updated" event for // this model instance. This will allow developers to hook into these after // models are updated, giving them a chance to do any special processing. + // this is needed because updating event might change the model $dirty = $this->getDirtyForUpdate(); - if (count($dirty) > 0) { + if ([] !== $dirty) { $this->setKeysForSaveQuery($query)->update($dirty); - + $this->tmpDirtyCache = $dirty; $this->syncChanges(); + unset($this->tmpDirtyCache); $this->fireModelEvent('updated', false); } From 4dc2d62c772cfe105545f602e082c818f8c12dab Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 17:52:45 +0200 Subject: [PATCH 11/33] POC for https://github.com/laravel/framework/discussions/31778 bullet proofing --- .../Database/Eloquent/Concerns/HasAttributes.php | 9 ++++++--- illuminate/Database/Eloquent/Model.php | 14 +++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 26cb280..40a7cd2 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2122,15 +2122,18 @@ protected function hasChanges($changes, $attributes = null) */ public function getDirty() { + $args = \func_get_args(); + $attributes = (array)($args[0] ?? []); + if (isset($this->tmpDirtyCache)) { - if ([] !== $args = \func_get_args()) { - return \array_intersect_key($this->tmpDirtyCache, \array_flip((array)$args[0])); + if ([] !== $attributes) { + return \array_intersect_key($this->tmpDirtyCache, \array_flip($attributes)); } return $this->tmpDirtyCache; } - $attributes = (array)(\func_get_args()[0] ?? \array_keys($this->attributes)); + $attributes = [] !== $attributes ? $attributes : \array_keys($this->attributes); $dirty = []; diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 14caf48..7e6d1c3 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1237,9 +1237,17 @@ protected function performUpdate(Builder $query) if ([] !== $dirty) { $this->setKeysForSaveQuery($query)->update($dirty); - $this->tmpDirtyCache = $dirty; - $this->syncChanges(); - unset($this->tmpDirtyCache); + + try { + $this->tmpDirtyCache = $dirty; + $this->syncChanges(); + } catch (\Throwable $e) { + unset($this->tmpDirtyCache); + + throw $e; + } finally { + unset($this->tmpDirtyCache); + } $this->fireModelEvent('updated', false); } From ae40dadd2dd41a60b59df2d8302cfd206a12ca81 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 17:55:01 +0200 Subject: [PATCH 12/33] POC for https://github.com/laravel/framework/discussions/31778 bullet proofing --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 40a7cd2..bd54d04 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2126,11 +2126,9 @@ public function getDirty() $attributes = (array)($args[0] ?? []); if (isset($this->tmpDirtyCache)) { - if ([] !== $attributes) { - return \array_intersect_key($this->tmpDirtyCache, \array_flip($attributes)); - } - - return $this->tmpDirtyCache; + return [] !== $attributes ? + \array_intersect_key($this->tmpDirtyCache, \array_flip($attributes)) : + $this->tmpDirtyCache; } $attributes = [] !== $attributes ? $attributes : \array_keys($this->attributes); From 9f3dbd9a9daa925c99d8e2e16c59fac6df4c629f Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 18:01:56 +0200 Subject: [PATCH 13/33] POC for https://github.com/laravel/framework/discussions/31778 bullet proofing --- illuminate/Database/Eloquent/Model.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 7e6d1c3..d6c0176 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1241,10 +1241,6 @@ protected function performUpdate(Builder $query) try { $this->tmpDirtyCache = $dirty; $this->syncChanges(); - } catch (\Throwable $e) { - unset($this->tmpDirtyCache); - - throw $e; } finally { unset($this->tmpDirtyCache); } From e446b2ee099e97ca34ccfc991f1e1bb7fd99fc21 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Sun, 2 Nov 2025 23:45:13 +0200 Subject: [PATCH 14/33] POC for https://github.com/laravel/framework/discussions/31778 avoid isDirty call from finishSave --- illuminate/Database/Eloquent/Model.php | 45 +++++++++++++++----------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index d6c0176..4c5f80d 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1152,29 +1152,36 @@ public function save(array $options = []) // that is already in this database using the current IDs in this "where" // clause to only update this model. Otherwise, we'll just insert them. if ($this->exists) { - $saved = $this->isDirty() ? $this->performUpdate($query) : null; - } else { - // If the model is brand new, we'll insert it into our database and set the - // ID attribute on the model to the value of the newly inserted row's ID - // which is typically an auto-increment value managed by the database. - $saved = $this->performInsert($query); - - if ( - '' === (string)$this->getConnectionName() && - ($connection = $query->getConnection()) instanceof Connection - ) { - $this->setConnection($connection->getName()); + if (!$this->isDirty()) { + return true; } + + if ($this->performUpdate($query)) { + $this->finishSave($options + ['touch' => true]); + + return true; + } + + return false; + } + + // If the model is brand new, we'll insert it into our database and set the + // ID attribute on the model to the value of the newly inserted row's ID + // which is typically an auto-increment value managed by the database. + $saved = $this->performInsert($query); + + if ( + '' === (string)$this->getConnectionName() && + ($connection = $query->getConnection()) instanceof Connection + ) { + $this->setConnection($connection->getName()); } - // If the model is successfully saved, we need to do a few more things once - // that is done. We will call the "saved" method here to run any actions - // we need to happen after a model gets successfully saved right here. - if (true === $saved) { - $this->finishSave($options); + if ($saved) { + $this->finishSave(['touch' => $this->isDirty()]); } - return $saved ?? true; + return $saved; } /** @@ -1200,7 +1207,7 @@ protected function finishSave(array $options) { $this->fireModelEvent('saved', false); - if ($this->isDirty() && ($options['touch'] ?? true)) { + if ($options['touch'] ?? true) { $this->touchOwners(); } From a7b9faa5a3c0fe3f270e8ef1ad1f99a8be14a0dc Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 09:47:40 +0200 Subject: [PATCH 15/33] POC for https://github.com/laravel/framework/discussions/31778 reduce even more the number of set calls during save --- illuminate/Database/Eloquent/Model.php | 36 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 4c5f80d..8e9472d 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1137,32 +1137,50 @@ public function saveQuietly(array $options = []) */ public function save(array $options = []) { - $this->mergeAttributesFromCachedCasts(); - $query = $this->newModelQuery(); // If the "saving" event returns false we'll bail out of the save and return // false, indicating that the save failed. This provides a chance for any // listeners to cancel save operations if validations fail or whatever. + /** Saving event might change the model so, no point in calling $this->mergeAttributesFromCachedCasts() before */ if ($this->fireModelEvent('saving') === false) { return false; } + /** $this->isDirty() will call $this->mergeAttributesFromCachedCasts() */ + $isDirty = $this->isDirty(); + // If the model already exists in the database we can just update our record // that is already in this database using the current IDs in this "where" // clause to only update this model. Otherwise, we'll just insert them. if ($this->exists) { - if (!$this->isDirty()) { + if (!$isDirty) { + /** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */ return true; } - if ($this->performUpdate($query)) { - $this->finishSave($options + ['touch' => true]); + try { + /** To avoid excessive set calls (https://github.com/laravel/framework/discussions/31778) when: + - calling $this->getDirtyForUpdate() which calls $this->getDirty() after updating event, + - calling $this->finishSave which calls $this->syncOriginal() which calls $this->getAttributes(), + cache must be temporarily cleared */ + $classCastCache = $this->classCastCache; + $attributeCastCache = $this->attributeCastCache; + $this->classCastCache = []; + $this->attributeCastCache = []; + + if ($this->performUpdate($query)) { + $this->finishSave($options + ['touch' => $isDirty]); + + return true; + } - return true; + return false; + } finally { + /** Reset cache by preserving the new cached objects created in updating/updated/saved events */ + $this->classCastCache += $classCastCache; + $this->attributeCastCache += $attributeCastCache; } - - return false; } // If the model is brand new, we'll insert it into our database and set the @@ -1178,7 +1196,7 @@ public function save(array $options = []) } if ($saved) { - $this->finishSave(['touch' => $this->isDirty()]); + $this->finishSave(['touch' => $isDirty]); } return $saved; From 867dc2b3b5fff0fb97fcd31dd0ee0af0c8a36157 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 10:08:31 +0200 Subject: [PATCH 16/33] POC for https://github.com/laravel/framework/discussions/31778 cr --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index bd54d04..de3ce9e 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1879,7 +1879,7 @@ protected function normalizeCastClassResponse($key, $value) /** * Get all of the current attributes on the model. - * @param bool $withoutCasting + * @param bool $withoutMergeAttributesFromCachedCasts * @return array */ public function getAttributes() From 2cbe2a7f63b5961212bec5adead6d722568048a0 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 12:10:38 +0200 Subject: [PATCH 17/33] POC for https://github.com/laravel/framework/discussions/31778 reduce even more the number of set calls during insert also --- .../Eloquent/Concerns/HasAttributes.php | 2 +- illuminate/Database/Eloquent/Model.php | 71 +++++++++---------- 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index de3ce9e..dd79407 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2136,7 +2136,7 @@ public function getDirty() $dirty = []; foreach ($attributes as $key) { - // this will merge/sync before the if condition + /** This will call $this->mergeAttributesFromCachedCasts($key) before the if condition */ $value = $this->getAttributeFromArray($key); if (!$this->originalIsEquivalent($key)) { diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 8e9472d..5b00e78 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1150,24 +1150,22 @@ public function save(array $options = []) /** $this->isDirty() will call $this->mergeAttributesFromCachedCasts() */ $isDirty = $this->isDirty(); - // If the model already exists in the database we can just update our record - // that is already in this database using the current IDs in this "where" - // clause to only update this model. Otherwise, we'll just insert them. - if ($this->exists) { - if (!$isDirty) { - /** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */ - return true; - } - - try { - /** To avoid excessive set calls (https://github.com/laravel/framework/discussions/31778) when: - - calling $this->getDirtyForUpdate() which calls $this->getDirty() after updating event, - - calling $this->finishSave which calls $this->syncOriginal() which calls $this->getAttributes(), - cache must be temporarily cleared */ - $classCastCache = $this->classCastCache; - $attributeCastCache = $this->attributeCastCache; - $this->classCastCache = []; - $this->attributeCastCache = []; + try { + /** To avoid excessive set calls (https://github.com/laravel/framework/discussions/31778) when: + - calling $this->getDirtyForUpdate() which calls $this->getDirty() after updating event, + - calling $this->getAttributesForInsert() which calls $this->getAttributes() after creating event, + - calling $this->finishSave which calls $this->syncOriginal() which calls $this->getAttributes(), + cache must be temporarily cleared */ + $classCastCache = $this->classCastCache; + $attributeCastCache = $this->attributeCastCache; + $this->classCastCache = []; + $this->attributeCastCache = []; + + if ($this->exists) { + if (!$isDirty) { + /** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */ + return true; + } if ($this->performUpdate($query)) { $this->finishSave($options + ['touch' => $isDirty]); @@ -1176,30 +1174,27 @@ public function save(array $options = []) } return false; - } finally { - /** Reset cache by preserving the new cached objects created in updating/updated/saved events */ - $this->classCastCache += $classCastCache; - $this->attributeCastCache += $attributeCastCache; } - } - // If the model is brand new, we'll insert it into our database and set the - // ID attribute on the model to the value of the newly inserted row's ID - // which is typically an auto-increment value managed by the database. - $saved = $this->performInsert($query); + $saved = $this->performInsert($query); - if ( - '' === (string)$this->getConnectionName() && - ($connection = $query->getConnection()) instanceof Connection - ) { - $this->setConnection($connection->getName()); - } + if ( + '' === (string)$this->getConnectionName() && + ($connection = $query->getConnection()) instanceof Connection + ) { + $this->setConnection($connection->getName()); + } - if ($saved) { - $this->finishSave(['touch' => $isDirty]); - } + if ($saved) { + $this->finishSave(['touch' => $isDirty]); + } - return $saved; + return $saved; + } finally { + /** Reset cache by preserving the new cached objects created in updating/updated/created/creating/saved events */ + $this->classCastCache += $classCastCache; + $this->attributeCastCache += $attributeCastCache; + } } /** @@ -1257,7 +1252,7 @@ protected function performUpdate(Builder $query) // Once we have run the update operation, we will fire the "updated" event for // this model instance. This will allow developers to hook into these after // models are updated, giving them a chance to do any special processing. - // this is needed because updating event might change the model + /** This is needed because updating event might change the model */ $dirty = $this->getDirtyForUpdate(); if ([] !== $dirty) { From 5b52d2dec87e3a29de50bf86884e09f71c6e09ae Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 16:35:36 +0200 Subject: [PATCH 18/33] POC for https://github.com/laravel/framework/discussions/31778 handle detached casted objects from $model and revert reduce even more the number of set calls during insert also --- .../Eloquent/Concerns/HasAttributes.php | 10 +-- illuminate/Database/Eloquent/Model.php | 85 ++++++++++--------- 2 files changed, 52 insertions(+), 43 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index dd79407..7b9381b 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -68,7 +68,7 @@ trait HasAttributes * Temporary cache to avoid multiple getDirty calls generating multiple set calls for * sync/merge casted attributes to objects to persist the possible changes made to those objects */ - protected ?array $tmpDirtyCache = null; + protected ?array $tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; /** * The attributes that should be cast. @@ -1994,7 +1994,7 @@ public function only($attributes) */ public function syncOriginal() { - $this->original = $this->getAttributes(); + $this->original = $this->getAttributes(isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts)); return $this; } @@ -2125,10 +2125,10 @@ public function getDirty() $args = \func_get_args(); $attributes = (array)($args[0] ?? []); - if (isset($this->tmpDirtyCache)) { + if (isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts)) { return [] !== $attributes ? - \array_intersect_key($this->tmpDirtyCache, \array_flip($attributes)) : - $this->tmpDirtyCache; + \array_intersect_key($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts, \array_flip($attributes)) : + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts; } $attributes = [] !== $attributes ? $attributes : \array_keys($this->attributes); diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 5b00e78..be55e62 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1147,24 +1147,24 @@ public function save(array $options = []) return false; } - /** $this->isDirty() will call $this->mergeAttributesFromCachedCasts() */ - $isDirty = $this->isDirty(); + /** $this->getDirty() will merge/sync attributes from cached casts objects */ + $isDirty = [] !== ($dirty =$this->getDirty()); - try { - /** To avoid excessive set calls (https://github.com/laravel/framework/discussions/31778) when: - - calling $this->getDirtyForUpdate() which calls $this->getDirty() after updating event, - - calling $this->getAttributesForInsert() which calls $this->getAttributes() after creating event, - - calling $this->finishSave which calls $this->syncOriginal() which calls $this->getAttributes(), - cache must be temporarily cleared */ - $classCastCache = $this->classCastCache; - $attributeCastCache = $this->attributeCastCache; - $this->classCastCache = []; - $this->attributeCastCache = []; - - if ($this->exists) { - if (!$isDirty) { - /** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */ - return true; + if ($this->exists) { + if (!$isDirty) { + /** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */ + return true; + } + + try { + /** We will try to optimize the execution by caching $dirty array BUT, + multiple set calls will be needed (https://github.com/laravel/framework/discussions/31778) + WHEN $this->usesTimestamps() or WHEN there are listeners for the following events: + - updating event can do changes so, $this->getDirtyForUpdate() will call $this->getDirty(), + - updated/saved events can do changes so, $this->syncOriginal() will call $this->getAttributes() */ + if (!$this->getEventDispatcher()->hasListeners($this::class . '.updating')) { + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = $dirty; + unset($dirty); } if ($this->performUpdate($query)) { @@ -1174,27 +1174,30 @@ public function save(array $options = []) } return false; + } finally { + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; } + } - $saved = $this->performInsert($query); + /** Multiple set calls are needed (https://github.com/laravel/framework/discussions/31778) because: + - $this->performInsert can do changes, + - creating event can do changes so, $this->getAttributesForInsert() will call $this->getAttributes(), + - created/saved events can do changes so, $this->syncOriginal() will call $this->getAttributes() */ - if ( - '' === (string)$this->getConnectionName() && - ($connection = $query->getConnection()) instanceof Connection - ) { - $this->setConnection($connection->getName()); - } + $saved = $this->performInsert($query); - if ($saved) { - $this->finishSave(['touch' => $isDirty]); - } + if ( + '' === (string)$this->getConnectionName() && + ($connection = $query->getConnection()) instanceof Connection + ) { + $this->setConnection($connection->getName()); + } - return $saved; - } finally { - /** Reset cache by preserving the new cached objects created in updating/updated/created/creating/saved events */ - $this->classCastCache += $classCastCache; - $this->attributeCastCache += $attributeCastCache; + if ($saved) { + $this->finishSave(['touch' => $isDirty]); } + + return $saved; } /** @@ -1218,6 +1221,10 @@ public function saveOrFail(array $options = []) */ protected function finishSave(array $options) { + if ($this->getEventDispatcher()->hasListeners($this::class . '.saved')) { + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; + } + $this->fireModelEvent('saved', false); if ($options['touch'] ?? true) { @@ -1246,23 +1253,25 @@ protected function performUpdate(Builder $query) // update timestamp on the model which are maintained by us for developer // convenience. Then we will just continue saving the model instances. if ($this->usesTimestamps()) { + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; $this->updateTimestamps(); } // Once we have run the update operation, we will fire the "updated" event for // this model instance. This will allow developers to hook into these after // models are updated, giving them a chance to do any special processing. - /** This is needed because updating event might change the model */ $dirty = $this->getDirtyForUpdate(); if ([] !== $dirty) { $this->setKeysForSaveQuery($query)->update($dirty); - try { - $this->tmpDirtyCache = $dirty; - $this->syncChanges(); - } finally { - unset($this->tmpDirtyCache); + $this->syncChanges(); + + if ( + isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts) + && $this->getEventDispatcher()->hasListeners($this::class . '.updated') + ) { + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; } $this->fireModelEvent('updated', false); From 8182c687c08f050513a6b9a1468da7276990bb27 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 16:47:11 +0200 Subject: [PATCH 19/33] POC for https://github.com/laravel/framework/discussions/31778 handle detached casted objects from $model and revert reduce even more the number of set calls during insert also --- illuminate/Database/Eloquent/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index be55e62..56ca55b 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1148,7 +1148,7 @@ public function save(array $options = []) } /** $this->getDirty() will merge/sync attributes from cached casts objects */ - $isDirty = [] !== ($dirty =$this->getDirty()); + $isDirty = [] !== ($dirty = $this->getDirty()); if ($this->exists) { if (!$isDirty) { From eeb4da649e38923a664b5445323de95ab2bf78eb Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 16:50:39 +0200 Subject: [PATCH 20/33] POC for https://github.com/laravel/framework/discussions/31778 handle detached casted objects from $model and revert reduce even more the number of set calls during insert also --- illuminate/Database/Eloquent/Model.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 56ca55b..a32e5f0 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1147,10 +1147,10 @@ public function save(array $options = []) return false; } - /** $this->getDirty() will merge/sync attributes from cached casts objects */ - $isDirty = [] !== ($dirty = $this->getDirty()); - if ($this->exists) { + /** $this->getDirty() will merge/sync attributes from cached casts objects */ + $isDirty = [] !== ($dirty = $this->getDirty()); + if (!$isDirty) { /** don't call saved event like in Laravel: https://github.com/laravel/framework/issues/56254 */ return true; @@ -1179,6 +1179,9 @@ public function save(array $options = []) } } + /** $this->isDirty() will merge/sync attributes from cached casts objects */ + $isDirty = $this->isDirty(); + /** Multiple set calls are needed (https://github.com/laravel/framework/discussions/31778) because: - $this->performInsert can do changes, - creating event can do changes so, $this->getAttributesForInsert() will call $this->getAttributes(), From fda86344eaffdac98297ef2168ded8e75448379f Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 16:54:08 +0200 Subject: [PATCH 21/33] POC for https://github.com/laravel/framework/discussions/31778 handle detached casted objects from $model and revert reduce even more the number of set calls during insert also --- illuminate/Database/Eloquent/Model.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index a32e5f0..c6bdb9b 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1197,7 +1197,7 @@ public function save(array $options = []) } if ($saved) { - $this->finishSave(['touch' => $isDirty]); + $this->finishSave($options + ['touch' => $isDirty]); } return $saved; From d279a1e6d1e391b22cf61365e041a5d33d646141 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Mon, 3 Nov 2025 17:55:57 +0200 Subject: [PATCH 22/33] POC for https://github.com/laravel/framework/discussions/31778 cr --- illuminate/Database/Eloquent/Model.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index c6bdb9b..d16abc2 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1224,7 +1224,10 @@ public function saveOrFail(array $options = []) */ protected function finishSave(array $options) { - if ($this->getEventDispatcher()->hasListeners($this::class . '.saved')) { + if ( + isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts) + && $this->getEventDispatcher()->hasListeners($this::class . '.saved') + ) { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; } From 248efdddaa15660a49e59bfa8c6eab66ffe70b6c Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Tue, 4 Nov 2025 08:33:38 +0200 Subject: [PATCH 23/33] POC for https://github.com/laravel/framework/discussions/31778 cr --- .../Database/Eloquent/Concerns/HasAttributes.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 7b9381b..ca7cc05 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -2131,10 +2131,18 @@ public function getDirty() $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts; } - $attributes = [] !== $attributes ? $attributes : \array_keys($this->attributes); - $dirty = []; + if ([] === $attributes) { + foreach ($this->getAttributes() as $key => $value) { + if (!$this->originalIsEquivalent($key)) { + $dirty[$key] = $value; + } + } + + return $dirty; + } + foreach ($attributes as $key) { /** This will call $this->mergeAttributesFromCachedCasts($key) before the if condition */ $value = $this->getAttributeFromArray($key); From 33d93d477d75ae4d2b5b16ddf4be1f530bb2b6c5 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Tue, 4 Nov 2025 09:06:30 +0200 Subject: [PATCH 24/33] POC for https://github.com/laravel/framework/discussions/31778 cr --- illuminate/Database/Eloquent/Model.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index d16abc2..3c0691a 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1160,7 +1160,8 @@ public function save(array $options = []) /** We will try to optimize the execution by caching $dirty array BUT, multiple set calls will be needed (https://github.com/laravel/framework/discussions/31778) WHEN $this->usesTimestamps() or WHEN there are listeners for the following events: - - updating event can do changes so, $this->getDirtyForUpdate() will call $this->getDirty(), + - updating event can do changes so, $this->getDirtyForUpdate() and $this->syncChanges() will call + $this->getDirty(), - updated/saved events can do changes so, $this->syncOriginal() will call $this->getAttributes() */ if (!$this->getEventDispatcher()->hasListeners($this::class . '.updating')) { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = $dirty; From daca387f900d6895c29961bb7154247f88cd00d1 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Tue, 4 Nov 2025 12:47:22 +0200 Subject: [PATCH 25/33] POC for https://github.com/laravel/framework/discussions/31778 cr --- illuminate/Database/Eloquent/Model.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 3c0691a..0deb516 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1163,7 +1163,7 @@ public function save(array $options = []) - updating event can do changes so, $this->getDirtyForUpdate() and $this->syncChanges() will call $this->getDirty(), - updated/saved events can do changes so, $this->syncOriginal() will call $this->getAttributes() */ - if (!$this->getEventDispatcher()->hasListeners($this::class . '.updating')) { + if (!$this->getEventDispatcher()->hasListeners('eloquent.updating: ' . $this::class)) { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = $dirty; unset($dirty); } @@ -1227,7 +1227,7 @@ protected function finishSave(array $options) { if ( isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts) - && $this->getEventDispatcher()->hasListeners($this::class . '.saved') + && $this->getEventDispatcher()->hasListeners('eloquent.saved: ' . $this::class) ) { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; } @@ -1276,7 +1276,7 @@ protected function performUpdate(Builder $query) if ( isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts) - && $this->getEventDispatcher()->hasListeners($this::class . '.updated') + && $this->getEventDispatcher()->hasListeners('eloquent.updated: ' . $this::class) ) { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; } From 0279e60c9864b60be7dcf050f7b7a8437bb75ff5 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Tue, 4 Nov 2025 20:48:19 +0200 Subject: [PATCH 26/33] POC for https://github.com/laravel/framework/discussions/31778 cover this case $model = User::query()->firstOrFail(); $carbon = $model->date_time_carbon_casted; $carbon->addDay(); echo $model->col_with_get_mutator_that_depends_on_date_time_carbon_casted; // would print 'value date time' without the added day before this commit --- .../Database/Eloquent/Concerns/HasAttributes.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index ca7cc05..f32136d 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -509,7 +509,7 @@ protected function throwMissingAttributeExceptionIfApplicable($key) */ public function getAttributeValue($key) { - return $this->transformModelValue($key, $this->getAttributeFromArray($key)); + return $this->transformModelValue($key, $this->getAttributeFromArray($key, true)); } /** @@ -517,10 +517,18 @@ public function getAttributeValue($key) * @see self::getAttributeValue * * @param string $key + * @param bool $mergeAllAttributesFromCachedCasts * @return mixed */ protected function getAttributeFromArray($key) { + if ( + true === (\func_get_args()[1] ?? false) + && ($this->hasGetMutator($key) || $this->hasAttributeGetMutator($key)) + ) { + return $this->getAttributes()[$key] ?? null; + } + $this->mergeAttributesFromClassCasts($key); $this->mergeAttributesFromAttributeCasts($key); From 564f123be0d36e59ccfdca296ef24f9de2b61988 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Thu, 6 Nov 2025 11:08:31 +0200 Subject: [PATCH 27/33] POC for https://github.com/laravel/framework/discussions/31778 cover changes in created updated saved that got into $original but not in db --- .../Eloquent/Concerns/HasAttributes.php | 8 ++- illuminate/Database/Eloquent/Model.php | 58 ++++++++++--------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index f32136d..ac3f053 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -70,6 +70,12 @@ trait HasAttributes */ protected ?array $tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; + /** + * Temporary original cache to prevent changes in created,updated,saved events from getting + * into $original without being saved into DB + */ + protected ?array $tmpOriginalBeforeAfterEvents = null; + /** * The attributes that should be cast. * @@ -2002,7 +2008,7 @@ public function only($attributes) */ public function syncOriginal() { - $this->original = $this->getAttributes(isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts)); + $this->original = $this->getAttributes(); return $this; } diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 0deb516..164f8de 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1159,10 +1159,9 @@ public function save(array $options = []) try { /** We will try to optimize the execution by caching $dirty array BUT, multiple set calls will be needed (https://github.com/laravel/framework/discussions/31778) - WHEN $this->usesTimestamps() or WHEN there are listeners for the following events: - - updating event can do changes so, $this->getDirtyForUpdate() and $this->syncChanges() will call - $this->getDirty(), - - updated/saved events can do changes so, $this->syncOriginal() will call $this->getAttributes() */ + WHEN $this->usesTimestamps() or WHEN there are listeners for updating event because they can do changes + so, $this->getDirtyForUpdate() and $this->syncChanges() will call $this->getDirty() which will call + getAttributes() */ if (!$this->getEventDispatcher()->hasListeners('eloquent.updating: ' . $this::class)) { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = $dirty; unset($dirty); @@ -1177,6 +1176,7 @@ public function save(array $options = []) return false; } finally { $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; + $this->tmpOriginalBeforeAfterEvents = null; } } @@ -1185,20 +1185,24 @@ public function save(array $options = []) /** Multiple set calls are needed (https://github.com/laravel/framework/discussions/31778) because: - $this->performInsert can do changes, - - creating event can do changes so, $this->getAttributesForInsert() will call $this->getAttributes(), - - created/saved events can do changes so, $this->syncOriginal() will call $this->getAttributes() */ + - creating event can do changes so, + $this->getAttributesForInsert() and $this->syncOriginal() will call $this->getAttributes() */ - $saved = $this->performInsert($query); + try { + $saved = $this->performInsert($query); - if ( - '' === (string)$this->getConnectionName() && - ($connection = $query->getConnection()) instanceof Connection - ) { - $this->setConnection($connection->getName()); - } + if ( + '' === (string)$this->getConnectionName() && + ($connection = $query->getConnection()) instanceof Connection + ) { + $this->setConnection($connection->getName()); + } - if ($saved) { - $this->finishSave($options + ['touch' => $isDirty]); + if ($saved) { + $this->finishSave($options + ['touch' => $isDirty]); + } + } finally { + $this->tmpOriginalBeforeAfterEvents = null; } return $saved; @@ -1225,19 +1229,19 @@ public function saveOrFail(array $options = []) */ protected function finishSave(array $options) { - if ( - isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts) - && $this->getEventDispatcher()->hasListeners('eloquent.saved: ' . $this::class) - ) { - $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; - } - $this->fireModelEvent('saved', false); if ($options['touch'] ?? true) { $this->touchOwners(); } + if (isset($this->tmpOriginalBeforeAfterEvents)) { + $this->original = $this->tmpOriginalBeforeAfterEvents; + $this->tmpOriginalBeforeAfterEvents = null; + + return; + } + $this->syncOriginal(); } @@ -1274,12 +1278,8 @@ protected function performUpdate(Builder $query) $this->syncChanges(); - if ( - isset($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts) - && $this->getEventDispatcher()->hasListeners('eloquent.updated: ' . $this::class) - ) { - $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; - } + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; + $this->tmpOriginalBeforeAfterEvents = $this->attributes; $this->fireModelEvent('updated', false); } @@ -1374,6 +1374,8 @@ protected function performInsert(Builder $query) $query->insert($attributes); } + $this->tmpOriginalBeforeAfterEvents = $this->attributes; + // We will go ahead and set the exists property to true, so that it is set when // the created event is fired, just in case the developer tries to update it // during the event. This will allow them to do so and run an update here. From cf56959e296af55442b9151911f8d4abbd228944 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Fri, 7 Nov 2025 09:12:07 +0200 Subject: [PATCH 28/33] POC for https://github.com/laravel/framework/discussions/31778 bulletproofing incrementOrDecrement and backport laravel pull/47450 --- illuminate/Database/Eloquent/Model.php | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 164f8de..8034a25 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -984,8 +984,12 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) $this->forceFill($extra); - if ($this->fireModelEvent('updating') === false) { - return false; + if (!$this->isDirty() || $this->fireModelEvent('updating') === false) { + return 0; + } + + if ($this->isClassDeviable($column)) { + $amount = (clone $this)->setAttribute($column, $amount)->getAttributeFromArray($column); } return tap( @@ -1271,18 +1275,18 @@ protected function performUpdate(Builder $query) // Once we have run the update operation, we will fire the "updated" event for // this model instance. This will allow developers to hook into these after // models are updated, giving them a chance to do any special processing. - $dirty = $this->getDirtyForUpdate(); + if ([] === $dirty = $this->getDirtyForUpdate()) { + return false; + } - if ([] !== $dirty) { - $this->setKeysForSaveQuery($query)->update($dirty); + $this->setKeysForSaveQuery($query)->update($dirty); - $this->syncChanges(); + $this->syncChanges(); - $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; - $this->tmpOriginalBeforeAfterEvents = $this->attributes; + $this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null; + $this->tmpOriginalBeforeAfterEvents = $this->attributes; - $this->fireModelEvent('updated', false); - } + $this->fireModelEvent('updated', false); return true; } From a1f2197543805f7901fbfebec15f7f16f85202c2 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Fri, 7 Nov 2025 09:26:35 +0200 Subject: [PATCH 29/33] POC for https://github.com/laravel/framework/discussions/31778 revert backport laravel pull/47450 because isDirty will sync/merge the cast cached objects --- illuminate/Database/Eloquent/Model.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 8034a25..9e5f383 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -988,10 +988,6 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) return 0; } - if ($this->isClassDeviable($column)) { - $amount = (clone $this)->setAttribute($column, $amount)->getAttributeFromArray($column); - } - return tap( $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($column, $amount, $extra), function () use ($column) { From 02bd9ccd1d183f72faf45e823c3520b91dfbd7a6 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Fri, 7 Nov 2025 09:50:13 +0200 Subject: [PATCH 30/33] POC for https://github.com/laravel/framework/discussions/31778 bulletproofing incrementOrDecrement --- illuminate/Database/Eloquent/Model.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 9e5f383..53ef525 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -980,7 +980,11 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) $this->{$column} = $this->isClassDeviable($column) ? $this->deviateClassCastableAttribute($method, $column, $amount) - : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1); + : (\extension_loaded('bcmath') ? \bcadd( + $s1 = (string)$this->{$column}, + $s2 = (string)($method === 'increment' ? $amount : $amount * -1), + \max(\strlen(\strrchr($s1, '.') ?: ''), \strlen(\strrchr($s2, '.') ?: '')) + ) : $this->{$column} + ($method === 'increment' ? $amount : $amount * -1)); $this->forceFill($extra); @@ -988,7 +992,7 @@ protected function incrementOrDecrement($column, $amount, $extra, $method) return 0; } - return tap( + return (int)tap( $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->{$method}($column, $amount, $extra), function () use ($column) { $this->syncChanges(); From aea765e41f4378d76b80fdb869c6734ec15ac06e Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Fri, 7 Nov 2025 15:50:53 +0200 Subject: [PATCH 31/33] POC for https://github.com/laravel/framework/discussions/31778 cr --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- illuminate/Database/Eloquent/Model.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index ac3f053..973dd8f 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1912,7 +1912,7 @@ public function getAttributes() */ protected function getAttributesForInsert() { - return $this->getAttributes(); + return $this->tmpOriginalBeforeAfterEvents = $this->getAttributes(); } /** diff --git a/illuminate/Database/Eloquent/Model.php b/illuminate/Database/Eloquent/Model.php index 53ef525..6ae7e04 100644 --- a/illuminate/Database/Eloquent/Model.php +++ b/illuminate/Database/Eloquent/Model.php @@ -1371,8 +1371,8 @@ protected function performInsert(Builder $query) // If the table isn't incrementing we'll simply insert these attributes as they // are. These attribute arrays must contain an "id" column previously placed // there by the developer as the manually determined key for these models. - if (empty($attributes)) { - return true; + if ([] === $attributes) { + return false; } $query->insert($attributes); From 23bcae6df2ee03fae7ce9c831edc09d14071a7b1 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Fri, 7 Nov 2025 16:13:08 +0200 Subject: [PATCH 32/33] POC for https://github.com/laravel/framework/discussions/31778 revert because they are handled in performInsert --- illuminate/Database/Eloquent/Concerns/HasAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/illuminate/Database/Eloquent/Concerns/HasAttributes.php b/illuminate/Database/Eloquent/Concerns/HasAttributes.php index 973dd8f..ac3f053 100644 --- a/illuminate/Database/Eloquent/Concerns/HasAttributes.php +++ b/illuminate/Database/Eloquent/Concerns/HasAttributes.php @@ -1912,7 +1912,7 @@ public function getAttributes() */ protected function getAttributesForInsert() { - return $this->tmpOriginalBeforeAfterEvents = $this->getAttributes(); + return $this->getAttributes(); } /** From 5d4c32afb7e4735cf7a793dbb38307cca0765623 Mon Sep 17 00:00:00 2001 From: Pantea Marius-ciclistu <> Date: Fri, 7 Nov 2025 16:16:44 +0200 Subject: [PATCH 33/33] POC for https://github.com/laravel/framework/discussions/31778 bump version 10.53.2 --- illuminate/Foundation/Application.php | 2 +- src/Application.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/illuminate/Foundation/Application.php b/illuminate/Foundation/Application.php index ea7043c..0246c21 100755 --- a/illuminate/Foundation/Application.php +++ b/illuminate/Foundation/Application.php @@ -44,7 +44,7 @@ class Application extends Container implements * * @var string */ - public const VERSION = '10.53.1'; + public const VERSION = '10.53.2'; /** * The base path for the Laravel installation. diff --git a/src/Application.php b/src/Application.php index df8fc58..3a396a7 100644 --- a/src/Application.php +++ b/src/Application.php @@ -58,7 +58,7 @@ class Application extends Container implements ApplicationContract * * @var string */ - public const VERSION = '10.53.1'; + public const VERSION = '10.53.2'; /** * Indicates if the class aliases have been registered.