Auto-gegenereerd via Reflection — blijft altijd in sync met src/.
CompositeRule
Framework\Form\Conditional\CompositeRuleA composite conditional: multiple ConditionalRules combined with AND or OR.
Serialises to:
{"logic": "and", "rules": [{"field":"x","operator":"equal","value":"y"}, ...]}
Usage:
CompositeRule::all(
ConditionalRule::whenEqual('type', 'business'),
ConditionalRule::whenEqual('region', 'nl'),
)
4 public methods
static all(\ConditionalRule ...$rules = ?): selfAll rules must pass (AND).
static any(\ConditionalRule ...$rules = ?): selfAt least one rule must pass (OR).
toArray(): arraytoJson(): stringConditionalRule
Framework\Form\Conditional\ConditionalRuleA single conditional rule: show/hide a field when another field's value
satisfies a comparison.
This value object serialises to the JSON format expected by form-enhanced.js:
{"field": "type", "operator": "equal", "value": "business"}
Usage:
ConditionalRule::when('type', RuleOperator::Equal, 'business')
__construct(string $field, \RuleOperator $operator, string $value)4 public methods
toArray(): arraySerialise to the JSON structure expected by form-enhanced.js.
toJson(): stringstatic when(string $field, \RuleOperator $operator, string $value): selfstatic whenEqual(string $field, string $value): selfShorthand for the most common case: show when field equals value.
RuleEvaluator
Framework\Form\Conditional\RuleEvaluatorEvaluates ConditionalRule / CompositeRule against a flat values map.
Extracted from Form::process() so that ConditionalValidator (or any other
code that needs to test a rule against submitted values) can reuse the
exact same semantics without duplication.
1 public method
evaluate(\ConditionalRule|\CompositeRule $rule, array $values): boolRuleOperator
Framework\Form\Conditional\RuleOperatorComparison operators for conditional field rules.
These values are serialised as strings in the data-conditional JSON
attribute consumed by form-enhanced.js on the client.
3 public methods
static cases(): arraystatic from(string|int $value): staticstatic tryFrom(string|int $value): ?staticEqual, NotEqual, Contains, NotContains, GreaterThan, LessThan, GreaterEqual, LessEqualFormSchemaException
Framework\Form\Exception\FormSchemaExceptionThrown when a JSON form schema cannot be parsed or contains invalid data.
The message always describes which part of the schema is invalid so
CMS developers get actionable feedback.
__construct(string $message = '', int $code = 0, ?Throwable $previous = NULL)5 public methods
static invalidJson(string $error): selfstatic missingKey(string $key, string $context = ''): selfstatic unknownOperator(string $operator): selfstatic unknownType(string $type, string $context = ''): selfstatic unknownValidator(string $type): selfAbstractField
Framework\Form\Field\AbstractFieldBase class for typed form field definitions.
A field is a pure data+metadata object — it carries no HTML knowledge.
The FormRenderer decides how to present each field (label placement,
wrapper elements, error styles, etc.).
Usage:
TextField::create('first_name')
->label('Voornaam')
->placeholder('Jan')
->required();
21 public methods
addValidator(\ValidatorInterface $validator): staticstatic create(string $name): staticdefaultValue(string $value): staticdisabled(bool $disabled = true): staticgetConditional(): \ConditionalRule|\CompositeRule|nullgetConditionalJson(): ?stringSerialise the conditional rule to the JSON format used by form-enhanced.js.
Returns null when no conditional is set.
getDefaultValue(): stringgetHint(): stringgetLabel(): stringgetPlaceholder(): stringgetValidators(): arrayhasConditional(): boolhint(string $hint): staticA short helper text shown below the field input.
Example: hint('Gebruik uw zakelijk e-mailadres')
isComposite(): boolComposite fields hebben sub-inputs (`name[part]`) en eigen render-/validate-pad.
Default = false; CompositeFieldInterface-implementations overschrijven.
isDisabled(): boolisRequired(): boollabel(string $label): staticplaceholder(string $placeholder): staticrequired(bool $required = true): staticshowWhen(\ConditionalRule|\CompositeRule $rule): staticMark this field as conditionally visible.
Example — show only when another field equals a certain value:
->showWhen(ConditionalRule::whenEqual('type', 'business'))
Example — show when multiple conditions are met:
->showWhen(CompositeRule::all(
ConditionalRule::whenEqual('type', 'business'),
ConditionalRule::whenEqual('region', 'nl'),
))
validate(string $value, array $allValues = array (
)): arrayRun all validators against $value.
Returns a list of error messages (empty = valid).
AddressField
Framework\Form\Field\AddressFieldComposite veld voor postadressen.
Submit-shape: `<name>[street]`, `<name>[number]`, `<name>[zipcode]`,
`<name>[city]`, en optioneel `<name>[country]`.
Default-opmaak: NL — postcode + plaats op één regel, daaronder straat + nr.
7 public methods
getDefaultCountry(): stringgetIncludeCountry(): boolgetParts(): arrayincludeCountry(bool $on = true, string $default = 'NL'): staticisComposite(): boolrenderComposite(array $value = array (
)): stringvalidateComposite(array $value, array $allValues = array (
)): arrayCheckboxField
Framework\Form\Field\CheckboxFieldA single <input type="checkbox">.
Het label naast de checkbox wordt gezet via {@see checkboxLabel()}. Dat
kan ofwel een plain string zijn (auto-escaped) ofwel een {@see NodeInterface}
(El, ElCollection, fragment) — handig om links in te bouwen, bv:
CheckboxField::create('terms')
->checkboxLabel(El::fragment()
->text('Ik accepteer de ')
->add(El::make('a', ['href' => '/voorwaarden'])->text('algemene voorwaarden'))
);
Voor het veelvoorkomende "tekst — link — tekst"-patroon is er de helper
{@see checkboxLabelWithLink()} zodat je geen El-builder hoeft te kennen.
3 public methods
checkboxLabel(\NodeInterface|string $label): staticStel het label naast de checkbox in. String is plain-text (auto-escaped).
NodeInterface wordt rendered as-is — gebruik dat voor inline-links of formatting.
checkboxLabelWithLink(string $prefix, string $href, string $linkText, string $suffix = '', bool $external = true): staticConvenience: tekst met één inline-link (en optioneel een suffix).
->checkboxLabelWithLink('Ik accepteer de ', '/terms', 'algemene voorwaarden', '.')
→ Ik accepteer de [algemene voorwaarden](/terms).
`$external = true` voegt `target="_blank"` + `rel="noopener noreferrer"` toe.
getCheckboxLabel(): \NodeInterface|stringCompositeFieldInterface
Framework\Form\Field\CompositeFieldInterfaceMarker-interface voor velden die uit meerdere sub-inputs bestaan.
Een composite field heeft één naam in het schema (`klantnaam`) maar levert
server-side meerdere `<input>`-tags op (`klantnaam[first]`, `klantnaam[last]`,
etc.). De submit-data komt binnen als geneste array.
Form::process() en validators behandelen composites anders dan atomic-velden:
- $values[$name] is een array, niet een string
- validate() krijgt de array door, niet trim() of (string)cast
Render: composites leveren hun eigen `renderComposite(): string` die de
sub-inputs in één wrapper plaatst. FormRenderer delegeert daaraan.
3 public methods
getParts(): arrayReturnt de sub-veld-namen + labels.
renderComposite(): stringRender de complete composite-input (sub-inputs in een wrapper).
Output is HTML-string die door FormRenderer rechtstreeks ingevoegd wordt.
validateComposite(array $value, array $allValues = array (
)): arrayValidate sub-data (ipv één string-value). Returns lijst foutmeldingen.
Countries
Framework\Form\Field\CountriesCentrale landen-lijst voor zowel het losse `country`-type
(CountryField via FormFactory) als de `address.country` sub-velden.
Sleutels = ISO-3166-1 alpha-2 codes (uppercase).
Labels = NL-talige landnaam.
Niet uitputtend — meest gebruikte landen + EU. Caller kan eigen lijst
opgeven via `options()` op SelectField of `salutationOptions()`-achtige
setters in andere fields.
2 public methods
static codes(): arraystatic defaults(): arrayCreditCardField
Framework\Form\Field\CreditCardFieldComposite veld voor creditcard-invoer.
Submit-shape: `<name>[number]`, `<name>[expiry]`, `<name>[cvc]`.
Geen pretentie van PCI-compliance — bedoeld als front-end widget;
gevoelige data hoort sowieso niet door de eigen server te gaan, gebruik
een payment-provider tokenisatie. We valideren format (Luhn-check op
card-number, MM/YY-format, 3-4-cijfer cvc).
4 public methods
getParts(): arrayisComposite(): boolrenderComposite(array $value = array (
)): stringvalidateComposite(array $value, array $allValues = array (
)): arrayDateField
Framework\Form\Field\DateFieldSingle-date field. Form-value is altijd ISO `YYYY-MM-DD` (of
`YYYY-MM-DDTHH:MM` als time enabled is). De zichtbare display
komt van de JS-component `public/modules/date-picker.js` en
volgt de actieve locale (of de format-override).
DateField::create('birthdate')
->label('Geboortedatum')
->min('1900-01-01')
->max(date('Y-m-d'))
->locale('nl-NL')
->months(1);
22 public methods
dataAttributes(): arrayBouw de data-attributen voor de wrapper-div (gebruikt door FormRenderer).
defaultPattern(?string $key): staticformat(?string $token): staticgetDefaultPattern(): ?stringgetFormat(): ?stringgetLocale(): ?stringgetMax(): ?stringgetMaxMonths(): intgetMin(): ?stringgetMonths(): intgetMonthsMobile(): intgetPatterns(): arraygetTimeStep(): intisTimeEnabled(): boollocale(?string $code): staticmax(?string $iso): staticmaxMonths(int $count): staticmin(?string $iso): staticmonths(int $count): staticmonthsMobile(int $count): staticpatterns(array $patterns): staticwithTime(bool $enabled = true, int $stepMinutes = 15): staticDateRangeField
Framework\Form\Field\DateRangeFieldRange-date field — twee form-velden (from + till) onder één UI.
DateRangeField::create('stay')
->from('checkin', 'Aankomst')
->till('checkout', 'Vertrek')
->min('2026-01-01')
->max('2027-12-31')
->locale('nl-NL')
->months(2)
->patterns([
['key' => 'weekend', 'label' => 'Weekend', 'anchor' => [5,6,0], 'nights' => 3],
['key' => 'week', 'label' => 'Week', 'anchor' => [5,6,0], 'nights' => 7],
]);
AbstractField->name wordt gebruikt voor de wrapper-id en als prefix voor de
input-namen wanneer from()/till() niet expliciet geset zijn.
28 public methods
dataAttributes(): arraydefaultPattern(?string $key): staticformat(?string $token): staticfrom(string $name, string $label = ''): staticgetDefaultPattern(): ?stringgetFormat(): ?stringgetFromLabel(): stringgetFromName(): stringgetLocale(): ?stringgetMax(): ?stringgetMaxMonths(): intgetMin(): ?stringgetMonths(): intgetMonthsMobile(): intgetPatterns(): arraygetTillLabel(): stringgetTillName(): stringgetTimeStep(): intisTimeEnabled(): boollocale(?string $code): staticmax(?string $iso): staticmaxMonths(int $count): staticmin(?string $iso): staticmonths(int $count): staticmonthsMobile(int $count): staticpatterns(array $patterns): statictill(string $name, string $label = ''): staticwithTime(bool $enabled = true, int $stepMinutes = 15): staticEmailField
Framework\Form\Field\EmailField__construct(string $name)MultiCheckboxField
Framework\Form\Field\MultiCheckboxFieldEen groep checkboxes — meerdere selecties tegelijk mogelijk.
Submit-shape: `<name>[]` (PHP-stijl array). FormResult slaat 'm op als
array van geselecteerde waarden, vergelijkbaar met een composite maar
platter (geen vaste sub-keys, alle items horen tot dezelfde lijst).
In het JSON-schema:
{ "type": "multicheckbox", "name": "talen", "options": {"nl": "Nederlands", ...} }
Niet `composite: true` — gebruikt z'n eigen render-pad maar de submit-data
is een homogene array van strings, niet een geneste struct met sub-keys.
2 public methods
getOptions(): arrayoptions(array $options): staticPersonalNameField
Framework\Form\Field\PersonalNameFieldComposite veld voor persoonsnamen.
Submit-shape:
`<name>[first]`, `<name>[middle]`, `<name>[last]`
en optioneel `<name>[salutation]` als `includeSalutation()` aan staat.
Gebruik:
PersonalNameField::create('klantnaam')
->includeSalutation(true)
->required();
In het JSON-schema:
{ "type": "personalname", "name": "klantnaam", "includeSalutation": true }
8 public methods
getIncludeSalutation(): boolgetParts(): arraygetSalutationOptions(): arrayincludeSalutation(bool $on = true): staticisComposite(): boolrenderComposite(array $value = array (
)): stringsalutationOptions(array $options): staticvalidateComposite(array $value, array $allValues = array (
)): arrayPhoneNumberField
Framework\Form\Field\PhoneNumberFieldTelefoonnummer — atomisch text-veld met `type=tel` en optionele
format-validatie per language. Geen composite (geen sub-velden); de
country-prefix is gewoon onderdeel van de tekstwaarde.
3 public methods
getLanguage(): stringlanguage(string $code): staticvalidate(string $value, array $allValues = array (
)): arrayRadioField
Framework\Form\Field\RadioFieldA group of <input type="radio"> buttons.
Example:
RadioField::create('type')
->label('Account type')
->options(['private' => 'Particulier', 'business' => 'Zakelijk']);
2 public methods
getOptions(): arrayoptions(array $options): staticReCaptchaField
Framework\Form\Field\ReCaptchaFieldForm-field voor Google reCAPTCHA. Drie modi:
ReCaptchaField::create()->siteKey('6Lc...')->mode(ReCaptchaMode::V2_CHECKBOX);
ReCaptchaField::create()->siteKey('6Lc...')->mode(ReCaptchaMode::V3)->action('contact');
De FormRenderer plaatst een placeholder-div (`.g-recaptcha`) en zorgt dat
de Google API geladen wordt. Server-side verificatie gebeurt apart via
{@see \Framework\Security\ReCaptcha\ReCaptchaVerifier} — dit veld zelf
doet alleen de UI.
De default `name` is `g-recaptcha-response` (Google's eigen veld).
13 public methods
action(string $action): staticv3-only — actienaam die je meegeeft aan grecaptcha.execute().
static create(string $name = 'g-recaptcha-response'): staticgetAction(): ?stringgetLocale(): stringgetMode(): \ReCaptchaModegetSiteKey(): stringgetSize(): stringgetTheme(): stringlocale(string $locale): staticmode(\ReCaptchaMode $mode): staticsiteKey(string $key): staticsize(string $size): statictheme(string $theme): staticSelectField
Framework\Form\Field\SelectFieldA `<select>` field.
Default krijgt het veld de `data-mm-select` attribuut, waardoor
`public/modules/custom-select.js` 'm automatisch upgraded naar
een dropdown met search + keyboard navigation. Per veld uit te
zetten met `->customSelect(false)`.
SelectField::create('country')
->label('Land')
->options(['nl' => 'Nederland', 'be' => 'België'])
->placeholder('Kies een land…')
->searchMode('always') // 'auto' | 'always' | 'never'
->searchThreshold(8); // toon search vanaf N opties (mode auto)
8 public methods
customSelect(bool $enabled = true): staticSchakel de custom-select-upgrade in/uit voor dit veld.
getOptions(): arraygetSearchMode(): stringgetSearchThreshold(): intisCustomSelect(): booloptions(array $options): staticsearchMode(string $mode): static'auto' | 'always' | 'never' — wanneer de search-input getoond wordt.
searchThreshold(int $n): staticAantal opties vanaf waar 'auto' search toont.
TextField
Framework\Form\Field\TextField2 public methods
getType(): stringtype(string $type): staticOverride HTML input type (e.g. 'password', 'tel', 'url', 'search').
TextareaField
Framework\Form\Field\TextareaField2 public methods
getRows(): introws(int $rows): staticZipcodeField
Framework\Form\Field\ZipcodeFieldPostcode-veld met optionele "afstand tot"-dropdown.
- Atomic mode: één tekstveld met regex-validatie per language.
- Composite mode (`withDistance(true)`): tekstveld + select met km-keuzes.
Submit-shape:
- zonder distance: `<name>` = "1234 AB"
- met distance: `<name>[code]` = "1234 AB", `<name>[distance]` = "10"
11 public methods
distances(array $km): staticgetDistances(): arraygetLanguage(): stringgetParts(): arraygetWithDistance(): boolisComposite(): boollanguage(string $code): staticrenderComposite(array $value = array (
)): stringvalidate(string $value, array $allValues = array (
)): arrayvalidateComposite(array $value, array $allValues = array (
)): arraywithDistance(bool $on = true): staticForm
Framework\Form\FormA form definition — an ordered collection of fields and layout rows.
Usage with plain fields:
$form = Form::create('contact', '/contact/submit')
->add(TextField::create('name')->label('Naam'))
->add(EmailField::create('email')->label('E-mail'));
Usage with side-by-side layout:
$form = Form::create('registratie', '/submit')
->add(FieldRow::of(
SelectField::create('land')->label('Land'),
TextField::create('telefoon')->label('Telefoonnummer'),
))
->add(FieldRow::of($postcode, $plaats)->widths([1, 2]));
$result = $form->process($_POST);
6 public methods
add(\FormElementInterface $element): selfAdd a field or a FieldRow (side-by-side layout).
static create(string $id, string $action = '', string $method = 'POST'): selfgetElements(): arraygetField(string $name): ?\AbstractFieldgetFields(): arrayprocess(array $data): \FormResultValidate submitted data against all field definitions.
Fields hidden by a conditional rule (given the submitted values)
are skipped entirely — their values are not validated.
FormElementInterface
Framework\Form\FormElementInterfaceMarker interface for anything that can be added to a Form.
Both AbstractField and FieldRow implement this, so Form::add()
accepts either without losing type safety.
FormFactory
Framework\Form\FormFactoryBuilds a Form from a JSON schema string (typically stored in the database
by the CMS form editor).
This is the bridge between CMS storage and the PHP Form Builder renderer.
The renderer knows nothing about JSON; the CMS knows nothing about PHP classes.
Usage:
$form = FormFactory::fromJson($jsonFromDatabase);
$html = (new FormRenderer())->render($form, $result);
Schema shape:
{
"id": "contact",
"action": "/contact/submit",
"method": "POST", // optional, default POST
"elements": [
{
"type": "field",
"field": { ... }
},
{
"type": "row",
"widths": [1, 2], // optional, CSS fr units
"conditional": { ... }, // optional, row-level
"fields": [ { ... }, ... ]
}
]
}
Field shape:
{
"type": "text|email|textarea|select|checkbox|radio",
"name": "field_name",
"label": "Label tekst", // optional
"placeholder": "...", // optional
"hint": "Hulptekst", // optional
"default": "standaard waarde", // optional
"required": true, // optional
"disabled": false, // optional
"options": {"value": "Label"}, // select / radio only
"rows": 4, // textarea only
"inputType": "password", // text only, overrides type attr
"checkboxLabel": "Ik ga akkoord", // checkbox only
"validators": [ ... ], // optional
"conditional": { ... } // optional, field-level
}
Validator shapes:
{"type": "required"}
{"type": "minLength", "min": 2}
{"type": "maxLength", "max": 255}
{"type": "email"}
{"type": "regex", "pattern": "/^[0-9]+$/", "message": "{label} mag alleen cijfers bevatten."}
Conditional shapes (single):
{"field": "type", "operator": "equal", "value": "zakelijk"}
Conditional shapes (composite):
{"logic": "and", "rules": [ {...}, {...} ]}
{"logic": "or", "rules": [ {...}, {...} ]}
2 public methods
static fromArray(array $data): \FormBuild a Form from an already-decoded array.
Useful when the JSON was decoded upstream (e.g. already fetched from DB).
static fromJson(string $json): \FormParse a JSON string and return a fully wired Form object.
FormRenderer
Framework\Form\FormRendererRenders a Form definition into an El tree.
The renderer is the only place where HTML presentation decisions are made.
Fields know nothing about HTML; FormRenderer knows nothing about validation
rules — it only reads field metadata.
1 public method
render(\Form $form, ?\FormResult $result = NULL): stringFormResult
Framework\Form\FormResultImmutable result of Form::process().
Usage:
$result = $form->process($_POST);
if ($result->isValid()) {
$name = $result->value('name'); // atomic veld
$email = $result->value('email');
$klant = $result->valueArray('klantnaam'); // composite veld
}
$errors = $result->errorsFor('email'); // string[]
__construct(array $values, array $errors)7 public methods
allErrors(): arrayallValues(): arrayerrorsFor(string $field): arrayhasErrors(string $field): boolisValid(): boolvalue(string $field, string $default = ''): stringReturns een atomic-value als string. Voor composites: gebruik valueArray().
Composite-waarden vallen terug op `$default` als je ze als string leest.
valueArray(string $field, array $default = array (
)): arrayReturns een composite-value als geneste array. Atomic-waarden vallen
terug op `$default`.
FieldRow
Framework\Form\Layout\FieldRowGroups fields side-by-side in a CSS grid row.
Basic usage — equal columns:
FieldRow::of(
SelectField::create('land')->label('Land'),
TextField::create('telefoon')->label('Telefoonnummer'),
)
Custom column widths (CSS fr units):
FieldRow::of($postcode, $plaats)->widths([1, 2])
// → "1fr 2fr" — postcode ≈ 1/3, plaats ≈ 2/3
The whole row can be conditionally hidden:
FieldRow::of($vat, $chamber)->showWhen(ConditionalRule::whenEqual('type', 'zakelijk'))
Responsive: on screens narrower than 640 px the columns collapse to a
single stack — no extra config needed, the CSS handles it.
7 public methods
getConditionalJson(): ?stringgetFields(): arraygetGridTemplateColumns(): stringCSS grid-template-columns value.
Falls back to "repeat(N, 1fr)" when no custom widths are set.
hasConditional(): boolstatic of(\AbstractField ...$fields = ?): selfCreate a row with equal-width columns.
showWhen(\ConditionalRule|\CompositeRule $rule): selfHide/show the entire row based on another field's value.
widths(array $fractions): selfSet custom column proportions in CSS fr units.
Example: ->widths([1, 2]) produces "1fr 2fr"
(first column gets 1/3 of the space, second gets 2/3)
Must contain the same number of values as there are fields.
ConditionalValidator
Framework\Form\Validator\ConditionalValidatorDecorator that wraps another ValidatorInterface and only delegates to it
when a `when`-rule evaluates true against the other submitted values.
This is what makes "verplicht als type=zakelijk" possible without changing
the inner validator. Used by FormFactory when a validator definition has
a `when`-clausule in its JSON-shape.
__construct(\ValidatorInterface $inner, \ConditionalRule|\CompositeRule $when, \RuleEvaluator $evaluator = \Framework\Form\Conditional\RuleEvaluator::__set_state(array(
)))1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringEmailValidator
Framework\Form\Validator\EmailValidator1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringMaxLengthValidator
Framework\Form\Validator\MaxLengthValidator__construct(int $max)1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringMinLengthValidator
Framework\Form\Validator\MinLengthValidator__construct(int $min)1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringReCaptchaValidator
Framework\Form\Validator\ReCaptchaValidatorServer-side validator voor een reCAPTCHA-token.
$form->add(ReCaptchaField::create()->siteKey($siteKey))
->validator('g-recaptcha-response',
new ReCaptchaValidator(
verifier: new ReCaptchaVerifier($secretKey),
remoteIp: $kernel->request->clientIp(),
));
Voor v3: geef `minScore` mee (default 0.5).
`remoteIp` is optioneel — Google accepteert het verzoek ook zonder, maar
met IP is de risk-scoring iets accurater. Geef 'm bij voorkeur door uit
`ServerRequest::clientIp()`.
__construct(\ReCaptchaVerifier $verifier, ?string $remoteIp = NULL, float $minScore = 0.5, string $message = 'Captcha-validatie mislukt.')1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringRegexValidator
Framework\Form\Validator\RegexValidator__construct(string $pattern, string $message)1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringRequiredValidator
Framework\Form\Validator\RequiredValidator1 public method
validate(string $value, string $label, array $allValues = array (
)): ?stringValidatorInterface
Framework\Form\Validator\ValidatorInterfaceA field validator.
Returns null on success, or a human-readable error message on failure.
1 public method
validate(string $value, string $label, array $allValues = array (
)): ?string