Skip to content

Form Alter System

When to Use

Use specific form alter hooks (hook_form_FORM_ID_alter) for performance. Use generic hook_form_alter only when altering multiple forms.

Reference: Hook Implementation Order

Execution Sequence:

1. hook_form_alter(&$form, $form_state, $form_id)
   - All modules, all forms

2. hook_form_BASE_FORM_ID_alter(&$form, $form_state, $form_id)
   - Shared base forms (e.g., node_form)

3. hook_form_FORM_ID_alter(&$form, $form_state, $form_id)
   - Specific form

4. Theme alter hooks (after all module hooks)

Module Weight:

Modules run by:
1. Module weight (system table)
2. Alphabetically by module name
Lower weight = runs first

Reference: /web/core/lib/Drupal/Core/Form/form.api.php lines 164-322

Pattern: Common Alter Patterns

Add Validation Handler:

function mymodule_form_user_login_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $form['#validate'][] = 'mymodule_user_login_validate';
}

function mymodule_user_login_validate(array &$form, FormStateInterface $form_state) {
  // Custom validation logic
}

Add Submit Handler:

$form['actions']['submit']['#submit'][] = 'mymodule_custom_submit';

// OR add to form level
$form['#submit'][] = 'mymodule_custom_submit';

Modify Element:

// Change required status
$form['field_name']['#required'] = TRUE;

// Update description
$form['field_name']['#description'] = t('Updated description');

// Change weight
$form['field_name']['#weight'] = 10;

// Hide field
$form['field_name']['#access'] = FALSE;

// Disable field
$form['field_name']['#disabled'] = TRUE;

Remove Element:

unset($form['field_name']);

// OR hide it
$form['field_name']['#access'] = FALSE;

Pattern: Advanced Alter Patterns

Add Conditional Field (#states):

$form['field_name']['#states'] = [
  'visible' => [
    ':input[name="trigger"]' => ['checked' => TRUE],
  ],
  'required' => [
    ':input[name="trigger"]' => ['checked' => TRUE],
  ],
];

Modify Button Submit Handlers:

// Replace handlers
$form['actions']['submit']['#submit'] = ['mymodule_custom_submit'];

// Prepend handler (run first)
array_unshift($form['#submit'], 'mymodule_first_submit');

// Append handler (run last)
$form['#submit'][] = 'mymodule_last_submit';

Change Button Text:

$form['actions']['submit']['#value'] = t('Custom Text');
$form['actions']['delete']['#value'] = t('Remove');

Add Custom Button:

$form['actions']['custom'] = [
  '#type' => 'submit',
  '#value' => t('Custom Action'),
  '#submit' => ['mymodule_custom_action_submit'],
  '#weight' => 10,
];

Reference: Cache Context Considerations

Add Cache Dependencies:

// When alter depends on user permissions
$form['#cache']['contexts'][] = 'user.permissions';

// When alter depends on config
$form['#cache']['tags'][] = 'config:mymodule.settings';

// When alter depends on current user
$form['#cache']['contexts'][] = 'user';

// When alter depends on URL
$form['#cache']['contexts'][] = 'url.path';

Why Cache Contexts Matter:

Forms are cached and reused
Without proper contexts: same form for all users
Add context when alter output varies by:
- User permissions
- User role
- Configuration
- URL parameters

Pattern: Debugging Form Alters

Display Form ID:

function mymodule_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  \Drupal::messenger()->addMessage('Form ID: ' . $form_id);
}

Log Form Structure:

\Drupal::logger('mymodule')->debug('<pre>@form</pre>', [
  '@form' => print_r($form, TRUE),
]);

Check Execution Order:

\Drupal::logger('mymodule')->debug('Form alter running for: @id', [
  '@id' => $form_id,
]);

Best Practices

DO:

  • Use specific hook (FORM_ID) when possible (performance)
  • Add cache contexts when alter varies by context
  • Preserve existing handlers unless replacing intentionally
  • Use #access instead of unset() (allows other modules to override)

DON'T:

  • Alter forms in .module file without hook
  • Remove elements other modules might need
  • Replace all submit handlers unless necessary
  • Forget cache contexts for conditional alters

Common Mistakes

  • Not checking if element exists before modifying
  • Using form_alter for all forms (performance)
  • Removing handlers other modules added
  • Not testing alter execution order with multiple modules

See Also