['required', 'array']], $ruleBuilder->strict($schema)); */ final class FormFieldRuleBuilder { public function __construct( private readonly FormFieldValidationRuleService $validationRuleService, ) {} /** * @return array> */ public function strict(FormSchema $schema): array { return $this->build($schema, strict: true); } /** * @return array> */ public function relaxed(FormSchema $schema): array { return $this->build($schema, strict: false); } /** * @return array> */ private function build(FormSchema $schema, bool $strict): array { $fields = $schema->fields()->get(); $rules = []; foreach ($fields as $field) { if (! (bool) $field->is_portal_visible || (bool) $field->is_admin_only) { continue; } if (in_array($field->field_type, [FormFieldType::HEADING->value, FormFieldType::PARAGRAPH->value], true)) { continue; } $key = 'values.'.$field->slug; $isMulti = $this->isMultiValue($field); $primaryRules = []; if ($strict && (bool) $field->is_required) { $primaryRules[] = $isMulti ? 'present' : 'required'; } else { $primaryRules[] = 'sometimes'; $primaryRules[] = 'nullable'; } if ($isMulti) { $primaryRules[] = 'array'; $rules[$key] = $primaryRules; foreach ($this->itemRulesFor($field, $strict) as $itemRule) { $rules[$key.'.*'][] = $itemRule; } continue; } foreach ($this->scalarTypeRules($field) as $r) { $primaryRules[] = $r; } foreach ($this->validationRuleShortcuts($field) as $r) { $primaryRules[] = $r; } $rules[$key] = $primaryRules; } return $rules; } private function isMultiValue(FormField $field): bool { return in_array($field->field_type, [ FormFieldType::MULTISELECT->value, FormFieldType::CHECKBOX_LIST->value, FormFieldType::TAG_PICKER->value, FormFieldType::AVAILABILITY_PICKER->value, FormFieldType::SECTION_PRIORITY->value, FormFieldType::TABLE_ROWS->value, ], true); } /** * @return array */ private function scalarTypeRules(FormField $field): array { return match ($field->field_type) { FormFieldType::EMAIL->value => ['email:rfc'], FormFieldType::URL->value => ['url'], FormFieldType::NUMBER->value => ['numeric'], FormFieldType::DATE->value => ['date_format:Y-m-d'], FormFieldType::DATETIME->value => ['date'], FormFieldType::BOOLEAN->value => ['boolean'], FormFieldType::PHONE->value => ['regex:/^[+]?[0-9\s\-()]{4,25}$/'], FormFieldType::SELECT->value, FormFieldType::RADIO->value => $this->inOptionsRule($field), default => ['string'], }; } /** * @return array */ private function itemRulesFor(FormField $field, bool $strict): array { $rules = []; if ($strict && (bool) $field->is_required) { $rules[] = 'required'; } else { $rules[] = 'nullable'; } return match ($field->field_type) { FormFieldType::MULTISELECT->value, FormFieldType::CHECKBOX_LIST->value => array_merge($rules, $this->inOptionsRule($field)), FormFieldType::TAG_PICKER->value => array_merge($rules, ['string', 'max:30']), FormFieldType::AVAILABILITY_PICKER->value => array_merge($rules, ['string', 'max:30']), FormFieldType::SECTION_PRIORITY->value => array_merge($rules, ['array:section_id,priority']), FormFieldType::TABLE_ROWS->value => array_merge($rules, ['array']), default => $rules, }; } /** * @return array */ private function inOptionsRule(FormField $field): array { $options = $this->scalarOptions($field); if ($options === []) { return []; } return ['in:'.implode(',', $options)]; } /** * @return array */ private function scalarOptions(FormField $field): array { $options = is_array($field->options) ? $field->options : []; $out = []; foreach ($options as $opt) { if (is_scalar($opt)) { $out[] = (string) $opt; } elseif (is_array($opt) && isset($opt['value']) && is_scalar($opt['value'])) { $out[] = (string) $opt['value']; } elseif (is_array($opt) && isset($opt['label']) && is_scalar($opt['label'])) { $out[] = (string) $opt['label']; } } return $out; } /** * Shortcuts picked up from the relational validation-rules table. * Service-layer FormValueService does the deeper min/max/regex/unique * enforcement — these are quick boundary checks surfaced at the * Request layer when cheap. * * Laravel's `min:N` / `max:N` already do the right thing for both * numeric inputs (value comparison) and strings (length check), so * `min_length`/`min_value` both emit the same `min:N` rule. * * @return array */ private function validationRuleShortcuts(FormField $field): array { $rules = []; $v = $this->validationRuleService->toJsonShape($field->validationRules); if (! is_array($v)) { return $rules; } if (isset($v['min_value']) && is_numeric($v['min_value'])) { $rules[] = 'min:'.(string) $v['min_value']; } if (isset($v['max_value']) && is_numeric($v['max_value'])) { $rules[] = 'max:'.(string) $v['max_value']; } if (isset($v['min_length']) && is_numeric($v['min_length'])) { $rules[] = 'min:'.(string) $v['min_length']; } if (isset($v['max_length']) && is_numeric($v['max_length'])) { $rules[] = 'max:'.(string) $v['max_length']; } if (isset($v['regex']) && is_string($v['regex'])) { $rules[] = 'regex:'.$v['regex']; } return $rules; } }