Zoo for Craft CMS

Deleting safely

The rule

Nothing is deleted on the strength of a stored result.

A finding is a claim about the site at the moment the census ran. Between then and now somebody may have dragged that image into a page, restored the entry that owned that block, or pointed a new template at that asset.

So every element is proved again, here, immediately before it goes — against an index built from the site as it is now, through the same References::inbound() call the census used. Preview and execution sharing one code path is the only version of this that stays true; a preview that builds its own query is a preview of a different deletion.

An element that has picked up a reference since the census is marked left alone and shown on the "Left alone" tab with the reference that saved it. It is not silently skipped.

The guards

1. The trash, by default

Craft keeps soft-deleted elements until garbage collection's softDeleteDuration elapses — thirty days out of the box. That window is the real safety net and it costs nothing to keep.

The permanent-delete button is separate, behind its own permission, and requires the confirmation below.

2. The stamp

Every finding records a fingerprint of the element at census time. If it no longer matches, the delete is refused with "it has been edited since the census ran".

This catches a different kind of staleness from the probes: not "somebody linked to this" but "this is not the thing the census looked at any more". An asset re-uploaded over the top of an unused one keeps its element row and its ID — the stamp includes the file size for exactly that reason.

3. Freshness

A census older than staleAfterMinutes (sixty by default) cannot be acted on at all. Every element is re-proved anyway, so this is not the safety net — it is what stops somebody working through a list from last month one row at a time.

4. Typed confirmation

A permanent delete is armed by typing the number of things selected. A number cannot be typed without having read how many rows are ticked. A fixed word becomes muscle memory inside a week.

5. The backup

A database backup is taken immediately before the first permanent delete in a batch, and its path is recorded on the run. If the backup fails, the delete does not happen — a backup setting that quietly does nothing when the backup fails is worse than not having the setting, because it is the reason somebody agreed to the permanent delete in the first place.

Batches are refused, not truncated

A selection larger than maximumBatchSize is rejected with a message saying so. A silently shortened delete looks exactly like a completed one, right up until somebody notices half the list is still there.

What actually gets deleted

AuditWhat the button does
Unused assetsDeletes the element. Permanently deleting it removes the file from its volume.
Unreferenced entriesDeletes the entry. Children are promoted, not deleted.
Orphaned nested entriesDeletes the block.
Abandoned draftsAlways permanent — there is nowhere for a deleted draft to go.
Empty categories / unused tagsDeletes the element.
Empty asset foldersRemoves the folder, after re-checking that it is still empty.
Dormant usersAlways to the trash, whatever the mode says.
Dangling relationsDeletes the row, after re-checking that the target is still missing.
Fields / entry typesA project-config change. Permanent, and deploys to production.

Every one of those re-checks its own precondition at the moment of deletion. An entry type that gained its first entry between the census and the click is in use, whatever the finding says.

The ledger

Zoo → History keeps the record of everything Zoo has deleted: what it was, which audit found it, how big it was, whether it went to the trash or for good, when, and by whom.

Those rows survive pruning. Old census runs are cleaned up by garbage collection, but a run that deleted something is kept whatever keepRuns says — that is not a census result any more, it is the record of what Zoo did, and a housekeeping setting should not be able to erase it.

Undoing

  • Trash — Craft's own restore, from the element index's Trashed status, until softDeleteDuration elapses.
  • Permanent — the backup taken before the batch. The path is on the run.
  • Project config (fields, entry types) — revert the change in config/project/ and run php craft up, if the change has not deployed yet.