Think of every form on your Drupal site as a document sitting on a shared desk right before it gets printed and handed to the visitor. hook_form_alter() is your standing permission to walk up to that desk and mark it up — add a note, cross something out, tighten a rule — no matter which module originally wrote the page. You never touch that module's own code; you just get one last look at the finished form before it ships.
What you'll learn in this lesson
- What
hook_form_alter()actually receives, and why it runs on every single form on the site - How
$form_idis the switch that lets you target one specific form safely - Why the
$formparameter is passed by reference, and what that means for how you write changes - The shape of a real, working alter hook, taken from Drupal's own Examples module
The source file
Path (relative to the Examples module's root): modules/hooks_example/hooks_example.module
/**
* 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. Invoking an alter hooks is a
// common pattern anytime lists or complex data structures are assembled.
// hook_form_alter(), which allows you to manipulate any form, is one of the
// most commonly implemented hooks.
//
// @see hook_form_alter()
// @see hook_form_FORM_ID_alter()
//
// 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().');
}
}
How it works
Every form on the site passes through here
Drupal calls every enabled module's hook_form_alter() implementation right before any form is rendered — the login form, a node edit form, a contact form, your own custom forms, all of them. That's why the very first thing a well-behaved implementation does is check $form_id and bail out (implicitly, by doing nothing) unless it matches the one form it actually cares about. Skip that check and your logic runs on forms you never meant to touch.
Why $form is passed by reference
function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) {
Notice the & before $form. That's PHP's pass-by-reference syntax, and it's the whole mechanism this hook relies on: you're not given a copy of the form to inspect — you're handed the actual, live array. Any change you make to $form inside this function changes the real form Drupal is about to render. There's no return value to worry about; the mutation itself is the effect.
Targeting one specific form
if ($form_id === 'user_login_form') {
$form['name']['#description'] = t('This text has been altered by hooks_example_form_alter().');
}
Every Drupal form has a machine-readable ID — core's login form is always user_login_form, no matter which theme or module renders the surrounding page. Here, the hook reaches directly into the form array's existing structure ($form['name']['#description']) and overwrites one property, leaving everything else on the form untouched. This is the pattern you'll use constantly: locate the exact key path to the property you want, then set it.
hook_form_alter() keeps your customization in your own module, completely separate from the form's original source, so it survives updates to whatever module built the form in the first place.See it for yourself
Visit /user/login on your own DDEV site with the Examples module's hooks_example enabled. Look at the description text under the username field — it now reads "This text has been altered by hooks_example_form_alter()," proof the hook fired and mutated the real, live form array before it ever reached your browser.
Quick check: if you removed the
if ($form_id === 'user_login_form')condition entirely, what would happen to$form['name']['#description']? (Answer: the hook would try to set that key on every form on the site, including ones that have nonameelement at all — at best a wasted array write, at worst a fatal error the moment some other form's structure doesn't match what you assumed.)
Key takeaways
hook_form_alter(&$form, FormStateInterface $form_state, $form_id)runs on every form on the site — always guard your logic with a$form_idcheck.$formis passed by reference; mutating it directly changes the real form Drupal is about to render, with no return value needed.- Form IDs are stable, predictable machine names (like
user_login_form) — the same one regardless of theme or context. - Altering a form from your own module keeps the customization independent of the original module's code, so it survives that module's future updates.
Coming up next
You've now seen the general-purpose alter hook that runs on every form. Next, we narrow the aim: hook_form_FORM_ID_alter() lets you target one specific form by name in the function signature itself, skipping the manual if check entirely.