Every validation technique so far has assumed you own the form — you're writing its class, its buildForm(), its validateForm(). But what if the form belongs to someone else entirely? Drupal core's login form, a contributed module's settings form, even a form from a module you can't edit — hook_form_alter() lets any module reach in and change another form's validation behavior from the outside, with zero access to that form's original source.
What you'll learn in this lesson
- How
hook_form_alter()gives you a chance to modify any form, including its validation, right before it renders - How to add a validation callback to a form you don't own
- How to target one specific form with
hook_form_FORM_ID_alter()instead of every form on the site - How to remove or reorder an existing validation callback that's already on the form
Adding a validator to someone else's form
/**
* Implements hook_form_FORM_ID_alter().
*/
function mymodule_form_user_login_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
$form['#validate'][] = 'mymodule_login_form_validate';
}
function mymodule_login_form_validate(array &$form, FormStateInterface $form_state) {
$name = $form_state->getValue('name');
if (str_contains($name, ' ')) {
$form_state->setErrorByName('name', t('Usernames cannot contain spaces.'));
}
}
$form['#validate'] is an array of callable validation functions Drupal runs during the validation phase — appending to it with [] = adds your function without disturbing the form's own existing validators. This is the alter-hook equivalent of overriding validateForm() on a form you actually own: same validation phase, same $form_state, just attached from outside instead of from within the form class itself.
hook_form_alter() vs. hook_form_FORM_ID_alter()
The generic hook_form_alter(array &$form, FormStateInterface $form_state, $form_id) runs for every single form rendered anywhere on the site — you'd need an if ($form_id === 'user_login_form') check inside it to target just one form. hook_form_FORM_ID_alter(), as used above, does that filtering for you at the function-name level: Drupal only invokes mymodule_form_user_login_form_alter() when the form actually being built is user_login_form. Prefer the specific variant whenever you know exactly which form you're targeting — it's both clearer and cheaper, since Drupal doesn't even call your function for forms you don't care about.
Removing or reordering an existing validator
$key = array_search('some_module_old_validate', $form['#validate']);
if ($key !== FALSE) {
unset($form['#validate'][$key]);
}
$form['#validate'] is just a plain PHP array at this point in the lifecycle — you can inspect it, remove entries you don't want, or reorder them, with the same array functions you'd use on any other array. This is a genuinely powerful (and genuinely risky) capability: you're editing another module's validation behavior, so change it only when you're confident about why the existing validator needs to go.
hook_form_alter() runs, the form has already been fully built by its owning code. Your additions to #validate are appended at the end of that array, meaning your custom validator always runs after the form's own built-in validation — you can't validate something before the form's own author's checks have already run.See it for yourself
Implement hook_form_user_login_form_alter(), append a validator rejecting usernames containing a space, and confirm it fires on the real core login form — a form your module never wrote a single line of.
Quick check: you implement the generic
hook_form_alter()instead of the specifichook_form_user_login_form_alter(), and forget theif ($form_id === 'user_login_form')guard inside it. What actually happens? (Answer: your alter logic runs on every single form rendered anywhere on the site, not just the login form — including forms where the change makes no sense or could break something. Always guard a generic hook_form_alter() with a form_id check, or use the FORM_ID-specific variant instead.)
Key takeaways
hook_form_alter()andhook_form_FORM_ID_alter()let any module modify another form's structure and validation, without owning that form's original code.- Append to
$form['#validate'][]to add a new validation callback to a form you don't own. - Prefer
hook_form_FORM_ID_alter()over the generic hook plus a manual$form_idcheck — Drupal only invokes it for the one form you're targeting. $form['#validate']is a plain array — you can remove or reorder existing validators, not just add new ones.- Alter-hook validators always run after the form's own built-in validation, since alter hooks fire after the form is already fully built.
Coming up next
Validation only tells you whether input is acceptable — it doesn't do anything with it. Next, the tutorial moves into the submission phase: what happens the moment a fully validated form is actually submitted, starting with handling the submission itself.