0

save() — INSERT vs UPDATE based on model state

Advanced5 min read·fw-08-003
sql

Concept

save() — INSERT vs UPDATE based on model state. A newly created model (not yet in the database) should INSERT. A model retrieved from the database should UPDATE only the changed fields.

$model->exists flag: false for new models, true for models loaded from the DB or after first save. save() branches on this flag.

INSERT path: Build INSERT INTO table (col1, col2) VALUES (?, ?) from $this->attributes. After executing, set $this->exists = true and set the primary key from lastInsertId().

UPDATE path: Build UPDATE table SET col1 = ?, col2 = ? WHERE id = ?. Only update CHANGED attributes (dirty tracking, covered in fw-08-005). Set primary key in WHERE clause.

Timestamps: If the model has $timestamps = true, automatically set created_at on INSERT and updated_at on INSERT and UPDATE. Use date('Y-m-d H:i:s') or Carbon::now()->toDateTimeString().

delete() method: DELETE FROM table WHERE id = ?. Sets $this->exists = false. For soft deletes: UPDATE table SET deleted_at = NOW() WHERE id = ?.

fresh(): Reload from the database. Returns a new model instance with fresh data from DB. Useful after an UPDATE that the DB might have modified (triggers, computed columns).

refresh(): Reloads the current model instance's attributes from the DB in-place.

Code Example

php
<?php
namespace Framework\Orm;

abstract class Model
{
    protected bool  $timestamps = true;
    protected array $original   = []; // snapshot of attributes after load

    // ... from fw-08-001/002

    public function save(): bool
    {
        if ($this->timestamps) {
            $now = date('Y-m-d H:i:s');
            if (!$this->exists) {
                $this->setAttribute('created_at', $now);
            }
            $this->setAttribute('updated_at', $now);
        }

        if ($this->exists) {
            return $this->performUpdate();
        } else {
            return $this->performInsert();
        }
    }

    protected function performInsert(): bool
    {
        $attributes = $this->attributes;
        $columns     = implode(', ', array_keys($attributes));
        $placeholders = implode(', ', array_fill(0, count($attributes), '?'));
        $sql = "INSERT INTO {$this->getTable()} ({$columns}) VALUES ({$placeholders})";

        $success = static::$connection->insert($sql, array_values($attributes));

        if ($success) {
            $id = static::$connection->lastInsertId();
            $this->setAttribute($this->primaryKey, $id);
            $this->exists   = true;
            $this->original = $this->attributes; // sync original after save
        }

        return $success;
    }

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

        $sets = implode(', ', array_map(fn($col) => "{$col} = ?", array_keys($dirty)));
        $sql  = "UPDATE {$this->getTable()} SET {$sets} WHERE {$this->primaryKey} = ?";

        $bindings = array_merge(array_values($dirty), [$this->getKey()]);
        $affected = static::$connection->update($sql, $bindings);

        if ($affected >= 0) {
            $this->original = $this->attributes; // sync after update
        }

        return $affected >= 0;
    }

    public function delete(): bool
    {
        if (!$this->exists) return false;

        $sql     = "DELETE FROM {$this->getTable()} WHERE {$this->primaryKey} = ?";
        $deleted = static::$connection->delete($sql, [$this->getKey()]) > 0;

        if ($deleted) {
            $this->exists = false;
        }

        return $deleted;
    }

    public function fresh(): static
    {
        return static::find($this->getKey());
    }

    protected function getDirty(): array
    {
        return array_diff_assoc($this->attributes, $this->original);
    }
}