Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
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
91 changes: 73 additions & 18 deletions illuminate/Database/Eloquent/Concerns/HasAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,18 @@ 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 $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.
*
Expand Down Expand Up @@ -503,18 +515,30 @@ protected function throwMissingAttributeExceptionIfApplicable($key)
*/
public function getAttributeValue($key)
{
return $this->transformModelValue($key, $this->getAttributeFromArray($key));
return $this->transformModelValue($key, $this->getAttributeFromArray($key, true));
}

/**
* Get an attribute from the $attributes array.
* Get an attribute from the $attributes array without transformation
* @see self::getAttributeValue
*
* @param string $key
* @param bool $mergeAllAttributesFromCachedCasts
* @return mixed
*/
protected function getAttributeFromArray($key)
{
return $this->getAttributes()[$key] ?? null;
if (
true === (\func_get_args()[1] ?? false)
&& ($this->hasGetMutator($key) || $this->hasAttributeGetMutator($key))
) {
return $this->getAttributes()[$key] ?? null;
}

$this->mergeAttributesFromClassCasts($key);
$this->mergeAttributesFromAttributeCasts($key);

return $this->attributes[$key] ?? null;
}

/**
Expand Down Expand Up @@ -1798,12 +1822,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 +1847,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 +1893,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 +2034,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 +2061,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 +2131,36 @@ 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->tmpDirtyIfAttributesAreSyncedFromCashedCasts)) {
return [] !== $attributes ?
\array_intersect_key($this->tmpDirtyIfAttributesAreSyncedFromCashedCasts, \array_flip($attributes)) :
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts;
}

$dirty = [];

foreach ($this->getAttributes() as $key => $value) {
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);

if (!$this->originalIsEquivalent($key)) {
$dirty[$key] = $value;
}
Expand Down
92 changes: 71 additions & 21 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,58 @@ 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->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;
}

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

if ($this->performUpdate($query)) {
$this->finishSave($options + ['touch' => $isDirty]);

return true;
}

return false;
} finally {
$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->tmpOriginalBeforeAfterEvents = null;
}
}

/** $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() and $this->syncOriginal() will call $this->getAttributes() */

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

if (
Expand All @@ -1165,16 +1197,15 @@ 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($options + ['touch' => $isDirty]);
}
} finally {
$this->tmpOriginalBeforeAfterEvents = null;
}

return $saved ?? true;
return $saved;
}

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

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

if (isset($this->tmpOriginalBeforeAfterEvents)) {
$this->original = $this->tmpOriginalBeforeAfterEvents;
$this->tmpOriginalBeforeAfterEvents = null;

return;
}

$this->syncOriginal();
}

Expand All @@ -1226,6 +1264,7 @@ 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();
}

Expand All @@ -1234,11 +1273,14 @@ protected function performUpdate(Builder $query)
// models are updated, giving them a chance to do any special processing.
$dirty = $this->getDirtyForUpdate();

if (count($dirty) > 0) {
if ([] !== $dirty) {
$this->setKeysForSaveQuery($query)->update($dirty);

$this->syncChanges();

$this->tmpDirtyIfAttributesAreSyncedFromCashedCasts = null;
$this->tmpOriginalBeforeAfterEvents = $this->attributes;

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

Expand Down Expand Up @@ -1332,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.
Expand Down Expand Up @@ -2356,7 +2400,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