refactor(form-builder): strict validator on save; strip rules.unique fallback

This commit is contained in:
2026-04-24 22:26:44 +02:00
parent 800b1b6c01
commit 64ec4bcc5c
12 changed files with 469 additions and 29 deletions

View File

@@ -26,6 +26,7 @@ final class FormValueService
{
public function __construct(
private readonly FieldAccessService $fieldAccess,
private readonly FormFieldValidationRuleService $validationRuleService,
) {}
/**
@@ -76,17 +77,23 @@ final class FormValueService
}
/**
* Backstop enforcement of form_fields.validation_rules JSON per
* S2c D8. The FormFieldRuleBuilder already surfaces min/max/regex
* shortcuts at the request layer; this is where the deeper checks
* (is_unique, validation_rules.unique) live.
* Backstop enforcement of per-field validation rules. Rules are sourced
* from the relational `form_field_validation_rules` table via
* `FormFieldValidationRuleService::toJsonShape()`, which returns the
* canonical flat bag: `min_value`/`max_value` for numeric fields,
* `min_length`/`max_length` for strings, `regex` for pattern checks.
* The pre-WS-5b ambiguous `min`/`max` keys no longer exist.
*
* Uniqueness: `is_unique` column is the single source of truth (WS-5b
* consolidation the legacy `validation_rules.unique` JSON fallback
* was removed in commit 3).
*
* @return array<int, string>
*/
private function validateAgainstFieldRules(FormField $field, mixed $raw, FormSubmission $submission): array
{
$errors = [];
$rules = is_array($field->validation_rules) ? $field->validation_rules : [];
$rules = $this->validationRuleService->toJsonShape($field->validationRules) ?? [];
if ($field->field_type === FormFieldType::SECTION_PRIORITY->value) {
$shapeErrors = $this->validateSectionPriorityShape($raw, $submission);
@@ -99,19 +106,24 @@ final class FormValueService
return $errors;
}
if (isset($rules['min']) && is_numeric($rules['min']) && is_numeric($raw) && (float) $raw < (float) $rules['min']) {
$errors[] = sprintf('Minimum is %s.', (string) $rules['min']);
if (isset($rules['min_value']) && is_numeric($rules['min_value']) && is_numeric($raw) && (float) $raw < (float) $rules['min_value']) {
$errors[] = sprintf('Minimum is %s.', (string) $rules['min_value']);
}
if (isset($rules['max']) && is_numeric($rules['max']) && is_numeric($raw) && (float) $raw > (float) $rules['max']) {
$errors[] = sprintf('Maximum is %s.', (string) $rules['max']);
if (isset($rules['max_value']) && is_numeric($rules['max_value']) && is_numeric($raw) && (float) $raw > (float) $rules['max_value']) {
$errors[] = sprintf('Maximum is %s.', (string) $rules['max_value']);
}
if (isset($rules['min_length']) && is_numeric($rules['min_length']) && is_string($raw) && mb_strlen($raw) < (int) $rules['min_length']) {
$errors[] = sprintf('Minimaal %d tekens.', (int) $rules['min_length']);
}
if (isset($rules['max_length']) && is_numeric($rules['max_length']) && is_string($raw) && mb_strlen($raw) > (int) $rules['max_length']) {
$errors[] = sprintf('Maximaal %d tekens.', (int) $rules['max_length']);
}
if (isset($rules['regex']) && is_string($rules['regex']) && is_string($raw)
&& @preg_match($rules['regex'], $raw) !== 1) {
$errors[] = 'Value does not match the expected format.';
}
$unique = (bool) $field->is_unique || (bool) ($rules['unique'] ?? false);
if ($unique) {
if ($field->is_unique) {
$scalar = is_scalar($raw) ? (string) $raw : null;
if ($scalar !== null) {
$exists = \App\Models\FormBuilder\FormValue::query()