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 ownsave()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.
$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 inheritedsubmitForm(), via each widget'sextractFormValues(). - 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.