The edit form for a content entity looks almost identical to the add form — same fields, same widgets, same class in most modules. But "almost identical" hides a few real differences worth understanding, especially around what's pre-filled, what routing key changes, and what a careless override can accidentally break.
What you'll learn in this lesson
- What actually differs between the add and edit operations under the hood
- How the edit route passes a specific entity ID in, and what that changes
- Why overriding
buildForm()for edit-only behavior needs an explicit check - How the edit form's save button label differs from add automatically
The edit route
entity.content_entity_example_contact.edit_form:
path: '/content_entity_example_contact/{content_entity_example_contact}/edit'
defaults:
_entity_form: 'content_entity_example_contact.edit'
_title: 'Edit contact'
requirements:
_entity_access: 'content_entity_example_contact.update'
Two differences from the add route jump out: the path contains a {content_entity_example_contact} placeholder (an entity-upcasting parameter — Drupal loads the real entity with that ID before your form class ever runs), and the operation is edit instead of add. Both operations can point at the exact same form class — ContentEntityForm only cares whether $this->entity is new or already exists, and the routing system decides that for it automatically based on which route matched.
What "already exists" changes at runtime
public function buildForm(array $form, FormStateInterface $form_state) {
$entity = $this->entity;
$form = parent::buildForm($form, $form_state);
if (!$entity->isNew()) {
$form['#title'] = $this->t('Edit %label', ['%label' => $entity->label()]);
}
return $form;
}
$entity->isNew() is the one check that reliably tells add and edit apart inside a shared form class. On the edit route, every field widget's #default_value is populated from the loaded entity automatically by the parent's field-widget logic — you never manually pull values out of the entity and assign them to form elements yourself.
ContentEntityForm's base actions() method already reads $entity->isNew() to decide between "Save" (new) and something like "Save Contact" using the entity type's own label — you don't need to override anything just to get an edit-appropriate button label.See it for yourself
Create a contact via the add form, then visit its edit page (the "Edit" operation link from the list page). Compare the two forms side by side — the field widgets are identical in shape, but the edit form's fields already contain your saved values.
Quick check: you want the edit form only (never add) to show a read-only "Created" timestamp field. Where's the safest place to add that? (Answer: inside
buildForm(), guarded byif (!$entity->isNew())— exactly the same pattern used above for the page title, so add doesn't get a field it has no value for yet.)
Key takeaways
- Add and edit routes typically share one form class; the entity-upcasting route parameter on the edit route is what actually supplies an existing entity.
$entity->isNew()is the standard way to branch edit-only logic inside a sharedbuildForm().- Field widgets pre-fill their
#default_valuefrom the loaded entity automatically — you don't wire this up by hand. - The base class already picks an appropriate save-button label based on whether the entity is new.
Coming up next
Both add and edit forms need to reject bad input before anything gets saved. Next, we look at how entity forms validate data — including validation logic that goes beyond what individual field widgets already check.