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_formin a routing file turns one route into a fully working add form - Why extending
ContentEntityFormmeans you almost never writebuildForm()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.
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 touchContactForm.phpto 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
ContentEntityFormgives you a workingbuildForm()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->entityarrives empty or already loaded. - Override
buildForm()(callingparent::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.