Upgrading
From < 0.6.0
Resolve pending Modification rows before upgrading if they touch an encrypted attribute: older rows hold its plain text in mod, and approving one now writes that to the column as is. Existing pending rows with ungrouped fields also keep losing them, and votes duplicated by different reasons, or stored under a class name before adding a morph map, still count separately.
- A rejected change can no longer be approved afterwards: send it again.
approve()/disapprove()returnfalsefor any change that’s already decided, and on a direct audit row. ori/modhold the stored form of attributes with an accessor or mutator, an enum,encrypted*,object,timestampor custom class cast. Code readingmodfor display must cast it itself.- With a morph map, votes and
modifier_typenow use the alias. Votes stored earlier under the class name aren’t found by$user->approvals(). - A model with
approvableItemGroups()gets an extra_ungroupeditem when a change touches fields outside its groups. - The audit row of a direct update is written right after the row, so an
updatinglistener no longer sees it throughgetLastModification(). Anupdatedlistener does. - A veto during approval now undoes the vote. An approver voting again gets the same result, but code counting votes after a refused approval will see none.
- Holding a raw SQL expression, or a change to a model loaded without its key, now throws a
LogicException. - After a gated save the instance holds the stored values, not the proposed ones. Code reading
$model->fieldafter a heldsave()must read$model->getLastModification()instead. orinow uses the same format asmod(adateisY-m-d, abooleanistrue/false). Rows already stored keep the old format, so code comparingoriagainst a live value should expect both.- Rows waiting for approval can hold a third key,
raw. Code reading the diff should readori/modby name rather than assume a pair. Modification::forceApprovalUpdate()does nothing on a change that isn’t pending.- A gated save with nothing to decide (no changes, a
touch(), only untracked columns) saves directly and leaves no pending row. If what needs approval lives outside the columns, such as a photo, callforceModification()on that save. That only works from 0.6.1: 0.6.0 ignored it on a gated save and saved directly, so go straight to 0.6.1 or later. - From 0.6.1,
forceModification()covers the next save only. Code relying on it for several saves of an instance must call it before each one. - A manual
Approvable, one not using the trait, must close theModificationinapplyModificationChanges(): setstatustoapprovedorrejectedand save it. If it’s still pending afterwards, the decision counts as refused: the vote and its database effects are undone, with no exception, andapprove()/disapprove()returnfalse. - Creation approval is opt-in. To use
draftmode, add its boolean column to the model’s table (see Creation Approval).
No migration is required.
From < 0.5.0
Modification rows already pending keep their old partial diff, and approving one still replaces the column with that fragment. Resolve them before upgrading.
For array, collection, AsCollection and AsArrayObject attributes, ori/mod now hold the whole value instead of the sub-keys that changed. Code reading sub-keys from them must compare both sides itself. Attributes exposing set() (e.g. schemaless attributes) keep their partial diff.
No migration is required.
From < 0.3.0
Publish and run the new migration:
php artisan vendor:publish --tag=approvals-migrations
php artisan migrate
It adds flow, status, event_type, operation_id, context, occurred_at to modifications, creates modification_items, and adds a nullable modification_item_id to approvals/disapprovals.
Existing rows are backfilled automatically: flow/status are derived from active/is_modification, and from vote counts for already-resolved rows.
No consumer code changes are required. active, is_modification, approve(), disapprove(), and the legacy diff format all keep working unchanged. modification_items stays empty unless a model opts in via approvableItemGroups().