Imagine a colleague finishing a report and placing it in a shared tray marked "final draft — last chance to edit before it's printed." Anyone can walk up and cross something out, add a note in the margin, or change a heading — directly on that same physical page — before it goes to the printer, without ever asking the original author for permission. That's exactly what hook_form_alter() gives every module on your site: a chance to reach into any form, right before it renders, and change it.
What you'll learn in this lesson
- What makes
hook_form_alter()different from a "gathering" hook likehook_help() - Why
&$formis passed by reference, and what that actually buys you - How to target one specific form instead of every form on the site
A real, working example
This is the exact pattern from Drupal's own Examples project — a hook implementation that rewrites the description text on the core login form's username field:
/**
* Implements hook_form_alter().
*/
function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// This is an example of what is known as an alter hook. The $form
// parameter in this case represents an already complete Form API array
// and our hook implementation is being given the opportunity to make
// changes to the existing data structure before it's used.
//
// If this is the user login form, change the description text of the
// username field.
if ($form_id === 'user_login_form') {
$form['name']['#description'] = t('This text has been altered by hooks_example_form_alter().');
}
}
Run this in any enabled module and every login form on the site — core's, a theme's custom one, anything — shows the altered description, without a single line of the login form's own code ever changing.
Why &$form is passed by reference
You're not handed a copy of the form to inspect — you're handed the actual, live data structure, with permission to change it directly, right before Drupal renders it. That's what the & before $form means. Change $form['name']['#description'] inside your function, and the calling code's $form variable reflects that change immediately — no return value needed, which is unusual compared to most PHP functions you write.
$form_id: the guard clause you almost always need
hook_form_alter() fires for every single form rendered anywhere on the site — the login form, every node edit form, every module's configuration form, all of them. The if ($form_id === 'user_login_form') check above is not optional decoration; without it, your alter logic runs against every form and will eventually collide with something you never meant to touch.
hook_form_FORM_ID_alter() — for example hooks_example_form_user_login_form_alter() — which Drupal only invokes for that one specific form ID. It's the exact same mechanism as hook_form_alter(), just with the target baked into the function name instead of an if-check in the body. Prefer it whenever you only care about one form.Quick check: what would happen if you deployed the code above but accidentally left off the
if ($form_id === 'user_login_form')check? (Answer: the description-rewriting line would try to run against every form'snameelement sitewide — on forms that don't have anamefield at all it would create one, and on forms wherenamemeans something else entirely, you'd silently corrupt unrelated functionality.)
Key takeaways
hook_form_alter(&$form, FormStateInterface $form_state, $form_id)fires for every form on the site — always guard with anifcheck on$form_id, or usehook_form_FORM_ID_alter()instead.$formis passed by reference — modify it directly, don't return a new value.- Alter hooks are the general pattern for "receive an already-assembled data structure and get a chance to change it before use" — the same idea applies well beyond forms.
Coming up next
Forms aren't the only fully-assembled data structure you can intercept before Drupal uses it. Next: Implementing hook_entity_type_alter, where the same alter-hook pattern reaches into how entity types themselves are defined.