Issue #3591611: Statically enforce that CanvasConfigUpdater Component update paths are wired into ::preSave()

Resolves #3591611 (closed).

What this does

Adds attribute-driven static enforcement so a CanvasConfigUpdater method that heals a Component cannot silently skip its Component::preSave() wiring — the bug found while reviewing #3591607 (closed) (!1175 note 1070980, suggested in note 1070982).

  • #[ComponentPreSaveUpdate(postUpdate: '…')] — marks an updater that must heal a Component on save, and records the one-time post_update that heals already-stored config.
  • #[NotAppliedOnComponentPreSave(reason: '…')] — explicit opt-out, so "must not run on save" is a stated decision rather than an omission.
  • ComponentPreSaveUpdateMethodsRule — every #[ComponentPreSaveUpdate] method must be (a) invoked from Component::preSave() and (b) name a post_update that exists.
  • ComponentConfigUpdaterMustDeclarePreSaveIntentRule — every public, non-needs*, Component-taking updater must declare exactly one of the two attributes, so forgetting to even consider preSave fails the build.
  • Annotates the five existing updaters (required-flag, text-value, prop-order, category-unset, multi-branch reference).

Verification

  • composer phpstan passes.
  • Each failure mode fails the build with a distinct identifier: un-wiring a call → canvas.componentPreSaveUpdate; naming a missing post_update → canvas.componentPreSaveUpdatePostUpdate; a mutator with no attribute → canvas.componentPreSaveUpdateIntent.

UI changes

None — developer-facing static analysis only.

AI-Generated: Yes (Claude Code (Opus 4.8) drafted the attributes, the two custom PHPStan rules, and this description; human-reviewed.)

Merge request reports

Loading