Creating Entity Edit Formsfor Drupal 11 , 10 , 9 , and 8

Last updated :  

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.

The save button label changes for free: 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 by if (!$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 shared buildForm().
  • Field widgets pre-fill their #default_value from 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.