refactor(form-builder): strict validator + drop form_fields.conditional_logic JSON column

WS-5c commit 3 of 4. FormRequests (Store/Update) now reject bad
conditional_logic trees at the HTTP boundary — the `after()` hook
unwraps the `show_when` envelope, normalises legacy `{all|any: [...]}`
group shape to the service's internal form, and delegates to
`FormFieldConditionalLogicService::assertSpecsValid()`. Unknown
operators, root conditions, empty groups, and unknown field_slug
references produce a 422 with a readable error before any write.

`form_fields.conditional_logic` JSON column dropped. FormField model
`$fillable` and `$casts` no longer mention the column; factory default
no longer writes `null` to it. Snapshot fixtures in the dev seeder and
the legacy-forms migration command keep `conditional_logic` in their
snapshot JSON shape — that's the schema_snapshot contract, not the DB
column.

FormFieldController now maps InvalidConditionalLogicSpecException to
422 alongside FrozenSchemaException / CyclicDependencyException.

Rollback path: roll back WS-5c commits 1–3 together. Partial rollback
(drop-column reversed but backfill still applied) is not a supported
state — matching the WS-5a/b precedent on the family's full-rollback
contract.

Tests: 6 new (strict FormRequest rejection cases + JSON-column drop
assertion). Rollback step counts in WS-5a/b migration tests bumped +1
for the drop_conditional_logic_json_column migration. Baseline
1142 → 1148 green (3085 → 3099 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:03:21 +02:00
parent d06ea01b09
commit 079d10975b
12 changed files with 372 additions and 25 deletions

View File

@@ -58,7 +58,6 @@ final class FormField extends Model
'is_unique',
'is_pii',
'display_width',
'conditional_logic',
'role_restrictions',
'translations',
'value_storage_hint',
@@ -69,7 +68,6 @@ final class FormField extends Model
/** @var array<string, string> */
protected $casts = [
'options' => 'array',
'conditional_logic' => 'array',
'role_restrictions' => 'array',
'translations' => 'array',
'is_required' => 'bool',
@@ -141,8 +139,9 @@ final class FormField extends Model
* events are worth logging e.g. created/deleted/restored, field_type
* changed (value storage changes), binding changed, is_pii toggled,
* is_filterable toggled (triggers backfill), structural options changes.
* NOT logged (noise): label/help_text/sort_order/conditional_logic/
* translations.
* Conditional-logic changes emit `field.conditional_logic_replaced`
* via FormFieldConditionalLogicService (ARCH §8; WS-5c commit 2).
* NOT logged (noise): label/help_text/sort_order/translations.
*
* Bulk-fixture suppression: the activitylog.enabled config key is the
* kill-switch. Seeders and one-shot commands wrap themselves in