loadedUpdatedAt = $this->currentUpdatedAt(); $this->warnOfOtherEditors(); } /** * @param array $data */ protected function handleRecordUpdate(Model $record, array $data): Model { $current = $this->currentUpdatedAt(); if ($this->loadedUpdatedAt !== null && $current !== null && $current !== $this->loadedUpdatedAt) { $this->warnOfConflict(); throw (new Halt)->rollBackDatabaseTransaction(); } $record = parent::handleRecordUpdate($record, $data); // What was just written is the new baseline: saving again without // reloading is judged against what this screen itself just wrote, // not against the moment it was first opened. $this->loadedUpdatedAt = $this->currentUpdatedAt(); return $record; } protected function warnOfOtherEditors(): void { $user = Auth::user(); if ($user === null) { return; } $others = Presence::touch($this->getRecord(), $user); if ($others->isEmpty()) { return; } Notification::make() ->warning() ->title(__('panel.presence.title')) ->body(__('panel.presence.body', ['names' => $others->pluck('name')->implode(', ')])) ->persistent() ->send(); } protected function warnOfConflict(): void { Notification::make() ->danger() ->title(__('panel.concurrency.title')) ->body($this->conflictMessage()) ->persistent() ->send(); } /** * Named when the history knows who — which is most of the time, since * every content model records who changed what. Without a name the * message still says everything that matters: it changed, and saving now * would have erased that. */ protected function conflictMessage(): string { $record = $this->getRecord(); $author = method_exists($record, 'revisions') ? $record->revisions()->value('user_name') : null; return $author !== null ? __('panel.concurrency.body_with_author', ['name' => $author]) : __('panel.concurrency.body'); } /** * Read straight from the table: one column, no relations, no casts to * disagree about, and no risk of a stale in-memory copy answering for the * database. * * Precision is whole seconds, which is what `timestamps()` stores here — * two saves inside the same second are a genuine race even in * production, and this guard is honest about not catching that one. */ protected function currentUpdatedAt(): ?string { $record = $this->getRecord(); return DB::table($record->getTable()) ->where($record->getKeyName(), $record->getKey()) ->value('updated_at'); } }