The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →To prevent one PHP request from silently overwriting another user’s changes, save a version number with the record and require each update to match the version the user originally read. A conditional update changes the row and increments its version only if that expected version is still current. If no row matches, treat the result as a conflict or a missing record—not as a successful save.
What optimistic offline locking solves
Optimistic locking is designed for edits that span separate HTTP requests. A user loads a form, spends time changing it, then submits it after another request may have modified the same record. Without a version check, the later write can replace the earlier one: a lost update.
The application assumes simultaneous edits are uncommon, so it does not hold a database lock while the user is working. Instead, it records the version read with the form and checks that version when saving. Doctrine explains that transactions are appropriate for concurrency control within one request, but should not span requests and the user’s think time. Doctrine: Transactions and Concurrency.
How the version check works
- Store a version value on each row that needs protection.
- When serving the form, send the current row and its version to the client.
- On submission, use the original version as the expected version.
- Update the row only if its ID and version still match; increment the version as part of that same update.
- If the update affects no rows, decide whether the record was changed or deleted and handle that outcome explicitly.
The version comparison and increment must be atomic. Do not fetch a fresh version after the form is submitted and then update unconditionally: that would accept stale user input and recreate the lost-update race.
#1 Best Overall
Framework-neutral SQL and PDO
A conditional update can implement the check directly:
UPDATE articles
SET title = :title,
body = :body,
version = version + 1,
updated_at = CURRENT_TIMESTAMP
WHERE id = :id
AND version = :expected_version;
Bind the submitted values, record ID, and version captured when the form was loaded. Execute the statement in a short transaction. One affected row means the save succeeded. Zero means either the expected version is stale or the record no longer exists; the application should distinguish those cases where its data and authorization rules allow it.
Rank #2
With PDO, call beginTransaction(), run the conditional update, and commit on success. Roll back in the exception path. PDO documents these transaction operations and rollback of uncommitted work in its transactions manual. Keep the transaction limited to the database work; never leave it open while waiting for a person to review or edit a form.
Using Doctrine ORM
Doctrine supports optimistic locking with a mapped version field, such as an integer property marked with #[Version, Column(type: 'integer')]. Load the entity, apply validated changes, and call flush() inside the write transaction. Doctrine compares the entity’s version to the database version and raises DoctrineORMOptimisticLockException if they differ. Its documentation recommends integer versions over timestamps for high-concurrency cases, because timestamps may have insufficient resolution and collide. See Doctrine’s version-field and concurrency guidance.
Doctrine’s UnitOfWork delays persistence SQL until flush(), making that call the persistence boundary to place inside the transaction. See Doctrine: Working with Objects.
Carry the original version through a form
For a multi-request edit, include the version read on GET in the submitted form, commonly as a hidden field, or preserve it in protected server-side session state. On POST, compare against that original expected version. Do not replace it with the entity’s current version after receiving the form: doing so would make stale edits appear current. Doctrine’s example carries a version in a hidden field and checks it on POST: Doctrine: Transactions and Concurrency.
Rank #4
A hidden field is user-controlled input, so it is not proof of authorization or a substitute for validating the submitted values. Use the version only for concurrency checking, and apply normal authentication, authorization, and business-rule validation.
Laravel transactions and row locks
In Laravel, DB::transaction(function () { ... }) commits when the closure succeeds and rolls back and rethrows if it fails. The transaction helper can also retry deadlocks when configured. See Laravel database transactions. A conditional update with a version predicate can run inside this transaction just as it can with PDO.
Laravel also offers sharedLock() and lockForUpdate() for pessimistic row locking, and recommends using them within a transaction. These locks are a different concurrency strategy: they protect rows while the transaction is active, rather than detecting that a version changed since an earlier request. See Laravel pessimistic locking.
Choosing a version check or a row lock
| Approach | Conflict detection | Lock duration | User think time | Implementation and conflict handling |
|---|---|---|---|---|
| Integer version column | Exact equality check against an incrementing version | No row lock needs to remain during editing; the conditional write is brief | Suitable for forms that remain open across requests | Requires carrying and checking the expected version; the application must present a conflict and recovery path |
| Timestamp version | Depends on timestamp resolution; two changes can share a timestamp | No row lock needs to remain during editing; the conditional write is brief | Suitable for forms across requests, subject to collision risk | Similar version-check flow, but Doctrine prefers integers for high concurrency because timestamps can collide |
| Database row lock | Prevents competing writes while the lock is held | Held for the active transaction | Not appropriate to hold while a user edits a form across requests | Requires careful transaction boundaries; use for work that should serialize during a short database operation |
Handle conflicts without losing the user’s work
When the expected version is stale, return HTTP 409 Conflict or an equivalent domain-level conflict. Preserve the attempted edits and show that the saved record has changed. Offer a safe path to reload the latest version, compare the two versions, or reapply the user’s changes. Avoid silently retrying the same update against the new version, because that would overwrite the intervening change without resolving the conflict.
A zero-row update is not always an edit conflict: the row might have been deleted, or the ID might not exist. Choose a response that does not expose records the user is not authorized to see. Validate authorization and field-level business rules before issuing the update, and log conflict counts and affected resource identifiers without recording secrets.
Test the stale-write path
In an isolated test environment, load one row twice and retain the same starting version in two simulated requests. Save the first request so the version increments, then submit the second request with its original version. The second update should affect zero rows (or cause Doctrine to raise its optimistic-lock exception), and the application should preserve the attempted changes and present its conflict flow. Also test the missing/deleted-row case separately so it is not accidentally reported as an ordinary stale edit.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

