| Option | Default | Description |
|---|---|---|
classes.form_layout |
required | Concrete layout entity |
classes.form_layout_field |
required | Concrete field entity |
classes.form_answer |
required | Concrete answer entity |
classes.form_answer_field_value |
required | Concrete value entity |
file_url_resolver |
required | Service id implementing FormAnswerFileUrlResolverInterface |
upload_path |
%kernel.project_dir%/uploads/form-files/ |
Where uploaded answer files are stored |
tel.pattern |
/^\+?[0-9]+$/ |
Regular expression a TelInput value must match |
tel.min_length / tel.max_length |
6 / 15 |
Length bounds of a TelInput value (equal values ⇒ exact length) |
form_theme |
@FormBuilder/form_theme/default.html.twig |
Twig theme for the bundle's own form types |
answers_template |
@FormBuilder/answers/shadcn.html.twig |
Template whose answers_view block renders answers |
answers_pdf_template |
same as answers_template |
Same, when form_builder_answers(…, true) |
The bundle also registers @FormBuilder/form_theme/structure.html.twig (the builder widget) in
twig.form_themes, and Doctrine resolve_target_entities for the four contracts.
| Service | Purpose |
|---|---|
Form\FormTypeGenerator |
buildForm(array $structure, array $data = [], array $options = []), buildNamedForm($name, …) |
Answer\AnswerGenerator |
generate($structure, $answerData) → structure enriched with values; flattenToFields() |
File\FormFileUploader |
upload(), getFile($filename), removeFile(?array), hydrate(). Stored names are random (<32 hex>.<ext>), the extension is guessed from the content, and any name that is not a plain file name (.., /, NUL…) is refused |
Service\AnswerFiles |
of($answer), filenames($answer), owns($answer, $filename) — the files an answer actually contains (also inside FieldSet, Repeatable, conditional blocks) |
UseCase\SyncFormLayoutStructureUseCase |
Merge a structure into a layout that may already have answers |
UseCase\DeleteRemovedFormLayoutFieldValuesUseCase |
Delete values of removed fields, returns the count |
| Type | Options |
|---|---|
FormType\FormStructureType |
answers_count (int, default 0), locale (string, default: request locale; the builder UI is available in fr and en, any other locale falls back to English), categories / types (`null |
FormType\FormBuilderType |
edit (bool). Root type used by FormTypeGenerator |
FormType\DatePickerType, DateTimePickerType |
Wrappers over DateType / DateTimeType |
FormType\ConditionalFieldType |
Wraps a conditional block (inherit_data); options block, owner, operator, option_label |
| Name | Signature |
|---|---|
form_builder_answers |
(array $answers, FormAnswerInterface $formAnswer, bool $pdf = false) → HTML |
form_builder_file_url |
(FormAnswerInterface $formAnswer, string $filename) → URL from your resolver |
formImageFileUri |
(string $filename) → file://… path or null |
convertToBase64 |
(string $filename) → data URI or null |
form_builder_date_format |
(array $block) → PHP date format for a DateTimeInput answer (d/m/Y, H:i, d/m/Y H:i) |
A structure is a list of blocks:
[
{"id": "name", "type": "TextInput", "label": "Name", "required": true},
{"id": "grp", "type": "FieldSet", "children": [{"id": "city", "type": "TextInput", "label": "City"}]},
{"id": "intro", "type": "Title", "text": "Section"}
]id and type are mandatory (blocks without them are skipped). label and text are stored in their own
columns; every other key goes to the field's config JSON column. children nests blocks. id is the answer
key and must be unique in the layout.
| Kind | Types | Has a value |
|---|---|---|
| DisplayOnly | Title, Paragraph (use text) |
no |
| Container | FieldSet |
no, groups children |
| Repeatable | Repeatable |
rows of its children |
| Value | everything else | yes |
Select and ChoiceGroup accept conditions: rules {id, operator: "is" | "is_not", optionId, children: [blocks]}.
The children are shown only while the rule holds for the owner's answer (is: the option is chosen; is_not: it
is not). Internally the structure is stored flat: each conditional block is a sibling of its owner, marked
condition: {owner, rule}, and the owner keeps its rules without children. getStructure() nests them back, and
answers stay at the owner's level (also inside a Repeatable or FieldSet). A conditional block is never
required, and is skipped when its option no longer exists. ConditionalStructure::flatten() / nest() convert
between the two forms; FormLayoutBlock::listFromArray() always flattens.
| Type | Config keys (besides id, label) |
|---|---|
TextInput, EmailInput, TelInput, UrlInput |
required, readOnly, placeHolder, helpText, defaultValue |
TextareaInput |
same + rows |
NumberInput |
required, readOnly, helpText, placeHolder, defaultValue, min, max, step (a non-integer step selects the decimal type); legacy allowDecimal |
DateTimeInput |
required, readOnly, helpText, mode (date, time, datetime-local), hasCurrentDate; legacy showDate / showHour when mode is absent |
Select |
options ([{label, isSelected?}], extra keys such as id ignored), multiple, checkCases (radio/checkbox), customOption, required, readOnly, helpText |
ChoiceGroup |
options ([{label}]), multiple (checkboxes instead of radios), required, readOnly, helpText. Always expanded. Value: the option label (list if multiple) |
HourMinuteInput |
required, readOnly, helpText, defaultValue (HH:MM). Value: string HH:MM:SS (seconds 00) |
FileInput |
acceptedFile, allowMultiple (false ⇒ one file; otherwise up to maxItems, default 5), required, helpText |
AddressInput |
required, helpText, placeHolder |
Signature |
required, helpText |
FieldSet |
children |
Repeatable |
children, maxItems (default 5; 0 = unlimited) |
Title |
text, heading (h1…h6, default h3) |
Paragraph |
text |
Categories group the types in the builder's "Add a block" menu and drive the FormStructureType restriction
(categories option). They are a builder concept: they are not stored in a structure. The category of each
type is exposed in assets/builder/contract/blocks.json and mirrored by Enum\FieldCategory (checked by a test).
| Category | Types |
|---|---|
text |
TextInput, TextareaInput, EmailInput, TelInput, UrlInput |
choice |
Select, ChoiceGroup |
number |
NumberInput |
datetime |
DateTimeInput, HourMinuteInput |
file |
FileInput |
special |
AddressInput, Signature |
layout |
Title, Paragraph |
group |
FieldSet, Repeatable |
getAnswerData() / setAnswerData() use a map fieldId → value:
| Field | Value |
|---|---|
| Simple fields | scalar, or list of labels for multiple Select / ChoiceGroup |
HourMinuteInput |
string HH:MM:SS (seconds 00) |
DateTimeInput |
JSON form of a DateTime (date, timezone_type, timezone) |
Repeatable |
list of rows: [{childId: value}, …] (stored per row index) |
FieldSet |
nested map under the fieldset id |
FileInput |
list of {file: {filename, originalFilename, extension}} |
Signature |
data URI |
What the application's own form theme must render for the other types: rendering-contract.md.
Form theme (form_theme) must define: fieldset_row, title_row, paragraph_row, repeatable_row,
repeatable_children_prototype, repeatable_child_row, add_btn, remove_btn, address_row,
signature_row, file_repeatable_row, file_repeatable_children_prototype, file_repeatable_child_row,
form_builder_conditional_field_row (wrapper read by the form-builder-condition Stimulus controller:
data-form-builder-condition-owner-value, -operator-value, -option-label-value; hidden fields are disabled).
Collection behaviour comes from the form-builder-collection Stimulus controller (data-prototype,
data-collection-child-selector, data-remove-btn-target, data-add-auto, data-max-items,
data-delete-selector), the add/remove buttons from <template id="add-btn"> / <template id="remove-btn">.
Answers template must define block answers_view (variables answers, formAnswer) and one block per
field type used (FieldSet, Repeatable, Title, Paragraph, TextInput, …, FileInput, Select,
Signature, ChoiceGroup, HourMinuteInput). Extend the default one with {% extends '@FormBuilder/answers/shadcn.html.twig' %} and override
only what you need (e.g. FileInput, fileList for PDF).
Domain form_builder_bundle (fr, en): builder-widget confirmation, theme buttons, signature labels, answers
view. Validation messages are in the validators domain under validators.form_builder.* (fr, en). Override
by providing the same keys in your own translations/ directory. Labels, help and placeholders coming from a
layout structure are user text and are never translated.
- A block with an unknown type throws
UnknownFieldTypeExceptionwhenkernel.debugis on, and is skipped with awarninglog otherwise. - Custom PHP field types cannot be registered yet (
FieldFactoryresolves types in the bundle namespace).