*/ final class FormFieldFactory extends Factory { protected $model = FormField::class; /** @return array */ public function definition(): array { $fieldType = fake()->randomElement([ FormFieldType::TEXT, FormFieldType::TEXTAREA, FormFieldType::EMAIL, FormFieldType::NUMBER, FormFieldType::BOOLEAN, FormFieldType::SELECT, ]); $label = fake('nl_NL')->randomElement([ 'Voornaam', 'Achternaam', 'E-mail', 'Telefoon', 'Opmerkingen', 'Shirtmaat', 'Allergieën', 'Motivatie', 'Geboortedatum', ]); return [ 'form_schema_id' => FormSchema::factory(), 'form_schema_section_id' => null, 'library_field_id' => null, 'field_type' => $fieldType->value, 'slug' => Str::slug($label).'-'.Str::lower(Str::random(4)), 'label' => $label, 'help_text' => fake()->boolean(30) ? fake('nl_NL')->sentence() : null, 'is_required' => fake()->boolean(40), 'is_filterable' => false, 'is_portal_visible' => true, 'is_admin_only' => false, 'is_unique' => false, 'is_pii' => false, 'display_width' => FormFieldDisplayWidth::FULL, 'role_restrictions' => null, 'translations' => null, 'value_storage_hint' => $fieldType->recommendedValueStorageHint(), 'review_required' => false, 'sort_order' => 0, ]; } /** * Attach option rows in `form_field_options` after the field is * persisted. Replaces populating the legacy `options` JSON column * (WS-5d commit 2). Pass either flat strings (each becomes * value+label) or full spec arrays. * * @param list}> $values */ public function withOptions(array $values): static { return $this->afterCreating(function (FormField $field) use ($values): void { $specs = []; foreach (array_values($values) as $i => $entry) { if (is_string($entry)) { $specs[] = ['value' => $entry, 'label' => $entry, 'sort_order' => $i]; continue; } $specs[] = [ 'value' => (string) $entry['value'], 'label' => (string) $entry['label'], 'sort_order' => $entry['sort_order'] ?? $i, 'translations' => $entry['translations'] ?? null, ]; } app(FormFieldOptionService::class)->replaceOptions($field, $specs); }); } public function ofType(FormFieldType $type): static { return $this->state(fn () => [ 'field_type' => $type->value, 'value_storage_hint' => $type->recommendedValueStorageHint(), ]); } public function filterable(): static { return $this->state(fn () => ['is_filterable' => true]); } /** * Attach an entity-binding row in `form_field_bindings` after the field * is persisted. Use this instead of populating the legacy `binding` JSON * column — which WS-5a will drop in commit 3. */ public function withEntityBinding( string $entity, string $attribute, FormFieldBindingMode $mode = FormFieldBindingMode::EntityOwned, ?string $syncDirection = null, ): static { return $this->afterCreating(function (FormField $field) use ($entity, $attribute, $mode, $syncDirection): void { FormFieldBinding::factory() ->forField($field) ->state([ 'target_entity' => $entity, 'target_attribute' => $attribute, 'mode' => $mode->value, 'sync_direction' => $mode === FormFieldBindingMode::Mirrored ? ($syncDirection ?? 'write_on_submit') : null, ]) ->create(); }); } /** * Attach a validation-rule row in `form_field_validation_rules` after * the field is persisted. Replaces populating the legacy * `validation_rules` JSON column — which WS-5b commit 5 drops. * * @param array $parameters */ public function withValidationRule(FormFieldValidationRuleType $type, array $parameters): static { return $this->afterCreating(function (FormField $field) use ($type, $parameters): void { FormFieldValidationRule::factory() ->forField($field) ->ofType($type, $parameters) ->create(); }); } /** * Build a conditional-logic tree for this field after persistence. * `$tree` mirrors the canonical ARCH §8 JSON shape without the * outer `show_when` wrapper — the root must be a group. * * Example: * * $factory->withConditionalLogic([ * 'operator' => 'all', * 'children' => [ * ['field_slug' => 'gate', 'operator' => 'equals', 'value' => 'yes'], * [ * 'operator' => 'any', * 'children' => [ * ['field_slug' => 'region', 'operator' => 'equals', 'value' => 'NL'], * ['field_slug' => 'region', 'operator' => 'equals', 'value' => 'BE'], * ], * ], * ], * ]); * * @param array $tree */ public function withConditionalLogic(array $tree): static { return $this->afterCreating(function (FormField $field) use ($tree): void { self::buildLogicTree($field->id, null, $tree, 0); }); } /** * Recursive walker used only by `withConditionalLogic`. The service * layer (WS-5c commit 2) owns the canonical write path; this helper * stays minimal and keeps factories self-contained. * * @param array $node */ private static function buildLogicTree(string $fieldId, ?string $parentId, array $node, int $sortOrder): void { if (isset($node['field_slug'])) { /** @var string $slug */ $slug = $node['field_slug']; $rawOperator = isset($node['operator']) && is_string($node['operator']) ? $node['operator'] : FormFieldConditionalLogicConditionOperator::Equals->value; $operator = FormFieldConditionalLogicConditionOperator::from($rawOperator); FormFieldConditionalLogicCondition::factory()->create([ 'group_id' => $parentId, 'field_slug' => $slug, 'comparison_operator' => $operator->value, 'value' => $operator->isValueless() ? null : ($node['value'] ?? null), 'sort_order' => $sortOrder, ]); return; } $rawOperator = isset($node['operator']) && is_string($node['operator']) ? $node['operator'] : FormFieldConditionalLogicGroupOperator::All->value; $groupOperator = FormFieldConditionalLogicGroupOperator::from($rawOperator); $group = FormFieldConditionalLogicGroup::factory()->create([ 'form_field_id' => $fieldId, 'parent_group_id' => $parentId, 'operator' => $groupOperator->value, 'sort_order' => $sortOrder, ]); /** @var array> $children */ $children = $node['children'] ?? []; foreach ($children as $childSortOrder => $child) { self::buildLogicTree($fieldId, $group->id, $child, $childSortOrder); } } }