diff --git a/core/includes/form.inc b/core/includes/form.inc
index 571a439..67f5bf8 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -743,9 +743,13 @@ function drupal_retrieve_form($form_id, &$form_state) {
   // the constructor function itself.
   $args = $form_state['build_info']['args'];
 
-  // We first check to see if there's a function named after the $form_id.
+  // If an explicit form builder callback is defined we just use it, otherwise
+  // we look for a function named after the $form_id.
+  $callback = !empty($form_state['build_info']['callback']) ? $form_state['build_info']['callback'] : $form_id;
+
+  // We first check to see if there is a valid form builder callback defined.
   // If there is, we simply pass the arguments on to it to get the form.
-  if (!function_exists($form_id)) {
+  if (!is_callable($callback)) {
     // In cases where many form_ids need to share a central constructor function,
     // such as the node editing form, modules can implement hook_forms(). It
     // maps one or more form_ids to the correct constructor functions.
@@ -804,7 +808,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
 
   // If $callback was returned by a hook_forms() implementation, call it.
   // Otherwise, call the function named after the form id.
-  $form = call_user_func_array(isset($callback) ? $callback : $form_id, $args);
+  $form = call_user_func_array($callback, $args);
   $form['#form_id'] = $form_id;
 
   return $form;
@@ -1465,7 +1469,7 @@ function form_execute_handlers($type, &$form, &$form_state) {
       $batch['has_form_submits'] = TRUE;
     }
     else {
-      $function($form, $form_state);
+      call_user_func_array($function, array(&$form, &$form_state));
     }
     $return = TRUE;
   }
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index a6230f2..31f2f6c 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -103,8 +103,11 @@ function comment_entity_info() {
       'base table' => 'comment',
       'uri callback' => 'comment_uri',
       'fieldable' => TRUE,
-      'controller class' => 'Drupal\comment\CommentStorageController',
       'entity class' => 'Drupal\comment\Comment',
+      'controller class' => 'Drupal\comment\CommentStorageController',
+      'form controller class' => array(
+        'default' => 'Drupal\comment\CommentFormController',
+      ),
       'entity keys' => array(
         'id' => 'cid',
         'bundle' => 'node_type',
@@ -765,8 +768,7 @@ function comment_node_page_additions(Node $node) {
 
   // Append comment form if needed.
   if (user_access('post comments') && $node->comment == COMMENT_NODE_OPEN && (variable_get('comment_form_location_' . $node->type, COMMENT_FORM_BELOW) == COMMENT_FORM_BELOW)) {
-    $comment = entity_create('comment', array('nid' => $node->nid));
-    $additions['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
+    $additions['comment_form'] = comment_edit($node);
   }
 
   if ($additions) {
@@ -782,6 +784,14 @@ function comment_node_page_additions(Node $node) {
 }
 
 /**
+ * Returns a rendered form to comment the given node.
+ */
+function comment_edit($node, $pid = NULL) {
+  $values = array('nid' => $node->nid, 'pid' => $pid, 'node_type' => 'comment_node_' . $node->type);
+  return entity_create('comment', $values)->form();
+}
+
+/**
  * Retrieves comments for a thread.
  *
  * @param Drupal\node\Node $node
@@ -1650,19 +1660,7 @@ function comment_get_display_page($cid, $node_type) {
  */
 function comment_edit_page(Comment $comment) {
   drupal_set_title(t('Edit comment %comment', array('%comment' => $comment->subject)), PASS_THROUGH);
-  $node = node_load($comment->nid);
-  return drupal_get_form("comment_node_{$node->type}_form", $comment);
-}
-
-/**
- * Implements hook_forms().
- */
-function comment_forms() {
-  $forms = array();
-  foreach (node_type_get_types() as $type) {
-    $forms["comment_node_{$type->type}_form"]['callback'] = 'comment_form';
-  }
-  return $forms;
+  return $comment->form();
 }
 
 /**
@@ -1688,7 +1686,9 @@ function comment_form($form, &$form_state, Comment $comment) {
   }
 
   $node = node_load($comment->nid);
+  // @todo Remove all the usages of $form['#node'].
   $form['#node'] = $node;
+  $form_state['node'] = $node;
 
   // Use #comment-form as unique jump target, regardless of node type.
   $form['#id'] = drupal_html_id('comment_form');
@@ -1848,24 +1848,8 @@ function comment_form($form, &$form_state, Comment $comment) {
     '#value' => $comment_langcode,
   );
 
-  // Only show the save button if comment previews are optional or if we are
-  // already previewing the submission.
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-    '#access' => ($comment->cid && user_access('administer comments')) || variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || isset($form_state['comment_preview']),
-  );
-  $form['actions']['preview'] = array(
-    '#type' => 'submit',
-    '#value' => t('Preview'),
-    '#access' => (variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED),
-    '#submit' => array('comment_form_build_preview'),
-  );
-
   // Attach fields.
   $comment->node_type = 'comment_node_' . $node->type;
-  field_attach_form('comment', $comment, $form, $form_state);
 
   return $form;
 }
@@ -1948,8 +1932,6 @@ function comment_preview(Comment $comment) {
 function comment_form_validate($form, &$form_state) {
   global $user;
 
-  entity_form_field_validate('comment', $form, $form_state);
-
   if (!empty($form_state['values']['cid'])) {
     // Verify the name in case it is being changed from being anonymous.
     $account = user_load_by_name($form_state['values']['name']);
diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc
index 5176846..7ee3e04 100644
--- a/core/modules/comment/comment.pages.inc
+++ b/core/modules/comment/comment.pages.inc
@@ -43,8 +43,7 @@ function comment_reply(Node $node, $pid = NULL) {
   // The user is previewing a comment prior to submitting it.
   if ($op == t('Preview')) {
     if (user_access('post comments')) {
-      $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid));
-      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
+      $build['comment_form'] = comment_edit($node, $pid);
     }
     else {
       drupal_set_message(t('You are not authorized to post comments.'), 'error');
@@ -92,8 +91,7 @@ function comment_reply(Node $node, $pid = NULL) {
       drupal_goto("node/$node->nid");
     }
     elseif (user_access('post comments')) {
-      $comment = entity_create('comment', array('nid' => $node->nid, 'pid' => $pid));
-      $build['comment_form'] = drupal_get_form("comment_node_{$node->type}_form", $comment);
+      $build['comment_form'] = comment_edit($node, $pid);
     }
     else {
       drupal_set_message(t('You are not authorized to post comments.'), 'error');
diff --git a/core/modules/comment/lib/Drupal/comment/CommentFormController.php b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
new file mode 100644
index 0000000..6f61634
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\comment\CommentFormController.
+ */
+
+namespace Drupal\comment;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Base for controller for comment edit forms.
+ */
+class CommentFormController extends EntityFormController {
+
+  /**
+   * @see Drupal\entity\EntityFormController::form()
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $comment) {
+    $form = comment_form($form, $form_state, $comment);
+    return parent::form($form, $form_state, $comment);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::actions()
+   */
+  protected function actions(array $form, array &$form_state, EntityInterface $comment) {
+    $element = parent::actions($form, $form_state, $comment);
+    $node = $form_state['node'];
+    $preview_mode = variable_get('comment_preview_' . $node->type, DRUPAL_OPTIONAL);
+
+    // No delete action on the comment form.
+    unset($element['delete']);
+
+    // Only show the save button if comment previews are optional or if we are
+    // already previewing the submission.
+    $element['submit']['#access'] = ($comment->cid && user_access('administer comments')) || $preview_mode != DRUPAL_REQUIRED || isset($form_state['comment_preview']);
+
+    $element['preview'] = array(
+      '#type' => 'submit',
+      '#value' => t('Preview'),
+      '#access' => $preview_mode != DRUPAL_DISABLED,
+      '#submit' => array('comment_form_build_preview'),
+    );
+
+    $element['#weight'] = $form['comment_body']['#weight'] + 0.01;
+
+    return $element;
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::validate()
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+    comment_form_validate($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::save()
+   */
+  public function save(array $form, array &$form_state) {
+    // Completely override the base method as we need to perform some advanced
+    // tweak before regularly building the entity from the submitted values.
+    comment_form_submit($form, $form_state);
+  }
+}
diff --git a/core/modules/entity/entity.api.php b/core/modules/entity/entity.api.php
index 02d8754..4d4830b 100644
--- a/core/modules/entity/entity.api.php
+++ b/core/modules/entity/entity.api.php
@@ -27,6 +27,12 @@
  *     The class has to implement the
  *     Drupal\entity\EntityStorageControllerInterface interface. Leave blank
  *     to use the Drupal\entity\DatabaseStorageController implementation.
+ *   - form controller class: An array of form controller class names keyed by
+ *     context name. Each class handle the edit form for the entity being
+ *     defined, alongside with its handlers, for the related context. The
+ *     context is also passed to the class costructor hence if only small tweaks
+ *     are needed to adapt the edit form to the various contexts a unique class
+ *     may be provided.
  *   - base table: (used by Drupal\entity\DatabaseStorageController) The
  *     name of the entity type's base table.
  *   - static cache: (used by Drupal\entity\DatabaseStorageController)
@@ -136,6 +142,9 @@ function hook_entity_info() {
       'label' => t('Node'),
       'entity class' => 'Drupal\node\Node',
       'controller class' => 'Drupal\node\NodeStorageController',
+      'form controller class' => array(
+        'default' => 'Drupal\node\NodeFormController',
+      ),
       'base table' => 'node',
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module
index a66b1e8..dae931d 100644
--- a/core/modules/entity/entity.module
+++ b/core/modules/entity/entity.module
@@ -71,6 +71,9 @@ function entity_get_info($entity_type = NULL) {
           'fieldable' => FALSE,
           'entity class' => 'Drupal\entity\Entity',
           'controller class' => 'Drupal\entity\DatabaseStorageController',
+          'form controller class' => array(
+            'default' => 'Drupal\entity\EntityFormController',
+          ),
           'static cache' => TRUE,
           'field cache' => TRUE,
           'bundles' => array(),
@@ -447,6 +450,90 @@ function entity_form_field_validate($entity_type, $form, &$form_state) {
 }
 
 /**
+ * Returns a form controller for the given context.
+ *
+ * Since there might be different contexts in which an entity or parts of it are
+ * edited, multiple form controllers suitable to the different contexts may be
+ * defined. If no valid controller is found for the given context the default
+ * one will be used.
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param
+ *   (optional) The bundle of the entity being edited.
+ * @param $context
+ *   (optional) The name of the context identifying the form controller.
+ *
+ * @return Drupal\entity\EntityFormController
+ *   An instance of the Drupal\Core\Entity\FormController class.
+ */
+function entity_get_form_controller($entity_type, $bundle = NULL, $context = 'default') {
+  $info = entity_get_info($entity_type);
+
+  // If no controller is specified default to the base implementation.
+  if (!empty($info) && empty($info['form controller class'])) {
+    $class = 'Drupal\entity\EntityFormController';
+  }
+
+  // Check whether a bundle-specific form controller exists.
+  if (!empty($bundle) && !empty($info['bundle form controller class'][$bundle][$context])) {
+    $class = $info['bundle form controller class'][$bundle][$context];
+  }
+  // Fall back to the per-entity-type form controller.
+  elseif (!empty($info['form controller class'][$context])) {
+    $class = $info['form controller class'][$context];
+  }
+  // If a non-existing context has been specified stop.
+  else {
+    throw new EntityMalformedException("Missing form controller for '$entity_type' bundle '$bundle' in context '$context'");
+  }
+
+  return new $class($context);
+}
+
+/**
+ * Helper function. Returns the form id for the given entity.
+ */
+function _entity_form_id(EntityInterface $entity, $context = 'default') {
+  $entity_type = $entity->entityType();
+  $bundle = $entity->bundle();
+  $form_id = $entity_type;
+  if ($bundle != $entity_type) {
+    $form_id = $bundle . '_' . $form_id;
+  }
+  if ($context != 'default') {
+    $form_id = $form_id . '_' . $context;
+  }
+  return $form_id . '_form';
+}
+
+/**
+ * Returns a rendered edit form for the given entity and context.
+ *
+ * @param EntityInterface|string $entity
+ *   The entity being edited or the entity type to be created.
+ * @param $context
+ *   (optional) The context for the form to be returned.
+ *
+ * @return
+ *   A rendered edit form for the given entity.
+ */
+function entity_get_form($entity, $context = 'default') {
+  if (is_string($entity)) {
+    $entity = entity_create($entity, array());
+  }
+
+  $controller = entity_get_form_controller($entity->entityType(), $entity->bundle(), $context);
+
+  $form_state['build_info']['callback'] = array($controller, 'build');
+  $form_state['build_info']['base_form_id'] = $entity->entityType() . '_form';
+  $form_state['build_info']['args'] = array($entity);
+
+  $form_id = _entity_form_id($entity, $context);
+  return drupal_build_form($form_id, $form_state);
+}
+
+/**
  * Copies submitted values to entity properties for simple entity forms.
  *
  * During the submission handling of an entity form's "Save", "Preview", and
diff --git a/core/modules/entity/lib/Drupal/entity/Entity.php b/core/modules/entity/lib/Drupal/entity/Entity.php
index eeaeee8..5b47228 100644
--- a/core/modules/entity/lib/Drupal/entity/Entity.php
+++ b/core/modules/entity/lib/Drupal/entity/Entity.php
@@ -270,4 +270,13 @@ class Entity implements EntityInterface {
     return $this->isCurrentRevision;
   }
 
+  /**
+   * Implements EntityInterface::form().
+   *
+   * @param $context
+   *   (optional) The context for the form to be returned.
+   */
+  public function form($context = 'default') {
+    return entity_get_form($this, $context);
+  }
 }
diff --git a/core/modules/entity/lib/Drupal/entity/EntityFormController.php b/core/modules/entity/lib/Drupal/entity/EntityFormController.php
new file mode 100644
index 0000000..15fc664
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/EntityFormController.php
@@ -0,0 +1,285 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\EntityFormController.
+ */
+
+namespace Drupal\entity;
+
+/**
+ * Base for controller for entity edit forms.
+ */
+class EntityFormController {
+
+  /**
+   * The name of the current context.
+   *
+   * Subclasses may use this value to implement different behaviors depending on
+   * this value.
+   *
+   * @var string
+   */
+  protected $context;
+
+  /**
+   * Constructs a FormController object.
+   *
+   * @param string $context
+   *   The name of the current context.
+   */
+  public function __construct($context) {
+    $this->context = $context;
+  }
+
+  /**
+   * Builds an entity edit form.
+   *
+   * @param $form
+   *   An associative array containing the structure of the form.
+   * @param $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   * @param $entity_type
+   *   The type of the entity being edited.
+   * @param $entity
+   *   The entity being edited.
+   *
+   * @return
+   *   The array containing the complete form.
+   */
+  public final function build(array $form, array &$form_state, EntityInterface $entity) {
+    // Set the entity form controller in the form state.
+    EntityFormController::setFormInstance($form_state, $this);
+
+    // During initial form build, add the entity to the form state for use
+    // during form building and processing. During a rebuild, use what is in the
+    // form state.
+    $form_entity = EntityFormController::getEntity($form_state);
+    if (empty($form_entity)) {
+      EntityFormController::setEntity($form_state, $entity);
+    }
+    else {
+      $entity = $form_entity;
+    }
+
+    // Retrieve the form array.
+    $form = $this->form($form, $form_state, $entity);
+
+    // Retrieve the form actions.
+    $actions = $this->actionsElement($form, $form_state, $entity);
+    if (!empty($actions)) {
+      $form['actions'] = $actions;
+    }
+
+    return $form;
+  }
+
+  /**
+   * Returns the actual form array to be built.
+   *
+   * @see FormController::build().
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $entity) {
+    // @todo Exploit the Property API to generate the default widgets for the
+    // entity properties.
+    $info = $entity->entityInfo();
+    if (!empty($info['fieldable'])) {
+      field_attach_form($entity->entityType(), $entity, $form, $form_state, $this->getFormLangcode($form_state));
+    }
+    return $form;
+  }
+
+  /**
+   * Returns the action form element for the current entity form.
+   */
+  protected final function actionsElement(array $form, array &$form_state, EntityInterface $entity) {
+    $element = $this->actions($form, $form_state, $entity);
+
+    // We cannot delete an entity that has not been created yet.
+    if (!$entity->id()) {
+      unset($element['delete']);
+    }
+    elseif (isset($element['delete'])) {
+      // Move the delete action as last one, unless weights are explictly
+      // provided.
+      $delete = $element['delete'];
+      unset($element['delete']);
+      $element['delete'] = $delete;
+    }
+
+    $count = 0;
+    $submit = array($this, 'submit');
+
+    foreach (element_children($element) as $action) {
+      $element[$action] += array(
+        '#type' => 'submit',
+        '#weight' => ++$count * 5,
+        '#validate' => array(),
+        '#submit' => array(),
+      );
+      // Ensure we always preprocess submitted data before calling the actual
+      // submission handlers.
+      array_unshift($element[$action]['#submit'], $submit);
+    }
+
+    if (!empty($element)) {
+      $element['#type'] = 'actions';
+    }
+
+    return $element;
+  }
+
+  /**
+   * Returns an array of supported actions for the current entity form.
+   */
+  protected function actions(array $form, array &$form_state, EntityInterface $entity) {
+    return array(
+      'submit' => array(
+        '#value' => t('Save'),
+        '#validate' => array(array($this, 'validate')),
+        '#submit' => array(array($this, 'save')),
+      ),
+      'delete' => array(
+        '#value' => t('Delete'),
+        // No need to validate the form when deleting the entity.
+        '#submit' => array(array($this, 'delete')),
+      ),
+    );
+  }
+
+  /**
+   * Validates the submitted form values.
+   *
+   * @param $form
+   *   An associative array containing the structure of the form.
+   * @param $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function validate(array $form, array &$form_state) {
+    // @todo Exploit the Property API to validate the values submitted for the
+    // entity properties.
+    $entity = EntityFormController::getEntity($form_state);
+    $info = $entity->entityInfo();
+    if (!empty($info['fieldable'])) {
+      entity_form_field_validate($entity->entityType(), $form, $form_state);
+    }
+
+    // @todo Remove this.
+    // Execute legacy global validation handlers.
+    unset($form_state['validate_handlers']);
+    form_execute_handlers('validate', $form, $form_state);
+  }
+
+  /**
+   * Processes the submitted form values and updates the entity object.
+   *
+   * @param $form
+   *   An associative array containing the structure of the form.
+   * @param $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function submit(array $form, array &$form_state) {
+    // @todo Exploit the Property API to process the submitted  entity property
+    // values.
+    $entity = EntityFormController::getEntity($form_state);
+    // @todo Do we really need this?
+    $this->prebuildEntity($form, $form_state);
+    // @todo Make entity_form_submit_build_entity() an entity form controller
+    // method.
+    entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state);
+    EntityFormController::setEntity($form_state, $entity);
+  }
+
+  /**
+   * Gives a chance to manipulate submitted data before building the entity.
+   *
+   * @param $form
+   *   An associative array containing the structure of the form.
+   * @param $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  protected function prebuildEntity(array $form, array &$form_state) {
+    if (isset($form['#entity_prebuilders'])) {
+      foreach ($form['#entity_prebuilders'] as $function) {
+        $function($form, $form_state);
+      }
+    }
+  }
+
+  /**
+   * Submit handler for the 'save' action.
+   *
+   * @param $form
+   *   An associative array containing the structure of the form.
+   * @param $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function save(array $form, array &$form_state) {}
+
+  /**
+   * Submit handler for the 'delete' action.
+   *
+   * @param array $form
+   * @param array $form_state
+   */
+  public function delete(array $form, array &$form_state) {}
+
+  /**
+   * Returns the code identifying the active form language.
+   */
+  public function getFormLangcode($form_state) {
+    // @todo Introduce a new language type to use as the default language.
+    $language = EntityFormController::getEntity($form_state)->language();
+    return !empty($language->langcode) ? $language->langcode : NULL;
+  }
+
+  /**
+   * Returns the entity being edited.
+   *
+   * @param $form_state
+   *   The current form state.
+   *
+   * @return Drupal\entity\Entity
+   *   The entity being edited in the current form.
+   */
+  public static final function getEntity($form_state) {
+    return isset($form_state['entity']) ? $form_state['entity'] : FALSE;
+  }
+
+  /**
+   * Stores the entity being edited.
+   *
+   * @param $form_state
+   *   The current form state.
+   * @param EntityInterface $entity
+   *   The entity being edited in the current form.
+   */
+  protected static final function setEntity(&$form_state, EntityInterface $entity) {
+    return $form_state['entity'] = $entity;
+  }
+
+  /**
+   * Returns an instance of the entity form controller.
+   *
+   * @param $form_state
+   *   The current form state.
+   * @return Drupal\entity\EntityFormController
+   *   An instance of the entity form controller associated with the current
+   *   form.
+   */
+  protected static final function getFormInstance($form_state) {
+    return isset($form_state['entity_form_controller']) ? $form_state['entity_form_controller'] : FALSE;
+  }
+
+  /**
+   * Associates the current form controller to the form being edited.
+   *
+   * @param $form_state
+   *   The current form state.
+   * @param EntityFormController $controller
+   *   The current entity form controller.
+   */
+  protected static final function setFormInstance(&$form_state, EntityFormController $controller) {
+    $form_state['entity_form_controller'] = $controller;
+  }
+}
diff --git a/core/modules/entity/lib/Drupal/entity/EntityInterface.php b/core/modules/entity/lib/Drupal/entity/EntityInterface.php
index 699c424..2479c28 100644
--- a/core/modules/entity/lib/Drupal/entity/EntityInterface.php
+++ b/core/modules/entity/lib/Drupal/entity/EntityInterface.php
@@ -205,4 +205,10 @@ interface EntityInterface {
    */
   public function isCurrentRevision();
 
+  /**
+   * Returns a rendered entity edit form.
+   *
+   * @param $context
+   */
+  public function form($context = 'default');
 }
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index ca9bf7e..54ed906 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -592,7 +592,7 @@ function forum_field_storage_pre_update($entity_type, $entity, &$skip_fields) {
 /**
  * Implements hook_form_FORM_ID_alter() for taxonomy_form_vocabulary().
  */
-function forum_form_taxonomy_form_vocabulary_alter(&$form, &$form_state, $form_id) {
+function forum_form_taxonomy_vocabulary_form_alter(&$form, &$form_state, $form_id) {
   $vid = variable_get('forum_nav_vocabulary', 0);
   if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
     $form['help_forum_vocab'] = array(
@@ -609,9 +609,9 @@ function forum_form_taxonomy_form_vocabulary_alter(&$form, &$form_state, $form_i
 }
 
 /**
- * Implements hook_form_FORM_ID_alter() for taxonomy_form_term().
+ * Implements hook_form_FORM_ID_alter() for taxonomy_term_form().
  */
-function forum_form_taxonomy_form_term_alter(&$form, &$form_state, $form_id) {
+function forum_form_taxonomy_term_form_alter(&$form, &$form_state, $form_id) {
   $vid = variable_get('forum_nav_vocabulary', 0);
   if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
     // Hide multiple parents select from forum terms.
diff --git a/core/modules/node/lib/Drupal/node/NodeFormController.php b/core/modules/node/lib/Drupal/node/NodeFormController.php
new file mode 100644
index 0000000..f1c9a30
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeFormController.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\node\NodeFormController.
+ */
+
+namespace Drupal\node;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Form controller for the node edit forms.
+ */
+class NodeFormController extends EntityFormController {
+
+  /**
+   * @see Drupal\entity\EntityFormController::form()
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $node) {
+    $form = node_form($form, $form_state, $node);
+    return parent::form($form, $form_state, $node);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::actions()
+   */
+  protected function actions(array $form, array &$form_state, EntityInterface $node) {
+    $element = parent::actions($form, $form_state, $node);
+    $preview_mode = variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL);
+
+    $element['preview'] = array(
+      '#access' => $preview_mode != DRUPAL_DISABLED,
+      '#value' => t('Preview'),
+      '#submit' => array('node_form_build_preview'),
+    );
+
+    $element['submit']['#access'] = $preview_mode != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview']));
+    $element['delete']['#access'] = node_access('delete', $node);
+
+    return $element;
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::validate()
+   */
+  public function validate(array $form, array &$form_state) {
+    node_form_validate($form, $form_state);
+    parent::validate($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::submit()
+   */
+  public function submit(array $form, array &$form_state) {
+    // Handle possible field translations first and then build the node from the
+    // submitted values.
+    node_field_language_form_submit($form, $form_state);
+    parent::submit($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::save()
+   */
+  public function save(array $form, array &$form_state) {
+    node_form_submit($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity.EntityFormController::delete()
+   */
+  public function delete(array $form, array &$form_state) {
+    node_form_delete_submit($form, $form_state);
+  }
+}
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 42d968d..10022b2 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -193,8 +193,11 @@ function node_entity_info() {
   $return = array(
     'node' => array(
       'label' => t('Node'),
-      'controller class' => 'Drupal\node\NodeStorageController',
       'entity class' => 'Drupal\node\Node',
+      'controller class' => 'Drupal\node\NodeStorageController',
+      'form controller class' => array(
+        'default' => 'Drupal\node\NodeFormController',
+      ),
       'base table' => 'node',
       'revision table' => 'node_revision',
       'uri callback' => 'node_uri',
@@ -3632,21 +3635,6 @@ function node_content_form(Node $node, $form_state) {
  */
 
 /**
- * Implements hook_forms().
- *
- * All node forms share the same form handler.
- */
-function node_forms() {
-  $forms = array();
-  if ($types = node_type_get_types()) {
-    foreach (array_keys($types) as $type) {
-      $forms[$type . '_node_form']['callback'] = 'node_form';
-    }
-  }
-  return $forms;
-}
-
-/**
  * Implements hook_action_info().
  */
 function node_action_info() {
diff --git a/core/modules/node/node.pages.inc b/core/modules/node/node.pages.inc
index faa3dea..a319fb8 100644
--- a/core/modules/node/node.pages.inc
+++ b/core/modules/node/node.pages.inc
@@ -9,6 +9,8 @@
  * @see node_menu()
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
 
 /**
@@ -19,7 +21,7 @@ use Drupal\node\Node;
 function node_page_edit($node) {
   $type_name = node_type_get_name($node);
   drupal_set_title(t('<em>Edit @type</em> @title', array('@type' => $type_name, '@title' => $node->title)), PASS_THROUGH);
-  return drupal_get_form($node->type . '_node_form', $node);
+  return $node->form();
 }
 
 /**
@@ -94,7 +96,7 @@ function node_add($type) {
     'langcode' => node_type_get_default_langcode($type)
   ));
   drupal_set_title(t('Create @name', array('@name' => $types[$type]->name)), PASS_THROUGH);
-  $output = drupal_get_form($type . '_node_form', $node);
+  $output = $node->form();
 
   return $output;
 }
@@ -111,12 +113,11 @@ function node_form_validate($form, &$form_state) {
   // $form_state['node'] contains the actual entity being edited, but we must
   // not update it with form values that have not yet been validated, so we
   // create a pseudo-entity to use during validation.
-  $node = clone $form_state['node'];
+  $node = clone EntityFormController::getEntity($form_state);
   foreach ($form_state['values'] as $key => $value) {
     $node->{$key} = $value;
   }
   node_validate($node, $form, $form_state);
-  entity_form_field_validate('node', $form, $form_state);
 }
 
 /**
@@ -326,43 +327,13 @@ function node_form($form, &$form_state, Node $node) {
     '#default_value' => $node->sticky,
   );
 
-  // Add the buttons.
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_REQUIRED || (!form_get_errors() && isset($form_state['node_preview'])),
-    '#value' => t('Save'),
-    '#weight' => 5,
-    '#submit' => array('node_form_submit'),
-  );
-  $form['actions']['preview'] = array(
-    '#access' => variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL) != DRUPAL_DISABLED,
-    '#type' => 'submit',
-    '#value' => t('Preview'),
-    '#weight' => 10,
-    '#submit' => array('node_form_build_preview'),
-  );
-  if (!empty($node->nid) && node_access('delete', $node)) {
-    $form['actions']['delete'] = array(
-      '#type' => 'submit',
-      '#value' => t('Delete'),
-      '#weight' => 15,
-      '#submit' => array('node_form_delete_submit'),
-    );
-  }
   // This form uses a button-level #submit handler for the form's main submit
   // action. node_form_submit() manually invokes all form-level #submit handlers
   // of the form. Without explicitly setting #submit, Form API would auto-detect
   // node_form_submit() as submit handler, but that is the button-level #submit
-  // handler for the 'Save' action. To maintain backwards compatibility, a
-  // #submit handler is auto-suggested for custom node type modules.
-  $form['#validate'][] = 'node_form_validate';
-  if (!isset($form['#submit']) && function_exists($node->type . '_node_form_submit')) {
-    $form['#submit'][] = $node->type . '_node_form_submit';
-  }
+  // handler for the 'Save' action.
   $form += array('#submit' => array());
 
-  field_attach_form('node', $node, $form, $form_state, $node->langcode);
   return $form;
 }
 
@@ -380,7 +351,7 @@ function node_form_delete_submit($form, &$form_state) {
     $destination = drupal_get_destination();
     unset($_GET['destination']);
   }
-  $node = $form['#node'];
+  $node = EntityFormController::getEntity($form_state);
   $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination));
 }
 
@@ -492,9 +463,6 @@ function theme_node_preview($variables) {
  * @see node_form_submit_build_node()
  */
 function node_form_submit($form, &$form_state) {
-  // Handle possible field translations first and then build the node from the
-  // submitted values.
-  node_field_language_form_submit($form, $form_state);
   $node = node_form_submit_build_node($form, $form_state);
   $insert = empty($node->nid);
   $node->save();
@@ -565,19 +533,7 @@ function node_field_language_form_submit($form, &$form_state) {
  * @see node_form_submit()
  */
 function node_form_submit_build_node($form, &$form_state) {
-  // @todo Legacy support for modules that extend the node form with form-level
-  //   submit handlers that adjust $form_state['values'] prior to those values
-  //   being used to update the entity. Module authors are encouraged to instead
-  //   adjust the node directly within a hook_node_submit() implementation. For
-  //   Drupal 8, evaluate whether the pattern of triggering form-level submit
-  //   handlers during button-level submit processing is worth supporting
-  //   properly, and if so, add a Form API function for doing so.
-  unset($form_state['submit_handlers']);
-  form_execute_handlers('submit', $form, $form_state);
-
-  $node = $form_state['node'];
-  entity_form_submit_build_entity('node', $node, $form, $form_state);
-
+  $node = EntityFormController::getEntity($form_state);
   node_submit($node);
   foreach (module_implements('node_submit') as $module) {
     $function = $module . '_node_submit';
diff --git a/core/modules/openid/openid.module b/core/modules/openid/openid.module
index 034c7c2..82973e4 100644
--- a/core/modules/openid/openid.module
+++ b/core/modules/openid/openid.module
@@ -732,6 +732,10 @@ function openid_authentication($response) {
 
     $form_state['values'] = array();
     $form_state['values']['op'] = t('Create new account');
+    $controller = entity_get_form_controller('user', NULL, 'register');
+    $form_state['build_info']['callback'] = array($controller, 'build');
+    $form_state['build_info']['base_form_id'] = 'user_form';
+    $form_state['build_info']['args'] = array(entity_create('user', array()), 'register');
     drupal_form_submit('user_register_form', $form_state);
 
     if (empty($form_state['user'])) {
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
index d01c623..7b73e20 100644
--- a/core/modules/path/path.module
+++ b/core/modules/path/path.module
@@ -232,9 +232,9 @@ function path_node_predelete(Node $node) {
 }
 
 /**
- * Implements hook_form_FORM_ID_alter() for taxonomy_form_term().
+ * Implements hook_form_FORM_ID_alter() for taxonomy_term_form().
  */
-function path_form_taxonomy_form_term_alter(&$form, $form_state) {
+function path_form_taxonomy_term_form_alter(&$form, $form_state) {
   // Make sure this does not show up on the delete confirmation form.
   if (empty($form_state['confirm_delete'])) {
     $path = (isset($form['#term']['tid']) ? path_load('taxonomy/term/' . $form['#term']['tid']) : array());
diff --git a/core/modules/poll/lib/Drupal/poll/PollFormController.php b/core/modules/poll/lib/Drupal/poll/PollFormController.php
new file mode 100644
index 0000000..0ab5a2f
--- /dev/null
+++ b/core/modules/poll/lib/Drupal/poll/PollFormController.php
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\poll\PollFormController.
+ */
+
+namespace Drupal\poll;
+
+use Drupal\entity\EntityInterface;
+use Drupal\node\NodeFormController;
+
+/**
+ * Form controller for the node edit forms.
+ */
+class PollFormController extends NodeFormController {
+  // @todo Remove hook_form() and move form additions here.
+}
diff --git a/core/modules/poll/poll.module b/core/modules/poll/poll.module
index 7954aaa..7a2131f 100644
--- a/core/modules/poll/poll.module
+++ b/core/modules/poll/poll.module
@@ -9,6 +9,13 @@
 use Drupal\node\Node;
 
 /**
+ * Implements hook_entity_info_alter().
+ */
+function poll_entity_info_alter(&$info) {
+  $info['node']['bundle form controller class']['poll']['default'] = 'Drupal\poll\PollFormController';
+}
+
+/**
  * Implements hook_help().
  */
 function poll_help($path, $arg) {
@@ -336,6 +343,8 @@ function poll_form(Node $node, &$form_state) {
     drupal_get_path('module', 'poll') . '/poll.admin.css',
   );
 
+  $form['#entity_prebuilders'][] = 'poll_node_form_submit';
+
   return $form;
 }
 
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 12428fe..6fb4cfc 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1355,9 +1355,10 @@ function hook_form_BASE_FORM_ID_alter(&$form, &$form_state, $form_id) {
  * @return
  *   An associative array whose keys define form_ids and whose values are an
  *   associative array defining the following keys:
- *   - callback: The name of the form builder function to invoke. This will be
- *     used for the base form ID, for example, to target a base form using
- *     hook_form_BASE_FORM_ID_alter().
+ *   - base form ids: An array containing a list of base form ids to be used to
+ *     alter the form using hook_form_BASE_FORM_ID_alter(). If no value is
+ *     provided the callback function name will be used.
+ *   - callback: The name of the form builder function to invoke.
  *   - callback arguments: (optional) Additional arguments to pass to the
  *     function defined in 'callback', which are prepended to $args.
  *   - wrapper_callback: (optional) The name of a form builder function to
diff --git a/core/modules/system/tests/modules/form_test/form_test.module b/core/modules/system/tests/modules/form_test/form_test.module
index 1e9e882..4954c1c 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -2104,8 +2104,8 @@ function form_test_two_instances() {
     'langcode' => LANGUAGE_NOT_SPECIFIED,
   ));
   $node2 = clone($node1);
-  $return['node_form_1'] = drupal_get_form('page_node_form', $node1);
-  $return['node_form_2'] = drupal_get_form('page_node_form', $node2);
+  $return['node_form_1'] = entity_get_form($node1);
+  $return['node_form_2'] = entity_get_form($node2);
   return $return;
 }
 
diff --git a/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module b/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
index 0ec6da6..00e6637 100644
--- a/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
+++ b/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
@@ -59,16 +59,14 @@ function taxonomy_test_taxonomy_term_delete(Term $term) {
 /**
  * Implements hook_form_alter().
  */
-function taxonomy_test_form_alter(&$form, $form_state, $form_id) {
-  if ($form_id == 'taxonomy_form_term') {
-    $antonym = taxonomy_test_get_antonym($form['#term']['tid']);
-    $form['advanced']['antonym'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Antonym'),
-      '#default_value' => !empty($antonym) ? $antonym : '',
-      '#description' => t('Antonym of this term.')
-    );
-  }
+function taxonomy_test_form_taxonomy_term_form_alter(&$form, $form_state, $form_id) {
+  $antonym = taxonomy_test_get_antonym($form['#term']['tid']);
+  $form['advanced']['antonym'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Antonym'),
+    '#default_value' => !empty($antonym) ? $antonym : '',
+    '#description' => t('Antonym of this term.')
+  );
 }
 
 /**
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php
new file mode 100644
index 0000000..cc626e0
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php
@@ -0,0 +1,55 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\taxonomy\TermFormController.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Base for controller for taxonomy term edit forms.
+ */
+class TermFormController extends EntityFormController {
+
+  /**
+   * @see Drupal\entity\EntityFormController::form()
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $term) {
+    $form = taxonomy_term_form($form, $form_state, $term);
+    return parent::form($form, $form_state, $term);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::validate()
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+    taxonomy_term_form_validate($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::submit()
+   */
+  public function submit(array $form, array &$form_state) {
+    parent::submit($form, $form_state);
+    taxonomy_term_form_submit_build_taxonomy_term($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::save()
+   */
+  public function save(array $form, array &$form_state) {
+    taxonomy_term_form_submit($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::delete()
+   */
+  public function delete(array $form, array &$form_state) {
+    taxonomy_term_form_delete_submit($form, $form_state);
+  }
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
index d041d16..a14cff8 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
@@ -278,7 +278,7 @@ class TermTest extends TaxonomyTestBase {
       'description[value]' => $this->randomName(100),
     );
     // Explicitly set the parents field to 'root', to ensure that
-    // taxonomy_form_term_submit() handles the invalid term ID correctly.
+    // taxonomy_term_form_submit() handles the invalid term ID correctly.
     $edit['parent[]'] = array(0);
 
     // Create the term to edit.
@@ -329,7 +329,7 @@ class TermTest extends TaxonomyTestBase {
     $this->drupalGet('taxonomy/term/' . $term->tid . '/feed');
 
     // Check that the term edit page does not try to interpret additional path
-    // components as arguments for taxonomy_form_term().
+    // components as arguments for taxonomy_term_form().
     $this->drupalGet('taxonomy/term/' . $term->tid . '/edit/' . $this->randomName());
 
     // Delete the term.
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
new file mode 100644
index 0000000..13096a1
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\taxonomy\VocabularyFormController.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Base for controller for vocabulary edit forms.
+ */
+class VocabularyFormController extends EntityFormController {
+
+  /**
+   * @see Drupal\entity\EntityFormController::form()
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $vocabluary) {
+    $form = taxonomy_form_vocabulary($form, $form_state, $vocabluary);
+    return parent::form($form, $form_state, $vocabluary);
+  }
+
+  /**
+   * Returns an array of supported actions for the current entity form.
+   */
+  protected function actions(array $form, array &$form_state, EntityInterface $entity) {
+    // If we are displaying the delete confirmation skip the regular actions.
+    return empty($form_state['confirm_delete']) ? parent::actions($form, $form_state, $entity) : array();
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::validate()
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+    taxonomy_form_vocabulary_validate($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::submit()
+   */
+  public function submit(array $form, array &$form_state) {
+    if ($form_state['triggering_element']['#value'] == t('Delete')) {
+      // Rebuild the form to confirm vocabulary deletion.
+      $form_state['rebuild'] = TRUE;
+      $form_state['confirm_delete'] = TRUE;
+    }
+    else {
+      parent::submit($form, $form_state);
+    }
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::save()
+   */
+  public function save(array $form, array &$form_state) {
+    taxonomy_form_vocabulary_submit($form, $form_state);
+  }
+}
diff --git a/core/modules/taxonomy/taxonomy.admin.inc b/core/modules/taxonomy/taxonomy.admin.inc
index be020a6..f1bf4c9 100644
--- a/core/modules/taxonomy/taxonomy.admin.inc
+++ b/core/modules/taxonomy/taxonomy.admin.inc
@@ -7,6 +7,7 @@
 
 use Drupal\taxonomy\Term;
 use Drupal\taxonomy\Vocabulary;
+use Drupal\entity\EntityFormController;
 
 /**
  * Form builder to list and manage vocabularies.
@@ -167,13 +168,9 @@ function taxonomy_form_vocabulary($form, &$form_state, Vocabulary $vocabulary =
     '#value' => '0',
   );
 
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save'));
   if (isset($vocabulary->vid)) {
-    $form['actions']['delete'] = array('#type' => 'submit', '#value' => t('Delete'));
     $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid);
   }
-  $form['#validate'][] = 'taxonomy_form_vocabulary_validate';
 
   return $form;
 }
@@ -207,15 +204,7 @@ function taxonomy_form_vocabulary_validate($form, &$form_state) {
  * @see taxonomy_form_vocabulary_validate()
  */
 function taxonomy_form_vocabulary_submit($form, &$form_state) {
-  if ($form_state['triggering_element']['#value'] == t('Delete')) {
-    // Rebuild the form to confirm vocabulary deletion.
-    $form_state['rebuild'] = TRUE;
-    $form_state['confirm_delete'] = TRUE;
-    return;
-  }
-
-  $vocabulary = $form_state['vocabulary'];
-  entity_form_submit_build_entity('taxonomy_vocabulary', $vocabulary, $form, $form_state);
+  $vocabulary = EntityFormController::getEntity($form_state);
 
   // Prevent leading and trailing spaces in vocabulary names.
   $vocabulary->name = trim($vocabulary->name);
@@ -643,6 +632,14 @@ function theme_taxonomy_overview_terms($variables) {
 }
 
 /**
+ * Returns a rendered edit form to create a new term associated to the given vocabulary.
+ */
+function taxonomy_term_add($vocabulary) {
+  $term = entity_create('taxonomy_term', array('vid' => $vocabulary->vid, 'vocabulary_machine_name' => $vocabulary->machine_name));
+  return $term->form();
+}
+
+/**
  * Form function for the term edit form.
  *
  * @param Drupal\taxonomy\Term|null $term
@@ -653,10 +650,10 @@ function theme_taxonomy_overview_terms($variables) {
  *   the term is omitted.
  *
  * @ingroup forms
- * @see taxonomy_form_term_validate()
- * @see taxonomy_form_term_submit()
+ * @see taxonomy_term_form_validate()
+ * @see taxonomy_term_form_submit()
  */
-function taxonomy_form_term($form, &$form_state, Term $term = NULL, Vocabulary $vocabulary = NULL) {
+function taxonomy_term_form($form, &$form_state, Term $term = NULL, Vocabulary $vocabulary = NULL) {
   // During initial form build, add the term entity to the form state for use
   // during form building and processing. During a rebuild, use what is in the
   // form state.
@@ -710,8 +707,6 @@ function taxonomy_form_term($form, &$form_state, Term $term = NULL, Vocabulary $
     '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name,
   );
 
-  field_attach_form('taxonomy_term', $term, $form, $form_state);
-
   $form['relations'] = array(
     '#type' => 'fieldset',
     '#title' => t('Relations'),
@@ -770,23 +765,7 @@ function taxonomy_form_term($form, &$form_state, Term $term = NULL, Vocabulary $
     '#value' => $term->tid,
   );
 
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-    '#weight' => 5,
-  );
-
-  if ($term->tid) {
-    $form['actions']['delete'] = array(
-      '#type' => 'submit',
-      '#value' => t('Delete'),
-      '#access' => taxonomy_term_access('delete', $term),
-      '#weight' => 10,
-      '#submit' => array('taxonomy_form_term_delete_submit'),
-    );
-  }
-  else {
+  if (empty($term->tid)) {
     $form_state['redirect'] = current_path();
   }
 
@@ -796,11 +775,9 @@ function taxonomy_form_term($form, &$form_state, Term $term = NULL, Vocabulary $
 /**
  * Validation handler for the term form.
  *
- * @see taxonomy_form_term()
+ * @see taxonomy_term_form()
  */
-function taxonomy_form_term_validate($form, &$form_state) {
-  entity_form_field_validate('taxonomy_term', $form, $form_state);
-
+function taxonomy_term_form_validate($form, &$form_state) {
   // Ensure numeric values.
   if (isset($form_state['values']['weight']) && !is_numeric($form_state['values']['weight'])) {
     form_set_error('weight', t('Weight value must be numeric.'));
@@ -810,10 +787,10 @@ function taxonomy_form_term_validate($form, &$form_state) {
 /**
  * Submit handler to insert or update a term.
  *
- * @see taxonomy_form_term()
+ * @see taxonomy_term_form()
  */
-function taxonomy_form_term_submit($form, &$form_state) {
-  $term = taxonomy_form_term_submit_build_taxonomy_term($form, $form_state);
+function taxonomy_term_form_submit($form, &$form_state) {
+  $term = EntityFormController::getEntity($form_state);
 
   $status = taxonomy_term_save($term);
   switch ($status) {
@@ -856,9 +833,8 @@ function taxonomy_form_term_submit($form, &$form_state) {
 /**
  * Updates the form state's term entity by processing this submission's values.
  */
-function taxonomy_form_term_submit_build_taxonomy_term($form, &$form_state) {
-  $term = $form_state['term'];
-  entity_form_submit_build_entity('taxonomy_term', $term, $form, $form_state);
+function taxonomy_term_form_submit_build_taxonomy_term($form, &$form_state) {
+  $term = EntityFormController::getEntity($form_state);
 
   // Prevent leading and trailing spaces in term names.
   $term->name = trim($term->name);
@@ -871,12 +847,12 @@ function taxonomy_form_term_submit_build_taxonomy_term($form, &$form_state) {
 }
 
 /**
- * Form submission handler for the 'Delete' button for taxonomy_form_term().
+ * Form submission handler for the 'Delete' button for taxonomy_term_form().
  *
- * @see taxonomy_form_term_validate()
- * @see taxonomy_form_term_submit()
+ * @see taxonomy_term_form_validate()
+ * @see taxonomy_term_form_submit()
  */
-function taxonomy_form_term_delete_submit($form, &$form_state) {
+function taxonomy_term_form_delete_submit($form, &$form_state) {
   $destination = array();
   if (isset($_GET['destination'])) {
     $destination = drupal_get_destination();
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index 6697a0a..85c1a83 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -113,6 +113,9 @@ function taxonomy_entity_info() {
       'label' => t('Taxonomy term'),
       'entity class' => 'Drupal\taxonomy\Term',
       'controller class' => 'Drupal\taxonomy\TermStorageController',
+      'form controller class' => array(
+        'default' => 'Drupal\taxonomy\TermFormController',
+      ),
       'base table' => 'taxonomy_term_data',
       'uri callback' => 'taxonomy_term_uri',
       'fieldable' => TRUE,
@@ -149,6 +152,9 @@ function taxonomy_entity_info() {
     'label' => t('Taxonomy vocabulary'),
     'entity class' => 'Drupal\taxonomy\Vocabulary',
     'controller class' => 'Drupal\taxonomy\VocabularyStorageController',
+    'form controller class' => array(
+      'default' => 'Drupal\taxonomy\VocabularyFormController',
+    ),
     'base table' => 'taxonomy_vocabulary',
     'entity keys' => array(
       'id' => 'vid',
@@ -293,8 +299,8 @@ function taxonomy_menu() {
   );
   $items['admin/structure/taxonomy/add'] = array(
     'title' => 'Add vocabulary',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('taxonomy_form_vocabulary'),
+    'page callback' => 'entity_get_form',
+    'page arguments' => array('taxonomy_vocabulary'),
     'access arguments' => array('administer taxonomy'),
     'type' => MENU_LOCAL_ACTION,
     'file' => 'taxonomy.admin.inc',
@@ -315,10 +321,10 @@ function taxonomy_menu() {
   );
   $items['taxonomy/term/%taxonomy_term/edit'] = array(
     'title' => 'Edit',
-    'page callback' => 'drupal_get_form',
+    'page callback' => 'entity_get_form',
     // Pass a NULL argument to ensure that additional path components are not
-    // passed to taxonomy_form_term() as the vocabulary machine name argument.
-    'page arguments' => array('taxonomy_form_term', 2, NULL),
+    // passed to taxonomy_term_form() as the vocabulary machine name argument.
+    'page arguments' => array(2),
     'access callback' => 'taxonomy_term_access',
     'access arguments' => array('edit', 2),
     'type' => MENU_LOCAL_TASK,
@@ -368,8 +374,8 @@ function taxonomy_menu() {
   );
   $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/edit'] = array(
     'title' => 'Edit',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('taxonomy_form_vocabulary', 3),
+    'page callback' => 'entity_get_form',
+    'page arguments' => array(3),
     'access arguments' => array('administer taxonomy'),
     'type' => MENU_LOCAL_TASK,
     'weight' => -10,
@@ -378,8 +384,8 @@ function taxonomy_menu() {
 
   $items['admin/structure/taxonomy/%taxonomy_vocabulary_machine_name/add'] = array(
     'title' => 'Add term',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('taxonomy_form_term', NULL, 3),
+    'page callback' => 'taxonomy_term_add',
+    'page arguments' => array(3),
     'access arguments' => array('administer taxonomy'),
     'type' => MENU_LOCAL_ACTION,
     'file' => 'taxonomy.admin.inc',
diff --git a/core/modules/user/lib/Drupal/user/ProfileFormController.php b/core/modules/user/lib/Drupal/user/ProfileFormController.php
new file mode 100644
index 0000000..542e411
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/ProfileFormController.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\ProfileFormController.
+ */
+
+namespace Drupal\user;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Form controller for the profile edit forms.
+ */
+class ProfileFormController extends EntityFormController {
+
+  /**
+   * @see Drupal\entity\EntityFormController::form()
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $account) {
+    // @todo Legacy support. Modules are encouraged to access the entity using
+    //   $form_state. Remove in Drupal 8.
+    $form['#user'] = $account;
+
+    user_account_form($form, $form_state);
+
+    return parent::form($form, $form_state, $account);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::actions()
+   */
+  protected function actions(array $form, array &$form_state, EntityInterface $account) {
+    $element = parent::actions($form, $form_state, $account);
+
+    // @todo Actually the cancel action can be assimilated to the delete one: we
+    // should alter it instead of providing a new one.
+    unset($element['delete']);
+
+    $element['cancel'] = array(
+      '#type' => 'submit',
+      '#value' => t('Cancel account'),
+      '#submit' => array('user_edit_cancel_submit'),
+      '#access' => $account->uid > 1 && (($account->uid == $GLOBALS['user']->uid && user_access('cancel account')) || user_access('administer users')),
+    );
+
+    return $element;
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::submit()
+   */
+  public function submit(array $form, array &$form_state) {
+    // @todo Consider moving this into the parent method.
+    // Remove unneeded values.
+    form_state_values_clean($form_state);
+    parent::submit($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::save()
+   */
+  public function save(array $form, array &$form_state) {
+    $account = EntityFormController::getEntity($form_state);
+    $account->save();
+    $form_state['values']['uid'] = $account->id();
+
+    // Clear the page cache because pages can contain usernames and/or profile
+    // information:
+    cache_invalidate(array('content' => TRUE));
+
+    drupal_set_message(t('The changes have been saved.'));
+  }
+}
diff --git a/core/modules/user/lib/Drupal/user/RegisterFormController.php b/core/modules/user/lib/Drupal/user/RegisterFormController.php
new file mode 100644
index 0000000..1a590d2
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/RegisterFormController.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\RegisterFormController.
+ */
+
+namespace Drupal\user;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Form controller for the user register forms.
+ */
+class RegisterFormController extends EntityFormController {
+
+  /**
+   * @see Drupal\entity\EntityFormController::form()
+   */
+  protected function form(array $form, array &$form_state, EntityInterface $account) {
+    return _user_register_form($form, $form_state, $account);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::actions()
+   */
+  protected function actions(array $form, array &$form_state, EntityInterface $account) {
+    $element = parent::actions($form, $form_state, $account);
+    $element['submit']['#value'] = t('Create new account');
+    return $element;
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::submit()
+   */
+  public function submit(array $form, array &$form_state) {
+    $admin = $form_state['values']['administer_users'];
+
+    if (!variable_get('user_email_verification', TRUE) || $admin) {
+      $pass = $form_state['values']['pass'];
+    }
+    else {
+      $pass = user_password();
+    }
+
+    // Remove unneeded values.
+    form_state_values_clean($form_state);
+
+    $form_state['values']['pass'] = $pass;
+    $form_state['values']['init'] = $form_state['values']['mail'];
+
+    parent::submit($form, $form_state);
+  }
+
+  /**
+   * @see Drupal\entity\EntityFormController::submit()
+   */
+  public function save(array $form, array &$form_state) {
+    user_register_submit($form, $form_state);
+  }
+}
diff --git a/core/modules/user/lib/Drupal/user/User.php b/core/modules/user/lib/Drupal/user/User.php
index 992923c..dd68536 100644
--- a/core/modules/user/lib/Drupal/user/User.php
+++ b/core/modules/user/lib/Drupal/user/User.php
@@ -141,4 +141,11 @@ class User extends Entity {
   public function id() {
     return $this->uid;
   }
+
+  /**
+   * Implements Drupal\entity\Entity::form()
+   */
+  public function form($context = 'profile') {
+    return parent::form($context);
+  }
 }
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index 58b0218..f044c64 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -11,7 +11,7 @@ function user_admin($callback_arg = '') {
   switch ($op) {
     case t('Create new account'):
     case 'create':
-      $build['user_register'] = drupal_get_form('user_register_form');
+      $build['user_register'] = entity_get_form('user', 'register');
       break;
     default:
       if (!empty($_POST['accounts']) && isset($_POST['operation']) && ($_POST['operation'] == 'cancel')) {
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 2250110..a54d919 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\Core\Database\Query\SelectInterface;
 use Drupal\Core\File\File;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -146,12 +148,16 @@ function user_entity_info() {
   return array(
     'user' => array(
       'label' => t('User'),
+      'entity class' => 'Drupal\user\User',
       'controller class' => 'Drupal\user\UserStorageController',
+      'form controller class' => array(
+        'profile' => 'Drupal\user\ProfileFormController',
+        'register' => 'Drupal\user\RegisterFormController',
+      ),
       'base table' => 'users',
       'uri callback' => 'user_uri',
       'label callback' => 'user_label',
       'fieldable' => TRUE,
-      'entity class' => 'Drupal\user\User',
       'entity keys' => array(
         'id' => 'uid',
       ),
@@ -1497,8 +1503,8 @@ function user_menu() {
 
   $items['user/register'] = array(
     'title' => 'Create new account',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('user_register_form'),
+    'page callback' => 'entity_get_form',
+    'page arguments' => array('user', 'register'),
     'access callback' => 'user_register_access',
     'type' => MENU_LOCAL_TASK,
   );
@@ -1657,8 +1663,8 @@ function user_menu() {
 
   $items['user/%user/edit'] = array(
     'title' => 'Edit',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('user_profile_form', 1),
+    'page callback' => 'entity_get_form',
+    'page arguments' => array(1, 'profile'),
     'access callback' => 'user_edit_access',
     'access arguments' => array(1),
     'type' => MENU_LOCAL_TASK,
@@ -3497,7 +3503,7 @@ function user_form_field_ui_field_edit_form_submit($form, &$form_state) {
  * @see user_account_form_validate()
  * @see user_register_submit()
  */
-function user_register_form($form, &$form_state) {
+function _user_register_form($form, &$form_state) {
   global $user;
 
   $admin = user_access('administer users');
@@ -3515,7 +3521,7 @@ function user_register_form($form, &$form_state) {
     drupal_goto('user/' . $user->uid);
   }
 
-  $form['#user'] = entity_create('user', array());
+  $form['#user'] = EntityFormController::getEntity($form_state);
 
   $form['#attached']['library'][] = array('system', 'jquery.cookie');
   $form['#attributes']['class'][] = 'user-info-from-cookie';
@@ -3538,54 +3544,21 @@ function user_register_form($form, &$form_state) {
     $form_state['redirect'] = current_path();
   }
 
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Create new account'),
-  );
-
-  $form['#validate'][] = 'user_register_validate';
-  // Add the final user registration form submit handler.
-  $form['#submit'][] = 'user_register_submit';
-
   return $form;
 }
 
 /**
- * Validation function for the user registration form.
- */
-function user_register_validate($form, &$form_state) {
-  entity_form_field_validate('user', $form, $form_state);
-}
-
-/**
  * Submit handler for the user registration form.
  *
- * This function is shared by the installation form and the normal registration form,
- * which is why it can't be in the user.pages.inc file.
- *
- * @see user_register_form()
+ * This function is shared by the installation form and the normal registration
+ * form, which is why it can't be in the user.pages.inc file.
  */
 function user_register_submit($form, &$form_state) {
+  $account = EntityFormController::getEntity($form_state);
+  $pass = $account->pass;
   $admin = $form_state['values']['administer_users'];
-
-  if (!variable_get('user_email_verification', TRUE) || $admin) {
-    $pass = $form_state['values']['pass'];
-  }
-  else {
-    $pass = user_password();
-  }
   $notify = !empty($form_state['values']['notify']);
 
-  // Remove unneeded values.
-  form_state_values_clean($form_state);
-
-  $form_state['values']['pass'] = $pass;
-  $form_state['values']['init'] = $form_state['values']['mail'];
-
-  $account = $form['#user'];
-
-  entity_form_submit_build_entity('user', $account, $form, $form_state);
   $status = $account->save();
 
   // Terminate if an error occurred while saving the account.
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 6cdfd25..ff36675 100644
--- a/core/modules/user/user.pages.inc
+++ b/core/modules/user/user.pages.inc
@@ -5,6 +5,8 @@
  * User page callback file for the user module.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
@@ -202,86 +204,6 @@ function template_preprocess_user_profile(&$variables) {
 }
 
 /**
- * Form builder; edit a user account.
- *
- * @ingroup forms
- * @see user_account_form()
- * @see user_account_form_validate()
- * @see user_profile_form_validate()
- * @see user_profile_form_submit()
- * @see user_cancel_confirm_form_submit()
- */
-function user_profile_form($form, &$form_state, $account) {
-  global $user;
-
-  // During initial form build, add the entity to the form state for use during
-  // form building and processing. During a rebuild, use what is in the form
-  // state.
-  if (!isset($form_state['user'])) {
-    $form_state['user'] = $account;
-  }
-  else {
-    $account = $form_state['user'];
-  }
-
-  // @todo Legacy support. Modules are encouraged to access the entity using
-  //   $form_state. Remove in Drupal 8.
-  $form['#user'] = $account;
-
-
-  user_account_form($form, $form_state);
-  // Attach field widgets.
-  field_attach_form('user', $account, $form, $form_state);
-
-  $form['actions'] = array('#type' => 'actions');
-  $form['actions']['submit'] = array(
-    '#type' => 'submit',
-    '#value' => t('Save'),
-  );
-  $form['actions']['cancel'] = array(
-    '#type' => 'submit',
-    '#value' => t('Cancel account'),
-    '#submit' => array('user_edit_cancel_submit'),
-    '#access' => $account->uid > 1 && (($account->uid == $user->uid && user_access('cancel account')) || user_access('administer users')),
-  );
-
-  $form['#validate'][] = 'user_profile_form_validate';
-  // Add the final user profile form submit handler.
-  $form['#submit'][] = 'user_profile_form_submit';
-
-  return $form;
-}
-
-/**
- * Validation function for the user account and profile editing form.
- */
-function user_profile_form_validate($form, &$form_state) {
-  entity_form_field_validate('user', $form, $form_state);
-}
-
-/**
- * Submit function for the user account and profile editing form.
- */
-function user_profile_form_submit($form, &$form_state) {
-  $account = $form_state['user'];
-  // Remove unneeded values.
-  form_state_values_clean($form_state);
-
-  entity_form_submit_build_entity('user', $account, $form, $form_state);
-  $account->save();
-  $form_state['values']['uid'] = $account->uid;
-
-  if (!empty($edit['pass'])) {
-    // Remove the password reset tag since a new password was saved.
-    unset($_SESSION['pass_reset_'. $account->uid]);
-  }
-  // Clear the page cache because pages can contain usernames and/or profile information:
-  cache_invalidate(array('content' => TRUE));
-
-  drupal_set_message(t('The changes have been saved.'));
-}
-
-/**
  * Submit function for the 'Cancel account' button on the user edit form.
  */
 function user_edit_cancel_submit($form, &$form_state) {
