Skip to content

refactor(undo): promote snapshots from array shapes to value classes#26

Merged
oxyc merged 1 commit into
masterfrom
chore/stan-tighten-types
May 29, 2026
Merged

refactor(undo): promote snapshots from array shapes to value classes#26
oxyc merged 1 commit into
masterfrom
chore/stan-tighten-types

Conversation

@oxyc

@oxyc oxyc commented May 29, 2026

Copy link
Copy Markdown
Member

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 ($snap['nme'] typo passes PHPStan, blows up at runtime)
  • the shape lives in PHPDoc only — easy to lie about
  • consumers re-state the field list at every call site

This PR is the direct follow-up: promote the five snapshot kinds to small value classes under src/Undo/Snapshots/. Each one:

  • typed readonly properties (real PHP type checks, not just PHPDoc)
  • 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

Class Purpose
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 a delete can be restored under the original id (existing references stay valid)
TranslationLinkBeforeSnapshot + TranslationLinkRow Polylang group capture

Snapshot stays as a thin 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 accepts both

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. 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 in fromArray() 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_Error instead of a TypeError.

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.
  • Pint clean.

Why arrays for the inner fields

You'll notice TermFieldsSnapshot::fields is still a typed array (array{name: string, slug: string, description: string, parent: int}) — not a sub-class. That matches the WP-style wp_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

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>
@oxyc oxyc merged commit 729c0f4 into master May 29, 2026
2 checks passed
@oxyc oxyc deleted the chore/stan-tighten-types branch May 29, 2026 20:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant