This section has covered the "standard" things a submit handler does — save to config, save an entity, save a file. But a submit handler is really just a PHP method that runs after validated data is available, which means it can trigger absolutely anything: send an email, call a third-party API, queue a background job, invalidate a cache tag. This closing lesson is about that general-purpose flexibility.
What you'll learn in this lesson
- That a submit handler isn't limited to the "obvious" save patterns
- How to trigger a background task instead of doing slow work inline
- Why custom actions still belong in
submitForm(), notvalidateForm()
A submit handler that queues background work
public function submitForm(array &$form, FormStateInterface $form_state) {
$email = $form_state->getValue('email');
/** @var \Drupal\Core\Queue\QueueFactory $queue_factory */
$queue_factory = \Drupal::service('queue');
$queue_factory->get('my_module_welcome_emails')->createItem(['email' => $email]);
$this->messenger()->addMessage($this->t('Thanks! A welcome email is on its way.'));
}
Rather than calling a mail-sending service directly inside submitForm() — which would make the visitor's browser wait for that network call to finish before the page responds — this pushes a small item onto a queue (the same Queue API from the Module Development topic) and lets cron process it moments later. The visitor sees an instant response either way.
Why this belongs in submitForm(), not validateForm()
Queueing a welcome email is a genuine side effect — exactly the category of action that has to wait until every validation handler has confirmed the submission is actually going through. Put it in validateForm() instead, and a user could trigger a queued email even on a submission that ultimately gets rejected for a completely unrelated field.
submitForm() — never in validateForm(), no matter how tempting it is to "get it done early."Quick check: your form has two validation handlers. The first passes; the second rejects the submission. If your custom action (say, queuing an email) was placed inside the first validation handler instead of
submitForm(), what happens? The email gets queued anyway, even though the form submission as a whole was rejected — exactly the bug this lesson's rule of thumb prevents.
Key takeaways
- A submit handler can trigger literally any PHP logic — it isn't limited to config/entity/file saves.
- Prefer queueing slow or external work (emails, API calls) rather than running it synchronously inside
submitForm(). - Any action that can't be cleanly undone belongs in
submitForm(), never invalidateForm(). - Inject services like
queuethe same way you would in a controller —\Drupal::service('queue')or proper constructor injection for a form with acreate()factory method.
Coming up next
You've now covered the entire submission lifecycle, from a single button to background side effects. Next, the tutorial moves into a more interactive territory: adding AJAX callbacks so a form can respond to the user without a full page reload at all.