Change record status: 
Project: 
Introduced in branch: 
8.x
Description: 

Summary

In Drupal 7, core entity forms were very similar in their basic functionality but each entity type replicated more or less the same code to implement more or less the same logic. In Drupal 8, this logic has been abstracted into entity form controllers.

An entity form controller is an object implementing the EntityFormInterface responsible for handling the entity form workflow: from building the form to performing validation and submission tasks. The base implementation provides the common logic and will exploit the Entity Field API to generalize most of its tasks, so that only minimal custom logic will need to be provided in the entity-specific subclasses.

Entity form controllers differ from storage controllers in the sense that an entity can have multiple form controllers, each one responsible for managing a different kind of operation. This way we can have, for instance, different controllers for the regular create/edit form and the deletion/deletion confirmation ones. See the examples below for more on this.

Regardless of the operation, the controller defines standard methods to build the entity from the submitted values and retrieve it from the form state, thus providing a more consistent and reliable API to code altering the entity form workflow. The controller itself is stored into the form state and can be retrieved by any form handler/callback. Entity forms can now be identified easily because they have form controllers which implement EntityFormInterface.

EntityFormInterface in turn extends Drupal\Core\Form\FormInterface.

API changes

There were previously two common ways to get the entity being operated on: for a node entity,

  • $node = $form_state['node']
  • $node = $form['#node']

Both of these methods are now deprecated, and will stop working in the future. The new recommended way to get the entity being operated on is

$entity = $form_state->getFormObject()->getEntity().

Form IDs for entity forms are now automatically generated by entity_form_id(). Most form IDs have stayed the same, but some have changed to make form IDs more consistent. The form IDs that have changed are:

  • taxonomy_form_term has been renamed to taxonomy_term_form
  • taxonomy_form_vocabulary has been renamed to taxonomy_vocabulary_form

The form functions for entities such as node_form() have been removed and replaced with entity form controllers.

The form functions that have been removed are:

  • node_form()
  • comment_form()
  • user_profile_form()
  • user_account_form()
  • user_register_form()
  • taxonomy_form_term()

New API functions have been introduced:

  • entity_form_controller() can be used to instantiate a new form controller class for the given operation
  • entity_form_state_defaults() provides a form state stub with the build info properly populated to build an entity form
  • entity_get_form() returns a processed entity form for the given operation. It replaces drupal_get_form() for entity forms.
  • entity_form_submit() submits an entity form with the given form state

The previous hook_node_prepare() has been replaced by a generic hook_entity_prepare_form() and an entity-type-specific version including hook_ENTITY_TYPE_prepare_form(). The hook signature has changed to include, besides the entity object, the current form display, the current operation and the form state.

Examples

Fetching an entity from $form_state

Before:

<?php
function my_entity_edit_page(MyEntity $entity) {
  return drupal_get_form('my_entity_form', $entity);
}

function my_module_form_my_entity_form_alter(&$form, &$form_state) {
  $entity = $form_state['my_entity'];
  // ...
}
?>

After:

<?php
function my_entity_edit_page(MyEntity $entity) {
  return entity_get_form($entity);
}
function my_module_form_my_entity_form_alter(&$form, &$form_state) {
  $entity = $form_state->getFormObject()->getEntity();
  // ...
}
?>

Forms based on EntityFormInterface should access the passed entity via $this->entity:

<?php
class MyEntityFormController extends EntityForm {
  public function form(array $form, array &$form_state) {
    $form['label'] = array(
      '#type' => 'text',
      '#title' => t('Label'),
      '#default_value' => $this->entity->label(),
    );
    return parent::form($form, $form_state);
  }
}
?>

An entity with multiple form controllers

/**
 * Defines the user entity class.
 *
 * @ContentEntityType(
 *   id = "user",
 *   label = @Translation("User"),
 *   controllers = {
 *     "storage" = "Drupal\user\UserStorage",
 *     "access" = "Drupal\user\UserAccessControlHandler",
 *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
 *     "form" = {
 *       "profile" = "Drupal\user\ProfileForm",
 *       "register" = "Drupal\user\RegisterForm"
 *     },
 *     "translation" = "Drupal\user\ProfileTranslationHandler"
 *   }
 *   // ...
 * )
 */

To get the user register form, run

<?php
return \Drupal::service('entity.form_builder')->getForm(User::create(), 'register');
?>

A route definition with an entity form

<?php
user.routing.yml
user.edit:
  path: '/user/{user}/edit'
  defaults:
    _entity_form: 'user.default'
    _title_callback: 'Drupal\user\Controller\UserController::userTitle'
  requirements:
    _entity_access: 'user.update'
?>
Impacts: 
Module developers
Updates Done (doc team, etc.)
Online documentation: 
Not done
Theming guide: 
Not done
Module developer documentation: 
Not done
Examples project: 
Not done
Coder Review: 
Not done
Coder Upgrade: 
Not done
Other: 
Other updates done