Some rules simply can't live on a single element. "The end date must be after the start date," "the confirmation password must match the original," "this promo code and this shipping country are incompatible" — none of these can be checked by looking at just one field in isolation. That's exactly what the form-wide validateForm() method is for.
What you'll learn in this lesson
- How to override
validateForm()to write genuinely custom, multi-field validation logic - How to read multiple submitted values at once via
$form_state->getValue() - How to attach an error to a specific field even from form-wide validation
- What happens if you don't call
setErrorByName()at all
Overriding validateForm()
public function validateForm(array &$form, FormStateInterface $form_state) {
parent::validateForm($form, $form_state);
$start = $form_state->getValue('start_date');
$end = $form_state->getValue('end_date');
if ($start && $end && $end < $start) {
$form_state->setErrorByName('end_date', $this->t('The end date must be after the start date.'));
}
}
Unlike an #element_validate callback, validateForm() has the entire form's submitted values available through $form_state at once — you're not limited to one element's own value. This is the right place for any rule that genuinely compares two or more fields against each other.
Always call the parent
parent::validateForm($form, $form_state);
If your form class extends FormBase directly, this line does nothing (the base implementation is empty) — but if you ever extend a more specialized base class like ConfigFormBase or EntityForm, skipping the parent call silently discards validation logic those base classes rely on. Calling it unconditionally, even when it's currently a no-op, protects you against a future refactor that changes what you extend.
setErrorByName() — attaching an error to one named field
$form_state->setErrorByName('end_date', $this->t('The end date must be after the start date.'));
Even though the rule itself needed to look at two fields (start_date and end_date) to evaluate, the error message still gets attached to just one of them — whichever field makes the most sense to the person filling out the form. setErrorByName() takes the field's machine name as a plain string (unlike the element-level setError() from the previous lesson, which takes the render array itself).
validateForm() method runs its checks but never calls setError()/setErrorByName() when a rule fails, Drupal has no way of knowing anything went wrong — the form submits as if everything were valid. The validation logic only matters if it actually reports failures back through the form state.See it for yourself
Add a validateForm() override comparing a start-date and end-date field, submit with the end date earlier than the start date, and confirm the error appears attached to the end-date field specifically — not as a generic top-of-form message.
Quick check: your rule needs to compare two fields, but you decide to attach the resulting error to just one of them with
setErrorByName('end_date', ...). Does this mean thestart_datefield is somehow exempt from the check? (Answer: no — the comparison itself still considers both fields' values every time;setErrorByName()only controls where the resulting message visually anchors, not which fields the underlying logic actually examined.)
Key takeaways
- Override
validateForm(array &$form, FormStateInterface $form_state)for any rule that needs to compare multiple fields at once. - Always call
parent::validateForm($form, $form_state)first, even when your base class's implementation is currently a no-op. $form_state->setErrorByName('field_name', $message)attaches an error to one named field from form-wide validation code.- A validation rule that never calls
setError()/setErrorByName()has no effect — Drupal only blocks submission on an explicit error report.
Coming up next
File uploads bring their own validation concerns — file type, size, and more — that don't fit neatly into either the element-level or form-level patterns you've seen so far. Next, you'll see the dedicated #upload_validators property built specifically for that job.