Model::save

Persists a model, inserting a new row or updating the existing one depending on its state.

Signature
$model->save(array $options = []): bool

Parameters

ParameterTypeDescription
$$optionsoptionalarray= []Save options. Recognises the 'touch' key: pass ['touch' => false] to skip updating the timestamps of parent models registered in $touches.

Return value

boolTrue when the row was written or there was nothing dirty to write; false when a saving, creating or updating event returned false.

Errors

Throws Illuminate\Database\QueryException when the underlying insert or update fails, for example on a not-null or unique constraint. saveOrFail() wraps the same call in a transaction.

save writes the model to the database. The direction is decided by the model's exists flag: a model retrieved from a query gets an UPDATE constrained to its primary key, a freshly constructed one gets an INSERT and has its key populated afterwards.

When to reach for it

Use it when you set attributes individually — from a form, a calculation, a state transition — or when you need to inspect the model before writing it. When you already hold a validated array, create and update say the same thing in one line.

What trips people up

The return value is not what most people assume. If the model already exists and nothing is dirty, save issues no query at all and still returns true. A true therefore means "nothing is wrong", not "a row was written". wasChanged() and getChanges() tell you what actually moved.

A false means an event handler vetoed the write by returning false from saving, creating or updating — no exception is thrown, so an unchecked save() can silently do nothing. Database-level failures raise a QueryException instead.

Saving does not cascade. Changing a related model requires calling save on it too.

Example

Example · php
<?php

use App\Models\Flight;

// Insert: the model does not exist yet.
$flight = new Flight;
$flight->name = 'London to Paris';

var_dump($flight->save());   // bool(true)
echo $flight->id;            // 1 — populated by the insert

// Update: the model was retrieved, so save() writes an UPDATE.
$flight = Flight::find(1);
$flight->name = 'Paris to London';

var_dump($flight->save());        // bool(true)
var_dump($flight->wasChanged());  // bool(true)
print_r($flight->getChanges());

/*
Array
(
    [name] => Paris to London
    [updated_at] => 2024-01-01 09:00:00
)
*/

When to use it

  • Update one field on a record you just retrieved and write it back.
  • Insert a model built up across several steps of a controller action.
  • Apply a state transition where the new value depends on the old one.
  • Save a child model after assigning it through a relationship.

Frequently asked questions

Why does save() return true when nothing was saved?

Because Eloquent skips the update when the model exists and no attribute is dirty, and reports success for that no-op. Call wasChanged() or getChanges() after saving to find out whether a row was actually written.

Why does save() return false in Laravel?

A saving, creating or updating event handler returned false, which cancels the write without throwing an exception. Database-level problems such as a constraint violation raise a QueryException instead of returning false.

Does save() insert or update?

Both — it inserts when the model's exists flag is false and updates by primary key when it is true. Models returned from a query have exists set to true; models built with new do not until the first successful save.

More examples

A true return does not mean a row was written

Eloquent compares each attribute against its original value and skips the update entirely when nothing is dirty. That is a useful optimisation, but it means updated_at does not move on a no-op save. Use touch() when you want to bump the timestamp regardless.

Example · php
<?php

use App\Models\Flight;

$flight = Flight::find(1);   // name is already 'Paris to London'

$flight->name = 'Paris to London';   // assigning the same value

var_dump($flight->isDirty());    // bool(false)
var_dump($flight->save());       // bool(true) — but no UPDATE was sent
var_dump($flight->wasChanged()); // bool(false)

// updated_at is untouched too, because no update ran at all.
echo $flight->updated_at;  // 2024-01-01 09:00:00 (unchanged)

An event handler can veto the save

Returning false from saving, creating or updating aborts the write and makes save() return false without throwing. Code that ignores the return value therefore reports success to the user while nothing has changed — always branch on it when observers are involved.

Example · php
<?php

namespace App\Observers;

use App\Models\Flight;

class FlightObserver
{
    public function saving(Flight $flight): bool
    {
        return $flight->price >= 0;   // returning false cancels the save
    }
}

// In a controller...
$flight = Flight::find(1);
$flight->price = -10;

if (! $flight->save()) {
    echo "save was cancelled\n";   // this is what prints
}

// saveOrFail() wraps the write in a transaction and rethrows failures.
// $flight->saveOrFail();

Skipping parent timestamp touches

The $touches array makes every comment save issue an extra update against its post, which is what keeps cache keys fresh. Passing ['touch' => false] suppresses that for one call — worth doing in migrations and backfill commands where bumping every parent row would be both slow and misleading.

Example · php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Comment extends Model
{
    /**
     * Relationships to be touched on save.
     *
     * @var array<int, string>
     */
    protected $touches = ['post'];
}

// Normally saving a comment also bumps posts.updated_at.
$comment = Comment::find(1);
$comment->body = 'Edited.';
$comment->save();

// Backfilling thousands of rows? Skip the parent updates.
$comment->body = 'Edited again.';
$comment->save(['touch' => false]);

echo $comment->body;  // Edited again.

Discussion

  • Be the first to comment on this lesson.