save() — INSERT vs UPDATE based on model state
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
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);
}
}