A bouncer at a club door doesn't stop people from walking up — they stop the wrong people from walking in. validateForm() is Drupal's bouncer: every submission reaches it, but only input that passes every check gets waved through to submitForm(). Reject something here, and the visitor sees the form again with a clear error, their other answers still intact.
What you'll learn in this lesson
- How
validateForm()fits between submission and processing - How to reject a specific field with
setErrorByName(), and why the field key matters - How to reject the form for a reason that isn't tied to one field
- Why some validation belongs on the element itself instead of in this method
A real validation method
public function validateForm(array &$form, FormStateInterface $form_state) {
$title = $form_state->getValue('title');
if (strlen($title) < 5) {
$form_state->setErrorByName('title', $this->t('The title must be at least 5 characters long.'));
}
}
Drupal calls this method automatically after a form is submitted, before submitForm() ever runs. $form_state->getValue('title') reads back exactly what the visitor typed into the field keyed title in buildForm() — the same key, every time, is what connects a field's definition to its validation to its eventual use.
setErrorByName() — rejecting one specific field
The first argument to setErrorByName() must exactly match the field's key from buildForm() — get it wrong (a typo, or the wrong nesting for a field inside a fieldset) and Drupal won't know which element to highlight, so the error message shows but the field itself doesn't get the red outline. Setting even one error here means submitForm() is never called at all for this request — the whole submission stops.
Rejecting the form as a whole
$form_state->setError($form, $this->t('Something about the overall submission is invalid.'));
Not every validation failure belongs to one field — sometimes two fields conflict with each other (an end date before a start date, say). Passing the whole $form array to setError() instead of one field's name attaches the message to the form generally rather than pointing at a specific input.
validateForm() isn't the only place to reject input. Simple constraints often belong directly on the element — '#required' => TRUE, or an #element_validate callback scoped to just one field — so the logic stays next to the field it governs instead of accumulating in one long method as a form grows.Quick check: if
validateForm()callssetErrorByName()for one field, doessubmitForm()still run for the rest of the form's now-valid fields? No — a single validation error stops the entire submission. The visitor sees the form again, with their previously entered values preserved, and must fix the flagged field before anything gets processed.
Key takeaways
validateForm()runs automatically after submission and beforesubmitForm()— any error it sets cancels the submission entirely.setErrorByName('field_key', $message)must reference the exact key used inbuildForm(), or the error won't attach to the right element.- Use
setError($form, $message)for a validation failure that isn't specific to one field. - Simple per-field constraints (
#required, an#element_validatecallback) often belong on the element itself rather than in one largevalidateForm()method.
Coming up next
Validation passed — now what actually happens with the data? Next, we cover submitForm(): reading validated values and doing something real with them.