refactor(undo): promote snapshots from array shapes to value classes#26
Merged
Conversation
Last week's level-6 PHPDoc burn-down gave Snapshot::postFields() etc. a
typed `array<string, mixed>` return. Plain shapes leave half the value
on the table: bugs only surface at first use, the shape lives in PHPDoc
(easy to lie about), and consumers re-state the field list at every
call site.
Promotes each of the five snapshot kinds to a small value class under
src/Undo/Snapshots/. Each class:
- typed readonly properties (real PHP type checks, not just PHPDoc)
- `static capture(...)` factory (constructed from WP)
- `static fromArray(array)` factory (constructed from a persisted
audit row — defensive about missing fields so older rows survive)
- `toArray()` for the JSON wire format
Classes:
- PostFieldsSnapshot — full post for an undoable edit
- PartialPostSnapshot — minimal capture for bulk-update
- TermFieldsSnapshot — term core fields + meta
- TermForRecreateSnapshot — extends TermFieldsSnapshot; adds
term_taxonomy_id + object assignments
so the restore can re-insert with the
original id and reattach posts
- TranslationLinkBeforeSnapshot + TranslationLinkRow — Polylang
group capture
Snapshot stays as a thin namespaced facade — abilities mostly want a
one-liner. Each helper now returns the typed class (or null when the
target's gone) instead of an array. Reversible::reversible() accepts
either an array (for ad-hoc payloads that don't fit a class — restore-
redirect, restore-feed, etc.) or a snapshot object and calls toArray()
at the wire boundary.
RestoreSnapshot's restore handlers all switched to the new fromArray()
factories. The two access patterns that did `$undo['fields']['name']`
became `$undo->fields['name']` (the inner array still uses an
array shape because it's WP-style core fields, deliberately).
Tests: existing RestoreSnapshotTest needed a tiny helper update —
restore() now accepts `array|object` and calls toArray() if needed.
All 373 tests still pass. PHPStan clean at level 6 with no baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Last week's level-6 PHPDoc burn-down gave
Snapshot::postFields()etc. a typedarray<string, mixed>return. Plain shapes leave half the value on the table:$snap['nme']typo passes PHPStan, blows up at runtime)This PR is the direct follow-up: promote the five snapshot kinds to small value classes under
src/Undo/Snapshots/. Each one:static capture(...)factory (constructed from WP —get_post,get_term,pll_get_post_translations)static fromArray(array)factory (constructed from a persisted audit row — defensive about missing fields so older audit rows survive)toArray()for the JSON wire format (audit log)Classes
PostFieldsSnapshotPartialPostSnapshotTermFieldsSnapshotTermForRecreateSnapshotterm_taxonomy_id+ object assignments so a delete can be restored under the original id (existing references stay valid)TranslationLinkBeforeSnapshot+TranslationLinkRowSnapshotstays as a thin facade — abilities mostly want a one-liner. Each helper now returns the typed class (ornullwhen the target's gone) instead of an array.Reversible accepts both
Reversible::reversible()accepts either anarray(for ad-hoc payloads that don't fit a class —restore-redirect,restore-feed, etc.) or a snapshot object and callstoArray()at the wire boundary. That keeps the change focused: snapshots that already had a clear shape get classes; one-off payloads stay as arrays.RestoreSnapshot
All restore handlers switched to the new
fromArray()factories. The defensive parsing infromArray()means an audit row written by an older release doesn't crash the restore path — missing fields become empty defaults and the caller returns a clean "snapshot was missing X"WP_Errorinstead of a TypeError.Tests
Existing
RestoreSnapshotTestneeded a tiny helper update —restore()now acceptsarray|objectand callstoArray()if needed.Why arrays for the inner
fieldsYou'll notice
TermFieldsSnapshot::fieldsis still a typed array (array{name: string, slug: string, description: string, parent: int}) — not a sub-class. That matches the WP-stylewp_update_term()argument shape and keeps the array passable to core directly. Composite shapes get classes; raw WP arg arrays stay arrays.🤖 Generated with Claude Code