0

Dirty tracking — knowing which attributes changed for smart UPDATEs

Expert5 min read·fw-08-005
sqlperformancelaravel-src

Concept

Dirty tracking is the mechanism for knowing which model attributes have changed since the model was loaded from the database. This enables smart UPDATEs that only set changed columns — avoiding unnecessary writes and preventing unintended overwrites.

$original array: A snapshot of attribute values at the point the model was loaded (or last saved). Stored alongside $attributes.

getDirty(): array: Returns the diff between $attributes (current) and $original (baseline). array_diff_assoc($attributes, $original) — returns key-value pairs where the value differs.

isDirty(string|array $attribute): bool: True if the given attribute(s) have changed. $model->isDirty('email') → true if email was changed since load.

isClean(string|array $attribute): bool: Opposite of isDirty().

getOriginal(string $key): mixed: Get the original value of an attribute. $user->getOriginal('email') → the email before the change.

wasChanged(string $attribute): bool: Check if an attribute changed during the LAST save. Different from isDirty()isDirty() checks current vs. original, wasChanged() checks what changed in the last save(). Requires storing the changes at save time.

Use cases for dirty tracking:

  • Smart UPDATE (only changed columns).
  • Conditional actions after save: "if email changed, send re-verification email".
  • Audit logging: record what changed.

Code Example

php
<?php
namespace Framework\Orm;

abstract class Model
{
    protected array $attributes = [];
    protected array $original   = [];   // baseline from last load/save
    protected array $changes    = [];   // changes from last save() call

    // Called after hydration and after save()
    protected function syncOriginal(): void
    {
        $this->original = $this->attributes;
    }

    // After save() — stores what was changed in the last save
    protected function syncChanges(): void
    {
        $this->changes = $this->getDirty();
    }

    public function getDirty(): array
    {
        $dirty = [];
        foreach ($this->attributes as $key => $value) {
            if (!array_key_exists($key, $this->original)) {
                $dirty[$key] = $value; // new attribute
            } elseif ($value !== $this->original[$key]) {
                $dirty[$key] = $value; // changed value
            }
        }
        return $dirty;
    }

    public function isDirty(string|array ...$attributes): bool
    {
        $dirty = $this->getDirty();
        if (empty($attributes)) return !empty($dirty);

        $attrs = is_array($attributes[0]) ? $attributes[0] : $attributes;
        foreach ($attrs as $attr) {
            if (array_key_exists($attr, $dirty)) return true;
        }
        return false;
    }

    public function isClean(string ...$attributes): bool
    {
        return !$this->isDirty(...$attributes);
    }

    public function getOriginal(?string $key = null): mixed
    {
        return $key ? ($this->original[$key] ?? null) : $this->original;
    }

    public function wasChanged(string ...$attributes): bool
    {
        if (empty($attributes)) return !empty($this->changes);
        foreach ($attributes as $attr) {
            if (array_key_exists($attr, $this->changes)) return true;
        }
        return false;
    }

    protected function performUpdate(): bool
    {
        $dirty = $this->getDirty();
        if (empty($dirty)) return true; // nothing changed

        $this->syncChanges(); // record what's about to be saved
        // ... execute SQL UPDATE ...
        $this->syncOriginal(); // after save, original = current
        return true;
    }
}

// Usage
$user = User::find(1);
$user->isDirty();           // false — just loaded, nothing changed
$user->name = 'Alice Smith';
$user->isDirty();           // true
$user->isDirty('name');     // true
$user->isDirty('email');    // false
$user->getOriginal('name'); // 'Alice' (before change)
$user->save();
$user->isDirty();           // false — just saved, synced
$user->wasChanged('name');  // true — changed in last save