Picture a shared office memo board with a note pinned to it: "final draft — last chance to edit before it's printed." Anyone can walk up and cross something out, add a line, or change a heading directly on that same page before it goes out — without ever asking the original author for permission. That's exactly what hook_form_alter() lets any module do to any form on the site, including forms defined by Drupal core or a contributed module you'll never touch the source code of.
What you'll learn in this lesson
- Why
hook_form_alter()receives the form by reference, and what that actually means - How to scope your changes to exactly one form using
hook_form_FORM_ID_alter() - How to find a form's exact ID so you know which alter hook to implement
- What kinds of changes an alter hook can safely make
The generic hook, and the scoped version
/**
* Implements hook_form_alter().
*/
function your_module_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
if ($form_id === 'user_login_form') {
$form['name']['#description'] = t('Enter your site username, not your email.');
}
}
This generic version fires for every form on the site, so it always needs an if ($form_id === '...') check to avoid touching forms it shouldn't. Drupal offers a cleaner alternative: name your function hook_form_FORM_ID_alter() with the real form ID spliced in, and Drupal only invokes it for that one form.
/**
* Implements hook_form_FORM_ID_alter() for user_login_form.
*/
function your_module_form_user_login_form_alter(array &$form, FormStateInterface $form_state) {
$form['name']['#description'] = t('Enter your site username, not your email.');
}
Passed by reference: what that actually buys you
The & before $form means you're handed the actual, live render array — not a copy. Any change you make to $form inside the hook directly changes the real form Drupal is about to render, with no return value needed. This is the entire mechanism that makes cross-module form alteration possible without either module knowing the other exists.
Finding a form's ID
A form's ID is whatever its getFormId() method returns — for core and contrib forms you don't have the source open for, the fastest way to find it is enabling the core Devel module and using its "Form debug" feature, which prints the exact ID (and full render array) of any form you view. Guessing the ID from the page URL or field names is unreliable; core form IDs don't always match what you'd expect.
Quick check: you want to change the description text on the login form's username field, and nowhere else. Should you implement
hook_form_alter()with anifcheck, orhook_form_user_login_form_alter()? The scoped variant — it's simpler, it's more explicit about intent to anyone reading your module later, and Drupal skips calling it for every other form on the site, which is marginally more efficient too.
Key takeaways
hook_form_alter()andhook_form_FORM_ID_alter()both receive$formby reference — changes apply directly, no return value needed.- Prefer the scoped
hook_form_FORM_ID_alter()whenever you only need to touch one specific form. - Find a form's real ID via its
getFormId()method, or the Devel module's form debug feature for forms you don't have the source for. - Alter hooks run in module-weight order — if your change seems to vanish, a later-running implementation is probably overwriting it.
Coming up next
Forms are one of many things Drupal lets you extend without touching the original code. Next, we open up the plugin system — the architecture behind blocks, field types, and dozens of other pluggable pieces of Drupal.