Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
8f9745d
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
3004879
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
161cb7d
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
bfc79ea
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
7ac9d61
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
6917fbd
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
89725c0
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
b2fc146
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
aeeb541
POC for https://github.com/laravel/framework/discussions/31778
Nov 1, 2025
fd6a5b2
POC for https://github.com/laravel/framework/discussions/31778 improv…
Nov 2, 2025
4dc2d62
POC for https://github.com/laravel/framework/discussions/31778 bullet…
Nov 2, 2025
ae40dad
POC for https://github.com/laravel/framework/discussions/31778 bullet…
Nov 2, 2025
9f3dbd9
POC for https://github.com/laravel/framework/discussions/31778 bullet…
Nov 2, 2025
e446b2e
POC for https://github.com/laravel/framework/discussions/31778 avoid …
Nov 2, 2025
a7b9faa
POC for https://github.com/laravel/framework/discussions/31778 reduce…
Nov 3, 2025
867dc2b
POC for https://github.com/laravel/framework/discussions/31778 cr
Nov 3, 2025
2cbe2a7
POC for https://github.com/laravel/framework/discussions/31778 reduce…
Nov 3, 2025
5b52d2d
POC for https://github.com/laravel/framework/discussions/31778 handle…
Nov 3, 2025
8182c68
POC for https://github.com/laravel/framework/discussions/31778 handle…
Nov 3, 2025
eeb4da6
POC for https://github.com/laravel/framework/discussions/31778 handle…
Nov 3, 2025
fda8634
POC for https://github.com/laravel/framework/discussions/31778 handle…
Nov 3, 2025
d279a1e
POC for https://github.com/laravel/framework/discussions/31778 cr
Nov 3, 2025
248efdd
POC for https://github.com/laravel/framework/discussions/31778 cr
Nov 4, 2025
33d93d4
POC for https://github.com/laravel/framework/discussions/31778 cr
Nov 4, 2025
daca387
POC for https://github.com/laravel/framework/discussions/31778 cr
Nov 4, 2025
0279e60
POC for https://github.com/laravel/framework/discussions/31778 cover …
Nov 4, 2025
564f123
POC for https://github.com/laravel/framework/discussions/31778 cover …
Nov 6, 2025
cf56959
POC for https://github.com/laravel/framework/discussions/31778 bullet…
Nov 7, 2025
a1f2197
POC for https://github.com/laravel/framework/discussions/31778 revert…
Nov 7, 2025
02bd9cc
POC for https://github.com/laravel/framework/discussions/31778 bullet…
Nov 7, 2025
aea765e
POC for https://github.com/laravel/framework/discussions/31778 cr
Nov 7, 2025
23bcae6
POC for https://github.com/laravel/framework/discussions/31778 revert…
Nov 7, 2025
5d4c32a
POC for https://github.com/laravel/framework/discussions/31778 bump v…
Nov 7, 2025
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
2 changes: 1 addition & 1 deletion illuminate/Database/Console/ShowModelCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion illuminate/Database/Eloquent/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
67 changes: 50 additions & 17 deletions illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -507,14 +513,18 @@ public function getAttributeValue($key)
}

/**
* Get an attribute from the $attributes array.
* Get an 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;
}

/**
Expand Down Expand Up @@ -1798,12 +1808,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(
Expand All @@ -1817,12 +1833,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) {
Expand Down Expand Up @@ -1857,12 +1879,14 @@ protected function normalizeCastClassResponse($key, $value)

/**
* Get all of the current attributes on the model.
*
* @param bool $withoutMergeAttributesFromCachedCasts
* @return array
*/
public function getAttributes()
{
$this->mergeAttributesFromCachedCasts();
if (true !== (\func_get_args()[0] ?? null)) {
$this->mergeAttributesFromCachedCasts();
}

return $this->attributes;
}
Expand Down Expand Up @@ -1996,10 +2020,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($attribute);
}

return $this;
Expand All @@ -2025,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());
}

/**
Expand Down Expand Up @@ -2098,14 +2117,28 @@ 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()
{
$args = \func_get_args();
$attributes = (array)($args[0] ?? []);

if (isset($this->tmpDirtyCache)) {
return [] !== $attributes ?
\array_intersect_key($this->tmpDirtyCache, \array_flip($attributes)) :
$this->tmpDirtyCache;
}

$attributes = [] !== $attributes ? $attributes : \array_keys($this->attributes);

$dirty = [];

foreach ($this->getAttributes() as $key => $value) {
foreach ($attributes as $key) {
/** This will call $this->mergeAttributesFromCachedCasts($key) before the if condition */
$value = $this->getAttributeFromArray($key);

if (!$this->originalIsEquivalent($key)) {
$dirty[$key] = $value;
}
Expand Down
80 changes: 56 additions & 24 deletions illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -995,7 +995,7 @@ function () use ($column) {

$this->fireModelEvent('updated', false);

$this->syncOriginalAttribute($column);
$this->syncOriginalAttributes(\array_keys($this->changes));
}
);
}
Expand Down Expand Up @@ -1137,26 +1137,45 @@ 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;
}

// 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) {
$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.
/** $this->isDirty() will call $this->mergeAttributesFromCachedCasts() */
$isDirty = $this->isDirty();

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]);

return true;
}

return false;
}

$saved = $this->performInsert($query);

if (
Expand All @@ -1165,16 +1184,17 @@ public function save(array $options = [])
) {
$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' => $isDirty]);
}

return $saved ?? true;
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;
}
}

/**
Expand All @@ -1200,7 +1220,7 @@ protected function finishSave(array $options)
{
$this->fireModelEvent('saved', false);

if ($this->isDirty() && ($options['touch'] ?? true)) {
if ($options['touch'] ?? true) {
$this->touchOwners();
}

Expand Down Expand Up @@ -1232,12 +1252,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.
/** 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->syncChanges();
try {
$this->tmpDirtyCache = $dirty;
$this->syncChanges();
} finally {
unset($this->tmpDirtyCache);
}

$this->fireModelEvent('updated', false);
}
Expand Down Expand Up @@ -2356,7 +2382,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);
}

Expand Down
4 changes: 2 additions & 2 deletions illuminate/Database/Eloquent/Relations/BelongsToMany.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down