Implementing submitForm() for Submission Logicfor Drupal 8 , 9 , 10 , and 11

Last updated :  

Think of a form as a restaurant order slip. buildForm() is the waiter handing you the menu, and validateForm() is the kitchen double-checking the order makes sense before cooking starts. submitForm() is the moment the food actually gets made — the point where a submitted form stops being just data sitting in memory and starts having a real effect on your site.

What you'll learn in this lesson

  • What submitForm() is responsible for, and when Drupal calls it
  • How to read submitted values out of $form_state
  • Why the real work belongs in submitForm(), not validateForm()
  • How to give the user feedback after a successful submission

The method signature

public function submitForm(array &$form, FormStateInterface $form_state) {
  $title = $form_state->getValue('title');
  $this->messenger()->addMessage($this->t('You specified a title of %title.', ['%title' => $title]));
}

This is the real submitForm() from the Drupal Examples module's SimpleForm class. Notice the shape: it takes the same two parameters as buildForm() and validateForm() — the render array $form (by reference, though most submit handlers never need to touch it) and $form_state, the object carrying everything the user actually typed.

Reading submitted values

$title = $form_state->getValue('title');

$form_state->getValue('key') is how you read back whatever the user entered into the form element with that #key. By the time submitForm() runs, Drupal has already collected every field's value into this one object — you never read straight from $_POST or reach into the $form array for user input.

Why the real work happens here, not in validateForm()

validateForm() only gets to say yes or no — it can reject bad input, but it should never have side effects like writing to the database or sending an email, because if a *later* validation handler on the same form rejects the submission, any side effect you already triggered can't be undone. submitForm() only runs once Drupal is certain every validation handler passed, which makes it the one safe place to actually change something.

Common mistake: reaching for the Drupal 7 function form_set_error() anywhere in a Form API class. It doesn't exist in Drupal 8 and later — the modern equivalent, $form_state->setErrorByName('element_name', $message), belongs in validateForm(), not submitForm(). If you see form_set_error() in any code sample online, it's a leftover from a much older Drupal version.

Quick check: a form's validateForm() passes, but a second form on the same page (also submitted in the same request) fails its own validation. Does your form's submitForm() still run? Yes — each form's validate/submit cycle is independent. Drupal doesn't hold your form's submission hostage to an unrelated form failing elsewhere on the page.

Key takeaways

  • submitForm(array &$form, FormStateInterface $form_state) runs only after every validation handler has passed — it's the one safe place for real side effects.
  • Read user input with $form_state->getValue('key'), never directly from the render array or superglobals.
  • form_set_error() is dead Drupal 7 code — use $form_state->setErrorByName() in validateForm() instead.
  • Use $this->messenger()->addMessage() to confirm success to the user after submitForm() finishes.
  • Each form on a page has its own independent validate/submit cycle — one form failing doesn't block another's submission.

Coming up next

A form rarely needs just one submit button doing one thing. The next lesson covers defining multiple submit handlers — giving a form several distinct buttons (Save, Preview, Cancel) that each run their own logic.