refactor(form-field): extract legacy conditional_logic shape normaliser

Three byte-identical copies of `normaliseLegacyGroupShape` lived in
FormFieldService, StoreFormFieldRequest, and UpdateFormFieldRequest.
WS-5d (form_fields.options) would have been the fourth copy. Hoist
the helper to a single public static on FormFieldConditionalLogicService
and have all three call sites delegate.

Implementation:

  - `FormFieldConditionalLogicService::normaliseLegacyShape(array)` —
    pure recursive passthrough. Translates the ARCH §8 JSON group shape
    (`{"all": [...]}` / `{"any": [...]}`) into the service's internal
    `{"operator", "children"}` form. Does NOT validate; malformed shapes
    return as-is and surface downstream as
    `InvalidConditionalLogicSpecException` from `assertSpecsValid`.
  - Group operator catalogue sourced from
    `FormFieldConditionalLogicGroupOperator::values()` instead of an
    `['all', 'any']` literal — single source of truth for future
    operator additions.
  - All three call sites switched to the static method. The two
    FormRequests reach it via the existing `use` import; FormFieldService
    sits in the same namespace.

Behaviour preserved exactly:

  - Existing FormFieldApiTest (cyclic logic rejection),
    FormFieldStrictConditionalLogicRequestTest (strict-validator
    rejection paths), and FormFieldConditionalLogicServiceTest
    (service-level paths) all green without modification.

New unit tests pin the passthrough contract (8 tests):

  - Valid ALL / ANY translations
  - Recursive nested-group translation (depth 2)
  - Internal shape unchanged
  - Condition leaf passthrough
  - Unknown group key (`xor`) returned unchanged for downstream
    `assertSpecsValid` to reject
  - Empty array unchanged
  - Non-array children stripped silently

Tests: 1150 → 1158 green (3110 → 3124 assertions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-25 00:57:06 +02:00
parent 64f5855fdb
commit 2656818c35
6 changed files with 205 additions and 115 deletions

View File

@@ -115,6 +115,57 @@ final class FormFieldConditionalLogicService
$this->assertNodeValid($tree, isRoot: true);
}
/**
* Translate the legacy ARCH §8 JSON group shape (`{"all": [...]}` /
* `{"any": [...]}`) into the service's internal tree form
* (`{"operator": "all"|"any", "children": [...]}`). Pure recursive
* passthrough does NOT validate; malformed shapes are returned
* as-is and surface as `InvalidConditionalLogicSpecException` from
* the downstream `assertSpecsValid`. Static so both the service path
* (`FormFieldService::extractConditionalLogicTree`) and the boundary
* validators (Store/Update FormRequests' `after()` hooks) can call
* it without container resolution.
*
* Group operator catalogue is sourced from
* `FormFieldConditionalLogicGroupOperator::values()` so a future
* operator addition lands in one place.
*
* @param array<string, mixed> $node
* @return array<string, mixed>
*/
public static function normaliseLegacyShape(array $node): array
{
if (isset($node['field_slug'])) {
return $node;
}
if (isset($node['operator'], $node['children']) && is_array($node['children'])) {
$children = [];
foreach ($node['children'] as $child) {
if (is_array($child)) {
$children[] = self::normaliseLegacyShape($child);
}
}
return ['operator' => $node['operator'], 'children' => $children];
}
foreach (FormFieldConditionalLogicGroupOperator::values() as $candidate) {
if (isset($node[$candidate]) && is_array($node[$candidate])) {
$children = [];
foreach ($node[$candidate] as $child) {
if (is_array($child)) {
$children[] = self::normaliseLegacyShape($child);
}
}
return ['operator' => $candidate, 'children' => $children];
}
}
return $node;
}
/**
* Cross-field cycle detection (ARCH §8; contract preserved from the
* pre-WS-5c `FormFieldService::assertNoConditionalCycle`). Builds a