Handling Entity Form Submissionfor Drupal 11 , 10 , 9 , and 8

Last updated :  

This is the moment an entity form actually does something permanent: turning validated form values into a saved database row. ContentEntityForm handles almost all of it for you, but understanding exactly what it does — and where you'd hook in extra behavior — matters the moment you need anything beyond a plain save.

What you'll learn in this lesson

  • How submitted form values end up assigned onto the entity object automatically
  • What the inherited submitForm() and your own save() override each do
  • How to run code after a successful save without breaking the redirect/messenger behavior
  • Why you almost never call set() on entity fields by hand inside an entity form

The real save() override

public function save(array $form, FormStateInterface $form_state) {
  $form_state->setRedirect('entity.content_entity_example_contact.collection');
  $entity = $this->getEntity();

  return $entity->save();
}

By the time save() runs, the entity is already fully populated — every field's submitted value has already been copied onto it by the inherited submitForm(), before save() is ever called. This override's job is almost entirely about what happens around the save: where to redirect afterward, and (via the entity's own MessengerInterface) what confirmation message the visitor sees.

Where the field values actually get copied in

This happens inside ContentEntityForm's own submitForm(), which you're not shown here because you almost never need to override it: it walks every field on the form, calls each widget's extractFormValues(), and assigns the result directly onto $this->entity. This is the mechanism that makes entity forms so different from a plain FormBase form — there's no manual $entity->set('name', $form_state->getValue('name')) anywhere, because the field widget system already did that step before save() ever runs.

Running extra logic after a successful save

public function save(array $form, FormStateInterface $form_state) {
  $entity = $this->getEntity();
  $result = $entity->save();

  \Drupal::logger('content_entity_example')->notice('Contact %name saved.', [
    '%name' => $entity->label(),
  ]);

  $form_state->setRedirect('entity.content_entity_example_contact.collection');
  return $result;
}

Notice the order: log after $entity->save() succeeds, not before. If the save itself throws (a database constraint violation, for example), you don't want a log entry claiming something was saved that wasn't.

The return value matters, even though it looks unused: $entity->save() returns an integer constant (SAVED_NEW or SAVED_UPDATED) that ContentEntityForm's caller uses to decide which status message to show ("created" vs. "updated"). Returning it from your save() override, rather than returning nothing, keeps that behavior intact.

See it for yourself

Submit the add-contact form and watch where you land — the redirect target set in save() is exactly the URL the browser ends up on.

Quick check: if you removed the $form_state->setRedirect() call entirely, where would the visitor land after saving? (Drupal's default fallback is the current request path — typically the form itself — which is why almost every real entity form sets an explicit redirect to somewhere more useful, like the entity's own page or a collection listing.)

Key takeaways

  • By the time your save() override runs, field values are already copied onto the entity — that happens inside the inherited submitForm(), via each widget's extractFormValues().
  • Your save() override is mainly responsible for the redirect and anything that should happen around the actual $entity->save() call.
  • Run post-save logic (logging, notifications) after $entity->save() succeeds, never before.
  • Return the actual result of $entity->save() — it's a status constant the framework uses to pick the right confirmation message.

Coming up next

You've now covered the whole add/edit/validate/save cycle. Next, we look at customizing how an entity form actually displays — which fields show, in what order, and with which widgets — without touching the form class at all.