In Eloquent, what do saveQuietly() and Model::withoutEvents() suppress, and what surprises teams that rely on them?
answer
- per call versus per block
- NullDispatcher swapped in, then restored
- one static dispatcher for every model
- timestamps and $fillable still apply
- derive fields in saving, not saved
basics
~20 ssaveQuietly() saves one model with Eloquent events muted; Model::withoutEvents() runs a closure with them muted. Observers, booted() closures and $dispatchesEvents are all skipped, timestamps are still written, and the muting covers every model class, not only the one named.
solid answer
~40 s`withoutEvents()` swaps the static dispatcher that all Eloquent models share for a `NullDispatcher`, runs the closure, and restores it in `finally`; `saveQuietly()`, `updateQuietly()`, `deleteQuietly()` and the other `*Quietly` methods just wrap one call in it. So no model event fires: no observer, no `booted()` closure, no `$dispatchesEvents` class, and no `saving` veto either. Timestamps and mass-assignment rules still apply, and application events you dispatch yourself still go out. The surprises are that it mutes every model class inside the block, including parents touched by the save, and that skipped side effects are never replayed. The classic legitimate use is a follow-up write inside an `updated` handler, where a plain `save()` would re-enter the handler.
code
php · 21 lines<?php
namespace App\Observers;
use App\Models\Listing;
class ListingObserver
{
// Preferred: derive before the write, no second save needed.
public function saving(Listing $listing): void
{
$listing->price_per_sqm = (int) round($listing->asking_price / max(1, $listing->floor_area));
}
// If a write must follow the update, keep it from re-entering.
public function updated(Listing $listing): void
{
$listing->last_synced_at = now();
$listing->saveQuietly();
}
}go deeper
Recall that saveQuietly() and the other Quietly methods save without firing observers or model event closures.
Explain the NullDispatcher swap, that it is shared by every model, and what still happens: timestamps, mass-assignment rules and explicit application events.
Use quiet writes deliberately, derive columns in saving to avoid re-entrant saves, and always pair a muted backfill with an explicit reindex and audit step.
Treat widespread use of quiet saves as a sign that side effects live in the wrong layer, and push for explicit service calls instead.
## What the quiet methods are Sometimes a write must not trigger the model's hooks: a data backfill that should not flood the search index, a seeder, or a handler that has to store one more column without re-running itself. Eloquent offers two levels of muting. **Per call**, the `*Quietly` methods run one operation with events muted: - `saveQuietly()` and `updateQuietly($attributes)`; - `deleteQuietly()`, plus `forceDeleteQuietly()` and `restoreQuietly()` with `SoftDeletes`; - `touchQuietly()`, `replicateQuietly()`, `pushQuietly()`; - `incrementQuietly()` / `decrementQuietly()`, and the builder's `createQuietly()` / `forceCreateQuietly()`. **Per block**, `Model::withoutEvents(fn () => ...)` runs a closure with model events muted and returns whatever the closure returns. The per-call methods are thin wrappers: `saveQuietly()` is literally `static::withoutEvents(fn () => $this->save($options))`. ## How muting works Every Eloquent model reads its dispatcher from one **static property declared on the base `Model` class**. `withoutEvents()`: 1. saves the current dispatcher; 2. replaces it with a `NullDispatcher` that swallows `dispatch()` and `until()` calls; 3. runs your closure; 4. restores the real dispatcher in a `finally` block, even if the closure throws. Everything in the table below follows from those four steps. | Question | Answer | |---|---| | Which models are muted? | **All** Eloquent models, not just the class you called it on | | Are observers, `booted()` closures and `$dispatchesEvents` skipped? | Yes, all three | | Is a `false` veto from `saving` still possible? | No; no handler runs to return it | | Are `created_at`/`updated_at` still set? | Yes; timestamps are not events | | Is mass assignment still enforced? | Yes, `$fillable`/`$guarded` still apply | | Do application events via `event()` or `Event::dispatch()` fire? | Yes; only the model dispatcher is swapped | ## The surprises - **It mutes more than you named.** `Listing::withoutEvents(...)` also silences an `Agent` or `AuditEntry` saved inside the closure. And because `saveQuietly()` wraps the whole `save()`, parent models listed in `$touches` still get their timestamp bumped, but the `saved` event Eloquent fires on each of them is muted as well. - **Validation hooks disappear.** If a `saving` handler guards an invariant, a quiet save bypasses the guard as well as the side effects. - **Silence is permanent.** Muted events are not queued for later; a search index that relied on `updated` simply goes stale for those rows. - **Only model events stop.** An application event your code dispatches explicitly inside the closure is delivered normally. ## The recursion pattern The most common legitimate use is inside a handler. Suppose `ListingObserver::updated()` computes a price-per-square-metre column and calls `$listing->save()`. That nested save is itself an update, so it fires `updating` and `updated` again, which calls the handler again. Because the outer save has not yet synced the model's original attributes, the model still looks dirty and the loop can continue until the process fails. Two fixes, in order of preference: 1. **Compute derived columns in `saving` or `updating`.** The value is set before the SQL runs and lands in the same UPDATE, with no second save. 2. If the write truly must happen after the fact, use **`saveQuietly()`** in the after-event so it cannot re-enter the handlers. ## A checklist before muting Before reaching for a quiet write, answer these in order: 1. **Which handlers am I skipping?** List the observer methods, closures and mapped event classes for every model the block touches, not just the one you are writing. 2. **Does any of them guard an invariant?** A `saving` or `deleting` veto that you bypass must be enforced some other way. 3. **Which side effects must still happen?** Search index, audit log, cache keys: schedule them explicitly for the affected IDs. 4. **Is the block as small as possible?** Wrap one loop or one call, never a whole request or command. If the honest answer to the third question is "all of them", a quiet write is the wrong tool; a normal save with the handlers running is simpler. ## When to use which - A one-off fix, backfill or import: `Model::withoutEvents()` around the loop, knowing everything inside is silent, then run the side effects explicitly (reindex the affected IDs). - One follow-up write inside a handler: `saveQuietly()`. - Tests that do not want observers: prefer faking the dependency the observer calls over muting the model, so the test still exercises real saves.
- Inside Listing::withoutEvents(), your code calls event(new ListingImported($listing)). Is that event delivered?Yes. `withoutEvents()` replaces only the dispatcher Eloquent models hold in their static property; the application's dispatcher behind `event()` and the `Event` facade is untouched. Model lifecycle events are muted, while events you dispatch explicitly still reach their listeners.
- A backfill wrapped in Model::withoutEvents() updated 10,000 listings. What must you do afterwards?Run the side effects the observer would have performed, because muted events are discarded, not deferred. Typically that means dispatching a reindex-and-audit job for the affected IDs, or rebuilding the search index for that range, and recording the backfill in the audit trail yourself.
It is like switching off the office's shared intercom rather than one phone: while it is off, nobody in the building hears an announcement, and nothing said in that time is replayed when it comes back on.
saying these in an interview costs you the question
- withoutEvents mutes only the model class it was called on
- Muted events are queued and replayed when the closure ends
- saveQuietly skips updating the updated_at timestamp
- saveQuietly still lets a saving handler veto the write
- withoutEvents also blocks events dispatched through the Event facade