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

Last updated :  

A generic Drupal form asks you to build every field by hand. An entity add form is different: hand Drupal an empty entity object and a bit of routing configuration, and it builds the entire "Add" form for you — fields, labels, validation, and the save button — straight from that entity type's own field definitions. This lesson shows you exactly how that happens, using the real ContentEntityForm class Drupal core provides.

What you'll learn in this lesson

  • How _entity_form in a routing file turns one route into a fully working add form
  • Why extending ContentEntityForm means you almost never write buildForm() yourself
  • How the "add" and "edit" operations share one form class but behave differently
  • Where to hook in extra fields the entity's own field definitions don't cover

The routing side: one line creates the whole form

entity.content_entity_example_contact.add_form:
  path: '/content_entity_example_contact/add'
  defaults:
    _entity_form: 'content_entity_example_contact.add'
    _title: 'Add contact'
  requirements:
    _entity_create_access: 'content_entity_example_contact'

The _entity_form key is doing all the heavy lifting here. Its value has two parts separated by a dot: the entity type ID (content_entity_example_contact) and the operation (add). Drupal looks up the form handler registered for that operation on that entity type's annotation, instantiates it, and calls its buildForm() — you never write a controller for this route at all.

The form class: mostly inherited

namespace Drupal\content_entity_example\Form;

use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Language\Language;
use Drupal\Core\Form\FormStateInterface;

class ContactForm extends ContentEntityForm {

  public function buildForm(array $form, FormStateInterface $form_state) {
    $entity = $this->entity;
    $form = parent::buildForm($form, $form_state);

    $form['langcode'] = [
      '#title' => $this->t('Language'),
      '#type' => 'language_select',
      '#default_value' => $entity->getUntranslated()->language()->getId(),
      '#languages' => Language::STATE_ALL,
    ];
    return $form;
  }

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

}

Notice what's missing: no field definitions, no #type => 'textfield' anywhere. parent::buildForm() already looped over every field this entity type declares (via its baseFieldDefinitions()) and built a widget for each one, using the widget each field's type registers as its default. This class only adds to that — here, a language selector the base class doesn't include by default.

Add and edit are the same class, on purpose: notice ContactForm never checks "is this a new entity?" anywhere. On the add route, $this->entity is created fresh and empty by the routing system before buildForm() runs; on the edit route, it's the real, loaded entity. The widgets render blank fields either way when there's no value, and pre-filled ones when there is — the same code path handles both.

See it for yourself

Visit /content_entity_example_contact/add on your own DDEV site (if you have the content_entity_example module enabled). Every field you see — Name, First Name, and so on — came from that entity type's own field definitions, not from any code in this form class.

Quick check: if you added a brand-new field to the Contact entity type's baseFieldDefinitions() tomorrow, would you need to touch ContactForm.php to make it show up on the add form? If you said no, you've got it — the parent class discovers fields dynamically every time the form builds, so a new field just appears with no form-class changes at all.

Key takeaways

  • _entity_form: '{entity_type}.{operation}' in a routing file is all it takes to wire up a complete entity form — no controller needed.
  • Extending ContentEntityForm gives you a working buildForm() for free, generated from the entity type's own field definitions.
  • The same form class typically handles both "add" and "edit" — the difference is only whether $this->entity arrives empty or already loaded.
  • Override buildForm() (calling parent::buildForm() first) only when you need to add something the field system itself doesn't cover, like a language selector.
  • Override save() to control what happens after a successful save — typically a redirect and the actual $entity->save() call.

Coming up next

You've seen how the add form gets built automatically. Next, we look at the edit form specifically — how it differs in practice from add, and what to watch for when customizing it.