Page restoration
Page restoration runs over the complete recorded member set in one database transaction. The selected page, its required deleted ancestors and eligible recorded descendants are read under row locks. Deletion membership comes from locked current database state, never the deletion state of a stale model. Membership is consumed per restored member and pruned on restore or permanent purge. A Site restore preserves its own batch history. Independent Page restoration consumes that Page’s membership in open Site batches too, so a later independent deletion cannot be restored by stale Site membership.
Untracked historical descendants are never inferred from timestamps. Only the selected page and its required deleted ancestors can be recovered from that history. Admin guidance explains this limit and notices name accessible excluded descendants with instructions for separate recovery. Notices and trash pagination use the page view policy and disclose neither inaccessible titles nor counts. Visibility is evaluated afresh rather than retained across actions.
Entry points and authorisation
Section titled “Entry points and authorisation”The edit action, list bulk action and Recently Deleted use the Admin restore
action. There is no separate list row restore action. The Core model boundary
also checks every member, so direct Page::restore() and restoreQuietly() do
not bypass the acting user’s policy or site scope. Unauthenticated console
maintenance is trusted; an unauthenticated web request is refused. Query-builder
updates are maintenance primitives without Eloquent model methods or policies
and must not be exposed as user restore endpoints.
Every member is authorised before restoration writes begin. Core re-reads the members and rechecks authorisation before the cascade completes. If an ability callback changes an earlier member’s access and the final check fails, the transaction rolls back. Core does not intercept SQL: ordinary database-backed permission-cache population is supported. Policies and listeners are trusted application callbacks, not a sandbox; this check does not promise to detect every arbitrary mutation or external effect they can perform.
Site restoration locks the current Site and deletion batch, validates every Page before restoring anything, and uses the same model boundary. A Page plan that would expand outside the recorded Site batch is refused. Recorded Site routes, including manual routes without a Page owner, receive collision checks.
Model listeners and transaction boundary
Section titled “Model listeners and transaction boundary”Ordinary Eloquent model events and database auditing run inside the restoration
transaction with the framework’s normal semantics. Additional
restoring, saving and updating listeners are supported, including Publishing
Studio’s draftable-model saving listener. A restoring listener returning
false cancels restoration as Eloquent defines. A cancellation or exception
rolls back all Page, translation, URL and deletion-membership rows written by the
cascade on its database connection. Quiet restoration keeps model events silent.
Each successful bulk selection has its own transaction.
Standard translation and URL restoration is batched; every Page still runs its
model restore lifecycle in parent-first order. Core emits one PageSaved
notification per restored member after commit. Availability-only restoration
does not create a duplicate content revision; listener changes to captured
Page attributes still create a revision. Ordinary saves continue to emit
PageSaved synchronously, recording each content state within their transaction.
Core’s restoration cache invalidation and content-graph work use the connection’s
afterCommit facility; an outer rollback
also discards that work. Listeners’ side effects outside the transaction’s
database are their responsibility. Use the framework’s afterCommit callbacks,
ShouldHandleEventsAfterCommit for listeners/observers, or queued after-commit
facilities for cache, search and notification work that must not run after a
rolled-back cascade.
After-commit delivery is not a durable outbox or an exactly-once protocol. A process failure can lose notifications; an after-commit listener can throw after the database is already committed. That exception cannot undo committed rows. The one-notification guarantee describes Core’s dispatch, not listener delivery or additional events emitted by extensions.
Route collisions
Section titled “Route collisions”The route key is (site_id, language_id, url) for enabled, live rows. The migration
indexes (site_id, language_id, url, deleted_at); its nullable deletion column does
not itself enforce live uniqueness on SQLite or MySQL. Restoration compares
candidate and live rows in SQL using the database’s collation and checks
candidates against each other before restoration writes. A different language
or site, or a disabled route, is not a conflict. Distinct enabled rows conflict
even with the same Page owner. Parent and translation slugs do not define public
route uniqueness.
Canonical URL saves and typed URLs
Section titled “Canonical URL saves and typed URLs”Normal URL saves have one deliberate recovery exception: saving an untyped canonical URL may reclaim a path held by that same Page’s own alias or redirect. The Page owner includes both the morph type and ID; a different owner still collides. This lets a slug change or revision rollback restore an older canonical path without replacing a historical redirect row.
The exception does not apply when saving an alias or redirect. An enabled typed URL cannot take its own Page’s canonical path, even when only unrelated metadata is being saved. Such a row is redundant or creates ambiguous routing; importers and seeders must avoid creating it, or disable it before saving. Updating a row excludes only that row’s own key from the collision query. Restoration keeps the stricter distinct-row rule above and does not use the normal-save exception.
The alias/redirect collision and unchanged persisted paths are covered by
PageUrlObserverTest; canonical reclamation, rollback, owner identity and
site/language isolation are covered by UpdatePageUrlActionTest.
Locks protect the examined rows. Server-backed concurrency, absent-key locking and deadlock behaviour need server database verification; SQLite cannot prove them. This boundary does not cover another connection, raw PDO or external I/O.