Statically enforce that CanvasConfigUpdater Component update paths are wired into ::preSave() (and ship a post_update)
## Overview While fixing #3591607 (MR !1175) we hit an easy-to-miss class of bug: a `CanvasConfigUpdater` method that updates a `Component` was given a `hook_post_update_N` but was **not** also invoked from `Component::preSave()`. Because a post-update only runs once per site (and never for imported config), a component re-saved outside the update path silently keeps its outdated data. See [!1175 note 1070980](https://git.drupalcode.org/project/canvas/-/merge_requests/1175#note_1070980) and [note 1070982](https://git.drupalcode.org/project/canvas/-/merge_requests/1175#note_1070982). Nothing currently stops a contributor from adding another such method and forgetting the `::preSave()` wiring — it's hard to catch in review. As @wimleers noted, this is a good candidate for deterministic, attribute-driven enforcement rather than relying on humans (or an LLM) to remember. ## Proposed resolution Add metadata + custom PHPStan rules: - `#[ComponentPreSaveUpdate(postUpdate: '…')]` — marks a `CanvasConfigUpdater` method that must heal a `Component` on save, and records the one-time update path that heals already-stored config. - `#[NotAppliedOnComponentPreSave(reason: '…')]` — explicit opt-out, so "must not run on save" is a stated decision, not an omission. - Rule 1: every `#[ComponentPreSaveUpdate]` method is (a) invoked from `Component::preSave()` and (b) names a `post_update` function that exists. - Rule 2: every public, non-`needs*`, `Component`-taking `CanvasConfigUpdater` method declares exactly one of the two attributes — so forgetting to even consider preSave fails the build. - Annotate the existing updater methods (required-flag, text-value, prop-order, category-unset, multi-branch reference, list_float hash recompute). ## Remaining tasks - [x] Land the two attributes in `src/Attribute/`. - [x] Land the two rules in `phpstan_rules/` and register them in `phpstan.neon`. - [x] Annotate the existing updater methods. - [ ] (Optional) Extend Rule 1 to assert the named `post_update` actually *calls* the method, not just that it exists. - [ ] (Optional) Consider the same guard for other entities' updaters (e.g. `updateJavaScriptComponent` → `JavaScriptComponent::preSave()`). ## UI changes None — developer-facing static analysis only. ## Related - Follow-up to #3591607. - Related: #3578767 (evaluating third-party PHPStan rule packages); this is a project-specific custom rule instead.
issue