A settings form with no validation is a dashboard where every dial accepts any input, sensible or not — type a negative number into a "cache lifetime in seconds" field and Drupal will happily save it, with no idea anything's wrong until something downstream breaks in a confusing way. validateForm() is where you catch that before it ever reaches submitForm(), and the Form API guarantees it runs first, every time, with zero extra wiring required.
What you'll learn in this lesson
- Where
validateForm()fits in the Form API request lifecycle - How to reject bad input with
$form_state->setErrorByName() - Why validation errors should name the specific field, not just fail generically
- How a config form's validation differs in stakes from an ordinary content form's
Implementing validateForm()
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {
// Confirm that the value is numeric.
if (!is_numeric($form_state->getValue('max_items'))) {
$form_state->setErrorByName('max_items', $this->t('Max items must be a number.'));
}
}
This follows the exact same real validation pattern used throughout Drupal's Examples module (its DBTNG forms validate an age field the same way) — adapted here for a config form's own "max_items" setting. The shape is always identical: check a value, and if it's wrong, call setErrorByName() naming the offending field.
Why setErrorByName(), specifically
$form_state->setErrorByName('max_items', $this->t('Max items must be a number.'));
The first argument is the form element's key — Drupal uses it to render the error message directly under that specific field and to add an error-state CSS class to it, rather than just showing a generic message at the top of the page with no indication which field caused it. Get this key wrong (a typo, or referencing a nested element incorrectly) and the message still appears, just detached from any field — technically working but confusing for the person filling out the form.
Where validateForm() sits in the request
Drupal's Form API always runs validation handlers before any submit handler, for every form, without you asking for it. If validateForm() calls setErrorByName() even once, Drupal stops the request right there: submitForm() never runs, the page re-renders with the error(s) shown, and nothing gets saved. This is exactly why validation is the right place to protect configuration integrity — bad data physically cannot reach $this->config()->set() if validation rejected it first.
-1 or a batch size of 0 can degrade or break the whole site, not just one page. Validate config values at least as carefully as you'd validate anything user-facing, arguably more.Quick check: if
validateForm()callssetErrorByName()for one field out of three on the form, do the other two fields still get saved when the administrator fixes the bad one and resubmits? (Answer: yes — once validation passes cleanly on the resubmission, all fields insubmitForm()are saved together as usual. The earlier failed submission saved nothing at all, for any field, sincesubmitForm()never ran.)
Key takeaways
validateForm()always runs beforesubmitForm()— Drupal enforces this ordering for every form automatically.- Call
$form_state->setErrorByName('field_key', $message)to reject bad input, naming the specific field so the error renders in the right place. - Any call to
setErrorByName()stops the whole submission —submitForm()is skipped entirely, and nothing on the form gets saved that round. - Config values often deserve stricter validation than content fields, since a bad config value can affect the entire site rather than one piece of content.
Coming up next
You've now built, saved, and validated a configuration form. The last piece is making sure a freshly installed site has sensible values from the very first page load — next, you'll set up default settings so your module works reasonably even before an administrator touches the form.