Every technique in this section so far has assumed you're editing a form's own buildForm() method directly. But you don't always own that code — the form might belong to Drupal core, or a contrib module, or someone else's custom module entirely. For those cases, Drupal gives you a way to reach into any form's render array from the outside, after it's been built but before it's rendered, and change it. That's hook_form_alter(), and it's the mechanism every other technique in this topic ultimately funnels through when you don't control the form's source.
What you'll learn in this lesson
- The difference between
hook_form_alter()(every form) andhook_form_FORM_ID_alter()(one specific form) - Why
&$formis passed by reference, and what that actually buys you - How to combine an alter hook with everything else covered in this section — attributes, states, custom elements
- Where
#processcallbacks fit as a third, less common alteration point
Altering every form vs. one specific form
The Drupal Examples project's hooks_example module demonstrates the general-purpose version:
/**
* Implements hook_form_alter().
*/
function hooks_example_form_alter(&$form, FormStateInterface $form_state, $form_id) {
if ($form_id != 'user_login_form') {
return;
}
$form['name']['#description'] = t('Enter your username. Login attempts are logged.');
}
hook_form_alter() fires for absolutely every form built on the site, which is why the manual $form_id check is essential — skip it, and code meant for one form silently runs against all of them. The far more common approach in real modules is to let Drupal do that filtering for you, by naming the target form ID directly in the function name:
function hooks_example_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) {
$form['name']['#description'] = t('Enter your username. Login attempts are logged.');
}
Same effect, but Drupal only invokes this second version when $form_id already equals user_login_form — no manual check needed. Prefer this form whenever you know the exact ID up front; reach for the generic hook_form_alter() only when you genuinely need to react to many different forms with shared logic.
Why &$form matters
The & before $form means the array is passed by reference — you're not editing a copy, you're editing the actual array Drupal will render. This is exactly the same by-reference mechanism you've already relied on throughout this section: #attributes, #states, and a custom element added via alter all work precisely because the change you make inside the hook is the change that ships to the browser, with no extra step to "save" or "apply" it.
Bringing it together: one alter hook, several techniques
A single hook_form_FORM_ID_alter() is often where every technique from this section actually gets applied to a form you don't own the source of:
function mymodule_form_contact_message_feedback_form_alter(&$form, FormStateInterface $form_state, $form_id) {
// From the #attributes lesson: add a class.
$form['message']['#attributes']['class'][] = 'feedback-message-field';
// From the #states lesson: hide a field until a checkbox is checked.
$form['follow_up_details']['#states'] = [
'visible' => [':input[name="wants_follow_up"]' => ['checked' => TRUE]],
];
// Adding an entirely new element that didn't exist in the original form.
$form['gdpr_notice'] = [
'#type' => 'item',
'#markup' => t('Your data is processed in accordance with our privacy policy.'),
'#weight' => -10,
];
}
Nothing here is a new mechanism — it's the same #attributes and #states arrays from earlier lessons, just set from inside an alter hook instead of inside the form class's own buildForm(). That's the real point of this lesson: alter hooks aren't a separate toolkit, they're a different entry point into the exact same render array.
#process callbacks. Where hook_form_alter() runs once, after the whole form is built, a #process callback attached to one specific element runs during the build itself, and can see the full $complete_form as its third argument. It's the right tool when an element needs to alter itself based on the rest of the form's current state — genuinely rare, but worth knowing exists if hook_form_alter() ever feels like the wrong altitude for a change.Quick check: you need to add a description to the username field, but only on the site's login form — nowhere else. Do you write
hook_form_alter()with a manual$form_idcheck, orhook_form_user_login_form_alter()? The FORM_ID-specific version. It's shorter, it's exactly as correct, and Drupal handles the filtering — reserve the generichook_form_alter()for logic that genuinely needs to inspect many different forms.
Key takeaways
hook_form_alter()fires for every form on the site — always guard it with a$form_idcheck unless the logic genuinely applies universally.hook_form_FORM_ID_alter()is the preferred, more specific alternative whenever you know the exact form ID — Drupal filters the invocation for you.&$formis passed by reference, so every change inside an alter hook applies directly to what gets rendered — no separate "commit" step.- An alter hook is just an alternate entry point into the same render array —
#attributes,#states, and new elements all work exactly as they do inside a form's ownbuildForm(). #processcallbacks are a rarer, element-scoped alternative for changes that need to happen during the build itself rather than after.
Coming up next
This section covered how a form looks. The next section shifts to how a form defends itself — starting with "Preventing CSRF Attacks," and the token mechanism that's been quietly protecting every form you've built throughout this entire tutorial.