diff --git a/core/includes/form.inc b/core/includes/form.inc
index 7498ea5..47c14ab 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -159,6 +159,8 @@ function drupal_get_form($form_id) {
  *   - build_info: Internal. An associative array of information stored by Form
  *     API that is necessary to build and rebuild the form from cache when the
  *     original context may no longer be available:
+ *     - callback: The actual callback to be used to retrieve the form array. If
+ *       none is provided $form_id is used instead. Can be any callable type.
  *     - args: A list of arguments to pass to the form constructor.
  *     - files: An optional array defining include files that need to be loaded
  *       for building the form. Each array entry may be the path to a file or
@@ -266,11 +268,11 @@ function drupal_get_form($form_id) {
  *     ones used by Form API internals) for this kind of storage. The
  *     recommended way to ensure that the chosen key doesn't conflict with ones
  *     used by the Form API or other modules is to use the module name as the
- *     key name or a prefix for the key name. For example, the Node module uses
- *     $form_state['node'] in node editing forms to store information about the
- *     node being edited, and this information stays available across successive
- *     clicks of the "Preview" button as well as when the "Save" button is
- *     finally clicked.
+ *     key name or a prefix for the key name. For example, the entity form
+ *     controller classes use $form_state['entity'] in entity forms to store
+ *     information about the entity being edited, and this information stays
+ *     available across successive clicks of the "Preview" button (if available)
+ *     as well as when the "Save" button is finally clicked.
  *   - buttons: A list containing copies of all submit and button elements in
  *     the form.
  *   - complete_form: A reference to the $form variable containing the complete
@@ -747,9 +749,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.
@@ -808,7 +814,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;
@@ -1469,7 +1475,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;
   }
@@ -1812,7 +1818,7 @@ function form_builder($form_id, &$element, &$form_state) {
   // checkboxes and files.
   if (isset($element['#process']) && !$element['#processed']) {
     foreach ($element['#process'] as $process) {
-      $element = $process($element, $form_state, $form_state['complete_form']);
+      $element = call_user_func_array($process, array(&$element, &$form_state, &$form_state['complete_form']));
     }
     $element['#processed'] = TRUE;
   }
diff --git a/core/modules/block/block.module b/core/modules/block/block.module
index c07b73d..fcc76f7 100644
--- a/core/modules/block/block.module
+++ b/core/modules/block/block.module
@@ -5,6 +5,8 @@
  * Controls the visual building blocks a page is constructed with.
  */
 
+use Drupal\entity\EntityFormController;
+
 /**
  * Denotes that a block is not enabled in any region and should not be shown.
  */
@@ -596,7 +598,7 @@ function block_custom_block_save($edit, $delta) {
  * Implements hook_form_FORM_ID_alter() for user_profile_form().
  */
 function block_form_user_profile_form_alter(&$form, &$form_state) {
-  $account = $form['#user'];
+  $account = $form_state['controller']->getEntity($form_state);
   $rids = array_keys($account->roles);
   $result = db_query("SELECT DISTINCT b.* FROM {block} b LEFT JOIN {block_role} r ON b.module = r.module AND b.delta = r.delta WHERE b.status = 1 AND b.custom <> 0 AND (r.rid IN (:rids) OR r.rid IS NULL) ORDER BY b.weight, b.module", array(':rids' => $rids));
 
diff --git a/core/modules/book/book.module b/core/modules/book/book.module
index 86a5d10..c169cac 100644
--- a/core/modules/book/book.module
+++ b/core/modules/book/book.module
@@ -5,6 +5,8 @@
  * Allows users to create and organize related content in an outline.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
 
 /**
@@ -423,7 +425,7 @@ function book_get_books() {
  * @see book_pick_book_nojs_submit()
  */
 function book_form_node_form_alter(&$form, &$form_state, $form_id) {
-  $node = $form['#node'];
+  $node = $form_state['controller']->getEntity($form_state);
   $access = user_access('administer book outlines');
   if (!$access) {
     if (user_access('add content to books') && ((!empty($node->book['mlid']) && !empty($node->nid)) || book_type_is_allowed($node->type))) {
@@ -462,7 +464,8 @@ function book_form_node_form_alter(&$form, &$form_state, $form_id) {
  * @see book_form_node_form_alter()
  */
 function book_pick_book_nojs_submit($form, &$form_state) {
-  $form_state['node']->book = $form_state['values']['book'];
+  $node = $form_state['controller']->getEntity($form_state);
+  $node->book = $form_state['values']['book'];
   $form_state['rebuild'] = TRUE;
 }
 
diff --git a/core/modules/comment/comment.admin.inc b/core/modules/comment/comment.admin.inc
index 0a59897..6210cc5 100644
--- a/core/modules/comment/comment.admin.inc
+++ b/core/modules/comment/comment.admin.inc
@@ -278,7 +278,7 @@ function comment_confirm_delete_page($cid) {
  * @see confirm_form()
  */
 function comment_confirm_delete($form, &$form_state, Comment $comment) {
-  $form['#comment'] = $comment;
+  $form_state['comment'] = $comment;
   // Always provide entity id in the same form key as in the entity edit form.
   $form['cid'] = array('#type' => 'value', '#value' => $comment->cid);
   return confirm_form(
@@ -295,7 +295,7 @@ function comment_confirm_delete($form, &$form_state, Comment $comment) {
  * Form submission handler for comment_confirm_delete().
  */
 function comment_confirm_delete_submit($form, &$form_state) {
-  $comment = $form['#comment'];
+  $comment = $form_state['comment'];
   // Delete the comment and its replies.
   comment_delete($comment->cid);
   drupal_set_message(t('The comment and all its replies have been deleted.'));
diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index 39faf90..212d471 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -9,7 +9,10 @@
  * book page, etc.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
+
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
@@ -106,6 +109,9 @@ function comment_entity_info() {
       'uri callback' => 'comment_uri',
       'fieldable' => TRUE,
       'controller class' => 'Drupal\comment\CommentStorageController',
+      'form controller class' => array(
+        'default' => 'Drupal\comment\CommentFormController',
+      ),
       'entity class' => 'Drupal\comment\Comment',
       'entity keys' => array(
         'id' => 'cid',
@@ -771,8 +777,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_add($node);
   }
 
   if ($additions) {
@@ -788,6 +793,15 @@ function comment_node_page_additions(Node $node) {
 }
 
 /**
+ * Returns a rendered form to comment the given node.
+ */
+function comment_add($node, $pid = NULL) {
+  $values = array('nid' => $node->nid, 'pid' => $pid, 'node_type' => 'comment_node_' . $node->type);
+  $comment = entity_create('comment', $values);
+  return entity_get_form($comment);
+}
+
+/**
  * Retrieves comments for a thread.
  *
  * @param Drupal\node\Node $node
@@ -1231,7 +1245,7 @@ function comment_form_node_type_form_alter(&$form, $form_state) {
  * Implements hook_form_BASE_FORM_ID_alter().
  */
 function comment_form_node_form_alter(&$form, $form_state) {
-  $node = $form['#node'];
+  $node = $form_state['controller']->getEntity($form_state);
   $form['comment_settings'] = array(
     '#type' => 'fieldset',
     '#access' => user_access('administer comments'),
@@ -1658,247 +1672,17 @@ 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;
-}
-
-/**
- * Form constructor for the basic commenting form.
- *
- * @see comment_form_validate()
- * @see comment_form_submit()
- * @see comment_form_build_preview()
- * @ingroup forms
- */
-function comment_form($form, &$form_state, Comment $comment) {
-  global $user;
-  $language_content = language(LANGUAGE_TYPE_CONTENT);
-
-  // During initial form build, add the comment 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['comment'])) {
-    $form_state['comment'] = $comment;
-  }
-  else {
-    $comment = $form_state['comment'];
-  }
-
-  $node = node_load($comment->nid);
-  $form['#node'] = $node;
-
-  // Use #comment-form as unique jump target, regardless of node type.
-  $form['#id'] = drupal_html_id('comment_form');
-  $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form');
-
-  $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
-  $is_admin = (!empty($comment->cid) && user_access('administer comments'));
-
-  if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
-    $form['#attached']['library'][] = array('system', 'jquery.cookie');
-    $form['#attributes']['class'][] = 'user-info-from-cookie';
-  }
-
-  // If not replying to a comment, use our dedicated page callback for new
-  // comments on nodes.
-  if (empty($comment->cid) && empty($comment->pid)) {
-    $form['#action'] = url('comment/reply/' . $comment->nid);
-  }
-
-  if (isset($form_state['comment_preview'])) {
-    $form += $form_state['comment_preview'];
-  }
-
-  $form['author'] = array(
-    '#weight' => 10,
-  );
-  // Display author information in a fieldset for comment moderators.
-  if ($is_admin) {
-    $form['author'] += array(
-      '#type' => 'fieldset',
-      '#title' => t('Administration'),
-      '#collapsible' => TRUE,
-      '#collapsed' => TRUE,
-    );
-  }
-
-  // Prepare default values for form elements.
-  if ($is_admin) {
-    $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
-    $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
-    $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O'));
-  }
-  else {
-    if ($user->uid) {
-      $author = $user->name;
-    }
-    else {
-      $author = ($comment->name ? $comment->name : '');
-    }
-    $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED);
-    $date = '';
-  }
-
-  // Add the author name field depending on the current user.
-  if ($is_admin) {
-    $form['author']['name'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Authored by'),
-      '#default_value' => $author,
-      '#maxlength' => 60,
-      '#size' => 30,
-      '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
-      '#autocomplete_path' => 'user/autocomplete',
-    );
-  }
-  elseif ($user->uid) {
-    $form['author']['_author'] = array(
-      '#type' => 'item',
-      '#title' => t('Your name'),
-      '#markup' => theme('username', array('account' => $user)),
-    );
-    $form['author']['name'] = array(
-      '#type' => 'value',
-      '#value' => $author,
-    );
-  }
-  else {
-    $form['author']['name'] = array(
-      '#type' => 'textfield',
-      '#title' => t('Your name'),
-      '#default_value' => $author,
-      '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
-      '#maxlength' => 60,
-      '#size' => 30,
-    );
-  }
-
-  // Add author e-mail and homepage fields depending on the current user.
-  $form['author']['mail'] = array(
-    '#type' => 'email',
-    '#title' => t('E-mail'),
-    '#default_value' => $comment->mail,
-    '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
-    '#maxlength' => 64,
-    '#size' => 30,
-    '#description' => t('The content of this field is kept private and will not be shown publicly.'),
-    '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
-  );
-  $form['author']['homepage'] = array(
-    '#type' => 'url',
-    '#title' => t('Homepage'),
-    '#default_value' => $comment->homepage,
-    '#maxlength' => 255,
-    '#size' => 30,
-    '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
-  );
-
-  // Add administrative comment publishing options.
-  $form['author']['date'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Authored on'),
-    '#default_value' => $date,
-    '#maxlength' => 25,
-    '#size' => 20,
-    '#access' => $is_admin,
-  );
-  $form['author']['status'] = array(
-    '#type' => 'radios',
-    '#title' => t('Status'),
-    '#default_value' => $status,
-    '#options' => array(
-      COMMENT_PUBLISHED => t('Published'),
-      COMMENT_NOT_PUBLISHED => t('Not published'),
-    ),
-    '#access' => $is_admin,
-  );
-
-  $form['subject'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Subject'),
-    '#maxlength' => 64,
-    '#default_value' => $comment->subject,
-    '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1,
-  );
-
-  // Used for conditional validation of author fields.
-  $form['is_anonymous'] = array(
-    '#type' => 'value',
-    '#value' => ($comment->cid ? !$comment->uid : !$user->uid),
-  );
-
-  // Add internal comment properties.
-  foreach (array('cid', 'pid', 'nid', 'uid') as $key) {
-    $form[$key] = array('#type' => 'value', '#value' => $comment->$key);
-  }
-  $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type);
-
-  // Make the comment inherit the node language unless specifically set.
-  $comment_langcode = $comment->langcode;
-  if ($comment_langcode == LANGUAGE_NOT_SPECIFIED) {
-    $comment_langcode = $language_content->langcode;
-  }
-
-  // Uses the language of the content as comment language.
-  $form['langcode'] = array(
-    '#type' => 'value',
-    '#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;
-}
-
-/**
- * Form submission handler for the 'preview' button in comment_form().
- */
-function comment_form_build_preview($form, &$form_state) {
-  $comment = comment_form_submit_build_comment($form, $form_state);
-  $form_state['comment_preview'] = comment_preview($comment);
-  $form_state['rebuild'] = TRUE;
+  return entity_get_form($comment);
 }
 
 /**
  * Generates a comment preview.
  *
  * @param Drupal\comment\Comment $comment
- *
- * @see comment_form_build_preview()
  */
 function comment_preview(Comment $comment) {
   global $user;
-
-  drupal_set_title(t('Preview comment'), PASS_THROUGH);
-
+  $preview_build = array();
   $node = node_load($comment->nid);
 
   if (!form_get_errors()) {
@@ -1928,7 +1712,7 @@ function comment_preview(Comment $comment) {
     $comment_build = comment_view($comment, $node);
     $comment_build['#weight'] = -100;
 
-    $form['comment_preview'] = $comment_build;
+    $preview_build['comment_preview'] = $comment_build;
   }
 
   if ($comment->pid) {
@@ -1942,169 +1726,10 @@ function comment_preview(Comment $comment) {
     $build = node_view($node);
   }
 
-  $form['comment_output_below'] = $build;
-  $form['comment_output_below']['#weight'] = 100;
+  $preview_build['comment_output_below'] = $build;
+  $preview_build['comment_output_below']['#weight'] = 100;
 
-  return $form;
-}
-
-/**
- * Form validation handler for comment_form().
- *
- * @see comment_form_submit()
- */
-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']);
-    $form_state['values']['uid'] = $account ? $account->uid : 0;
-
-    if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
-      form_set_error('date', t('You have to specify a valid date.'));
-    }
-    if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
-      form_set_error('name', t('You have to specify a valid author.'));
-    }
-  }
-  elseif ($form_state['values']['is_anonymous']) {
-    // Validate anonymous comment author fields (if given). If the (original)
-    // author of this comment was an anonymous user, verify that no registered
-    // user with this name exists.
-    if ($form_state['values']['name']) {
-      $query = db_select('users', 'u');
-      $query->addField('u', 'uid', 'uid');
-      $taken = $query
-        ->condition('name', db_like($form_state['values']['name']), 'LIKE')
-        ->countQuery()
-        ->execute()
-        ->fetchField();
-      if ($taken) {
-        form_set_error('name', t('The name you used belongs to a registered user.'));
-      }
-    }
-  }
-}
-
-/**
- * Prepare a comment for submission.
- *
- * @param Drupal\comment\Comment $comment
- *
- */
-function comment_submit(Comment $comment) {
-  if (empty($comment->date)) {
-    $comment->date = 'now';
-  }
-  $comment->created = strtotime($comment->date);
-  $comment->changed = REQUEST_TIME;
-
-  // If the comment was posted by a registered user, assign the author's ID.
-  // @todo Too fragile. Should be prepared and stored in comment_form() already.
-  if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) {
-    $comment->uid = $account->uid;
-  }
-  // If the comment was posted by an anonymous user and no author name was
-  // required, use "Anonymous" by default.
-  if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) {
-    $comment->name = variable_get('anonymous', t('Anonymous'));
-  }
-
-  // Validate the comment's subject. If not specified, extract from comment body.
-  if (trim($comment->subject) == '') {
-    // The body may be in any format, so:
-    // 1) Filter it into HTML
-    // 2) Strip out all HTML tags
-    // 3) Convert entities back to plain-text.
-    $comment_body = $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0];
-    if (isset($comment_body['format'])) {
-      $comment_text = check_markup($comment_body['value'], $comment_body['format']);
-    }
-    else {
-      $comment_text = check_plain($comment_body['value']);
-    }
-    $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE);
-    // Edge cases where the comment body is populated only by HTML tags will
-    // require a default subject.
-    if ($comment->subject == '') {
-      $comment->subject = t('(No subject)');
-    }
-  }
-  return $comment;
-}
-
-/**
- * Updates the comment entity by processing the submission's values.
- *
- * This is the default builder function for the comment form. It is called
- * during the "Save" and "Preview" submit handlers to retrieve the entity to
- * save or preview. This function can also be called by a "Next" button of a
- * wizard to update the form state's entity with the current step's values
- * before proceeding to the next step.
- *
- * @see comment_form()
- * @see comment_form_preview()
- * @see comment_form_submit()
- */
-function comment_form_submit_build_comment($form, &$form_state) {
-  $comment = $form_state['comment'];
-  entity_form_submit_build_entity('comment', $comment, $form, $form_state);
-  comment_submit($comment);
-  return $comment;
-}
-
-/**
- * Form submission handler for comment_form().
- *
- * @see comment_form_validate()
- * @see comment_form_submit_build_comment()
- */
-function comment_form_submit($form, &$form_state) {
-  $node = node_load($form_state['values']['nid']);
-  $comment = comment_form_submit_build_comment($form, $form_state);
-  if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
-    // Save the anonymous user information to a cookie for reuse.
-    if (user_is_anonymous()) {
-      user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
-    }
-
-    comment_save($comment);
-    $form_state['values']['cid'] = $comment->cid;
-
-    // Add an entry to the watchdog log.
-    watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
-
-    // Explain the approval queue if necessary.
-    if ($comment->status == COMMENT_NOT_PUBLISHED) {
-      if (!user_access('administer comments')) {
-        drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.'));
-      }
-    }
-    else {
-      drupal_set_message(t('Your comment has been posted.'));
-    }
-    $query = array();
-    // Find the current display page for this comment.
-    $page = comment_get_display_page($comment->cid, $node->type);
-    if ($page > 0) {
-      $query['page'] = $page;
-    }
-    // Redirect to the newly posted comment.
-    $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid));
-  }
-  else {
-    watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING);
-    drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error');
-    // Redirect the user to the node they are commenting on.
-    $redirect = 'node/' . $node->nid;
-  }
-  $form_state['redirect'] = $redirect;
-  // Clear the block and page caches so that anonymous users see the comment
-  // they have posted.
-  cache_invalidate(array('content' => TRUE));
+  return $preview_build;
 }
 
 /**
diff --git a/core/modules/comment/comment.pages.inc b/core/modules/comment/comment.pages.inc
index ae50379..ed91d23 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_add($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_add($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..c216707
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/CommentFormController.php
@@ -0,0 +1,378 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\comment\CommentFormController.
+ */
+
+namespace Drupal\comment;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Base for controller for comment forms.
+ */
+class CommentFormController extends EntityFormController {
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $comment) {
+    global $user;
+    $language_content = language(LANGUAGE_TYPE_CONTENT);
+
+    $node = node_load($comment->nid);
+    $form_state['comment']['node'] = $node;
+
+    // Use #comment-form as unique jump target, regardless of node type.
+    $form['#id'] = drupal_html_id('comment_form');
+    $form['#theme'] = array('comment_form__node_' . $node->type, 'comment_form');
+
+    $anonymous_contact = variable_get('comment_anonymous_' . $node->type, COMMENT_ANONYMOUS_MAYNOT_CONTACT);
+    $is_admin = (!empty($comment->cid) && user_access('administer comments'));
+
+    if (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT) {
+      $form['#attached']['library'][] = array('system', 'jquery.cookie');
+      $form['#attributes']['class'][] = 'user-info-from-cookie';
+    }
+
+    // If not replying to a comment, use our dedicated page callback for new
+    // comments on nodes.
+    if (empty($comment->cid) && empty($comment->pid)) {
+      $form['#action'] = url('comment/reply/' . $comment->nid);
+    }
+
+    if (isset($form_state['comment_preview'])) {
+      $form += $form_state['comment_preview'];
+    }
+
+    $form['author'] = array(
+      '#weight' => 10,
+    );
+    // Display author information in a fieldset for comment moderators.
+    if ($is_admin) {
+      $form['author'] += array(
+        '#type' => 'fieldset',
+        '#title' => t('Administration'),
+        '#collapsible' => TRUE,
+        '#collapsed' => TRUE,
+      );
+    }
+
+    // Prepare default values for form elements.
+    if ($is_admin) {
+      $author = (!$comment->uid && $comment->name ? $comment->name : $comment->registered_name);
+      $status = (isset($comment->status) ? $comment->status : COMMENT_NOT_PUBLISHED);
+      $date = (!empty($comment->date) ? $comment->date : format_date($comment->created, 'custom', 'Y-m-d H:i O'));
+    }
+    else {
+      if ($user->uid) {
+        $author = $user->name;
+      }
+      else {
+        $author = ($comment->name ? $comment->name : '');
+      }
+      $status = (user_access('skip comment approval') ? COMMENT_PUBLISHED : COMMENT_NOT_PUBLISHED);
+      $date = '';
+    }
+
+    // Add the author name field depending on the current user.
+    if ($is_admin) {
+      $form['author']['name'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Authored by'),
+        '#default_value' => $author,
+        '#maxlength' => 60,
+        '#size' => 30,
+        '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
+        '#autocomplete_path' => 'user/autocomplete',
+      );
+    }
+    elseif ($user->uid) {
+      $form['author']['_author'] = array(
+        '#type' => 'item',
+        '#title' => t('Your name'),
+        '#markup' => theme('username', array('account' => $user)),
+      );
+
+      $form['author']['name'] = array(
+        '#type' => 'value',
+        '#value' => $author,
+      );
+    }
+    else {
+      $form['author']['name'] = array(
+        '#type' => 'textfield',
+        '#title' => t('Your name'),
+        '#default_value' => $author,
+        '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
+        '#maxlength' => 60,
+        '#size' => 30,
+      );
+    }
+
+    // Add author e-mail and homepage fields depending on the current user.
+    $form['author']['mail'] = array(
+      '#type' => 'email',
+      '#title' => t('E-mail'),
+      '#default_value' => $comment->mail,
+      '#required' => (!$user->uid && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),
+      '#maxlength' => 64,
+      '#size' => 30,
+      '#description' => t('The content of this field is kept private and will not be shown publicly.'),
+      '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
+    );
+
+    $form['author']['homepage'] = array(
+      '#type' => 'url',
+      '#title' => t('Homepage'),
+      '#default_value' => $comment->homepage,
+      '#maxlength' => 255,
+      '#size' => 30,
+      '#access' => $is_admin || (!$user->uid && $anonymous_contact != COMMENT_ANONYMOUS_MAYNOT_CONTACT),
+    );
+
+    // Add administrative comment publishing options.
+    $form['author']['date'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Authored on'),
+      '#default_value' => $date,
+      '#maxlength' => 25,
+      '#size' => 20,
+      '#access' => $is_admin,
+    );
+
+    $form['author']['status'] = array(
+      '#type' => 'radios',
+      '#title' => t('Status'),
+      '#default_value' => $status,
+      '#options' => array(
+        COMMENT_PUBLISHED => t('Published'),
+        COMMENT_NOT_PUBLISHED => t('Not published'),
+      ),
+      '#access' => $is_admin,
+    );
+
+    $form['subject'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Subject'),
+      '#maxlength' => 64,
+      '#default_value' => $comment->subject,
+      '#access' => variable_get('comment_subject_field_' . $node->type, 1) == 1,
+    );
+
+    // Used for conditional validation of author fields.
+    $form['is_anonymous'] = array(
+      '#type' => 'value',
+      '#value' => ($comment->cid ? !$comment->uid : !$user->uid),
+    );
+
+    // Add internal comment properties.
+    foreach (array('cid', 'pid', 'nid', 'uid') as $key) {
+      $form[$key] = array('#type' => 'value', '#value' => $comment->$key);
+    }
+    $form['node_type'] = array('#type' => 'value', '#value' => 'comment_node_' . $node->type);
+
+    // Make the comment inherit the node language unless specifically set.
+    $comment_langcode = $comment->langcode;
+    if ($comment_langcode == LANGUAGE_NOT_SPECIFIED) {
+      $comment_langcode = $language_content->langcode;
+    }
+
+    // Uses the language of the content as comment language.
+    $form['langcode'] = array(
+      '#type' => 'value',
+      '#value' => $comment_langcode,
+    );
+
+    // Attach fields.
+    $comment->node_type = 'comment_node_' . $node->type;
+
+    return parent::form($form, $form_state, $comment);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    $element = parent::actions($form, $form_state);
+    $comment = $this->getEntity($form_state);
+    $node = $form_state['comment']['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,
+      '#validate' => array(
+        array($this, 'validate'),
+      ),
+      '#submit' => array(
+        array($this, 'submit'),
+        array($this, 'preview'),
+      ),
+    );
+
+    $element['#weight'] = $form['comment_body']['#weight'] + 0.01;
+
+    return $element;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($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']);
+      $form_state['values']['uid'] = $account ? $account->uid : 0;
+
+      if ($form_state['values']['date'] && strtotime($form_state['values']['date']) === FALSE) {
+        form_set_error('date', t('You have to specify a valid date.'));
+      }
+      if ($form_state['values']['name'] && !$form_state['values']['is_anonymous'] && !$account) {
+        form_set_error('name', t('You have to specify a valid author.'));
+      }
+    }
+    elseif ($form_state['values']['is_anonymous']) {
+      // Validate anonymous comment author fields (if given). If the (original)
+      // author of this comment was an anonymous user, verify that no registered
+      // user with this name exists.
+      if ($form_state['values']['name']) {
+        $query = db_select('users', 'u');
+        $query->addField('u', 'uid', 'uid');
+        $taken = $query
+        ->condition('name', db_like($form_state['values']['name']), 'LIKE')
+        ->countQuery()
+        ->execute()
+        ->fetchField();
+        if ($taken) {
+          form_set_error('name', t('The name you used belongs to a registered user.'));
+        }
+      }
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::submit().
+   */
+  public function submit(array $form, array &$form_state) {
+    $comment = parent::submit($form, $form_state);
+
+    if (empty($comment->date)) {
+      $comment->date = 'now';
+    }
+    $comment->created = strtotime($comment->date);
+    $comment->changed = REQUEST_TIME;
+
+    // If the comment was posted by a registered user, assign the author's ID.
+    // @todo Too fragile. Should be prepared and stored in comment_form()
+    // already.
+    if (!$comment->is_anonymous && !empty($comment->name) && ($account = user_load_by_name($comment->name))) {
+      $comment->uid = $account->uid;
+    }
+    // If the comment was posted by an anonymous user and no author name was
+    // required, use "Anonymous" by default.
+    if ($comment->is_anonymous && (!isset($comment->name) || $comment->name === '')) {
+      $comment->name = variable_get('anonymous', t('Anonymous'));
+    }
+
+    // Validate the comment's subject. If not specified, extract from comment
+    // body.
+    if (trim($comment->subject) == '') {
+      // The body may be in any format, so:
+      // 1) Filter it into HTML
+      // 2) Strip out all HTML tags
+      // 3) Convert entities back to plain-text.
+      $comment_body = $comment->comment_body[LANGUAGE_NOT_SPECIFIED][0];
+      if (isset($comment_body['format'])) {
+        $comment_text = check_markup($comment_body['value'], $comment_body['format']);
+      }
+      else {
+        $comment_text = check_plain($comment_body['value']);
+      }
+      $comment->subject = truncate_utf8(trim(decode_entities(strip_tags($comment_text))), 29, TRUE);
+      // Edge cases where the comment body is populated only by HTML tags will
+      // require a default subject.
+      if ($comment->subject == '') {
+        $comment->subject = t('(No subject)');
+      }
+    }
+
+    return $comment;
+  }
+
+  /**
+   * Form submission handler for the 'preview' 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 preview(array $form, array &$form_state) {
+    $comment = $this->getEntity($form_state);
+    drupal_set_title(t('Preview comment'), PASS_THROUGH);
+    $form_state['comment_preview'] = comment_preview($comment);
+    $form_state['rebuild'] = TRUE;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $node = node_load($form_state['values']['nid']);
+    $comment = $this->getEntity($form_state);
+
+    if (user_access('post comments') && (user_access('administer comments') || $node->comment == COMMENT_NODE_OPEN)) {
+      // Save the anonymous user information to a cookie for reuse.
+      if (user_is_anonymous()) {
+        user_cookie_save(array_intersect_key($form_state['values'], array_flip(array('name', 'mail', 'homepage'))));
+      }
+
+      comment_save($comment);
+      $form_state['values']['cid'] = $comment->cid;
+
+      // Add an entry to the watchdog log.
+      watchdog('content', 'Comment posted: %subject.', array('%subject' => $comment->subject), WATCHDOG_NOTICE, l(t('view'), 'comment/' . $comment->cid, array('fragment' => 'comment-' . $comment->cid)));
+
+      // Explain the approval queue if necessary.
+      if ($comment->status == COMMENT_NOT_PUBLISHED) {
+        if (!user_access('administer comments')) {
+          drupal_set_message(t('Your comment has been queued for review by site administrators and will be published after approval.'));
+        }
+      }
+      else {
+        drupal_set_message(t('Your comment has been posted.'));
+      }
+      $query = array();
+      // Find the current display page for this comment.
+      $page = comment_get_display_page($comment->cid, $node->type);
+      if ($page > 0) {
+        $query['page'] = $page;
+      }
+      // Redirect to the newly posted comment.
+      $redirect = array('node/' . $node->nid, array('query' => $query, 'fragment' => 'comment-' . $comment->cid));
+    }
+    else {
+      watchdog('content', 'Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject), WATCHDOG_WARNING);
+      drupal_set_message(t('Comment: unauthorized comment submitted or comment submitted to a closed post %subject.', array('%subject' => $comment->subject)), 'error');
+      // Redirect the user to the node they are commenting on.
+      $redirect = 'node/' . $node->nid;
+    }
+    $form_state['redirect'] = $redirect;
+    // Clear the block and page caches so that anonymous users see the comment
+    // they have posted.
+    cache_invalidate(array('content' => TRUE));
+  }
+}
diff --git a/core/modules/contact/contact.module b/core/modules/contact/contact.module
index 245c125..8619058 100644
--- a/core/modules/contact/contact.module
+++ b/core/modules/contact/contact.module
@@ -5,6 +5,8 @@
  * Enables the use of personal and site-wide contact forms.
  */
 
+use Drupal\entity\EntityFormController;
+
 /**
  * Implements hook_help().
  */
@@ -221,7 +223,7 @@ function contact_form_user_profile_form_alter(&$form, &$form_state) {
     '#weight' => 5,
     '#collapsible' => TRUE,
   );
-  $account = $form['#user'];
+  $account = $form_state['controller']->getEntity($form_state);
   $form['contact']['contact'] = array(
     '#type' => 'checkbox',
     '#title' => t('Personal contact form'),
diff --git a/core/modules/entity/entity.api.php b/core/modules/entity/entity.api.php
index 5359376..98e32b8 100644
--- a/core/modules/entity/entity.api.php
+++ b/core/modules/entity/entity.api.php
@@ -27,6 +27,13 @@
  *     The class has to implement the
  *     Drupal\entity\EntityStorageControllerInterface interface. Leave blank
  *     to use the Drupal\entity\DatabaseStorageController implementation.
+ *   - form controller class: An associative array where the keys are the names
+ *     of the different form operations (such as creation, editing or deletion)
+ *     and the values are the names of the controller classes. To facilitate
+ *     supporting the case where an entity form varies only slightly between
+ *     different operations, the name of the operation is passed also to the
+ *     constructor of the form controller class. This way, one class can be used
+ *     for multiple entity forms.
  *   - base table: (used by Drupal\entity\DatabaseStorageController) The
  *     name of the entity type's base table.
  *   - static cache: (used by Drupal\entity\DatabaseStorageController)
@@ -139,6 +146,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 3d37a2a..bfbeb51 100644
--- a/core/modules/entity/entity.module
+++ b/core/modules/entity/entity.module
@@ -73,6 +73,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(),
@@ -418,16 +421,123 @@ function entity_page_label(EntityInterface $entity, $langcode = NULL) {
 }
 
 /**
- * Attaches field API validation to entity forms.
+ * Returns an entity form controller for the given operation.
+ *
+ * Since there might be different scenarios in which an entity is edited,
+ * multiple form controllers suitable to the different operations may be defined.
+ * If no controller is found for the default operation, the base class will be
+ * used. If a non-existing non-default operation is specified an exception will
+ * be thrown.
+ *
+ * @see hook_entity_info()
+ *
+ * @param $entity_type
+ *   The type of the entity.
+ * @param $operation
+ *   (optional) The name of an operation, such as creation, editing or deletion,
+ *   identifying the controlled form. Defaults to 'default' which is the usual
+ *   create/edit form.
+ *
+ * @return Drupal\entity\EntityFormControllerInterface
+ *   An entity form controller instance.
+ */
+function entity_form_controller($entity_type, $operation = 'default') {
+  $info = entity_get_info($entity_type);
+
+  // Check whether there is a form controller class for the specified operation.
+  if (!empty($info['form controller class'][$operation])) {
+    $class = $info['form controller class'][$operation];
+  }
+  // If no controller is specified default to the base implementation.
+  elseif (empty($info['form controller class']) && $operation == 'default') {
+    $class = 'Drupal\entity\EntityFormController';
+  }
+  // If a non-existing operation has been specified stop.
+  else {
+    throw new EntityMalformedException("Missing form controller for '$entity_type', operation '$operation'");
+  }
+
+  return new $class($operation);
+}
+
+/**
+ * Returns the form id for the given entity and operation.
+ *
+ * @param EntityInterface $entity
+ *   The entity to be created or edited.
+ * @param $operation
+ *   (optional) The operation for the form to be processed.
+ *
+ * @return
+ *   A string representing the entity form id.
+ */
+function entity_form_id(EntityInterface $entity, $operation = 'default') {
+  $entity_type = $entity->entityType();
+  $bundle = $entity->bundle();
+  $form_id = $entity_type;
+  if ($bundle != $entity_type) {
+    $form_id = $bundle . '_' . $form_id;
+  }
+  if ($operation != 'default') {
+    $form_id = $form_id . '_' . $operation;
+  }
+  return $form_id . '_form';
+}
+
+/**
+ * Returns the default form state for the given entity and operation.
+ *
+ * @param EntityInterface $entity
+ *   The entity to be created or edited.
+ * @param $operation
+ *   (optional) The operation identifying the form to be processed.
+ *
+ * @return
+ *   A $form_state array already filled the entity form controller.
+ */
+function entity_form_state_defaults(EntityInterface $entity, $operation = 'default') {
+  $form_state = array();
+  $controller = entity_form_controller($entity->entityType(), $operation);
+  $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);
+  return $form_state;
+}
+
+/**
+ * Retrieves, populates, and processes an entity form.
+ *
+ * @param EntityInterface $entity
+ *   The entity to be created or edited.
+ * @param $operation
+ *   (optional) The operation identifying the form to be submitted.
+ * @param $form_state
+ *   (optional) A keyed array containing the current state of the form.
+ *
+ * @return
+ *   A $form_state array already filled with the entity form controller.
+ */
+function entity_form_submit(EntityInterface $entity, $operation = 'default', &$form_state = array()) {
+  $form_state += entity_form_state_defaults($entity, $operation);
+  $form_id = entity_form_id($entity, $operation);
+  drupal_form_submit($form_id, $form_state);
+}
+
+/**
+ * Returns the built and processed entity form for the given entity.
+ *
+ * @param EntityInterface $entity
+ *   The entity to be created or edited.
+ * @param $operation
+ *   (optional) The operation identifying the form variation to be returned.
+ *
+ * @return
+ *   The processed form for the given entity and operation.
  */
-function entity_form_field_validate($entity_type, $form, &$form_state) {
-  // All field attach API functions act on an entity object, but during form
-  // validation, we don't have one. $form_state contains the entity as it was
-  // prior to processing the current form submission, and we must not update it
-  // until we have fully validated the submitted input. Therefore, for
-  // validation, act on a pseudo entity created out of the form values.
-  $pseudo_entity = entity_create($entity_type, $form_state['values']);
-  field_attach_form_validate($entity_type, $pseudo_entity, $form, $form_state);
+function entity_get_form(EntityInterface $entity, $operation = 'default') {
+  $form_state = entity_form_state_defaults($entity, $operation);
+  $form_id = entity_form_id($entity, $operation);
+  return drupal_build_form($form_id, $form_state);
 }
 
 /**
diff --git a/core/modules/entity/lib/Drupal/entity/Entity.php b/core/modules/entity/lib/Drupal/entity/Entity.php
index eeaeee8..e2afd2a 100644
--- a/core/modules/entity/lib/Drupal/entity/Entity.php
+++ b/core/modules/entity/lib/Drupal/entity/Entity.php
@@ -269,5 +269,4 @@ class Entity implements EntityInterface {
   public function isCurrentRevision() {
     return $this->isCurrentRevision;
   }
-
 }
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..db0050b
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/EntityFormController.php
@@ -0,0 +1,256 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\EntityFormController.
+ */
+
+namespace Drupal\entity;
+
+/**
+ * Base class for entity form controllers.
+ */
+class EntityFormController implements EntityFormControllerInterface {
+
+  /**
+   * The name of the current operation.
+   *
+   * Subclasses may use this to implement different behaviors depending on its
+   * value.
+   *
+   * @var string
+   */
+  protected $operation;
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::__construct().
+   */
+  public function __construct($operation) {
+    $this->operation = $operation;
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::build().
+   */
+  public function build(array $form, array &$form_state, EntityInterface $entity) {
+
+    // During the 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 (!$this->getEntity($form_state)) {
+      $this->init($form_state, $entity);
+    }
+
+    // Retrieve the form array using the possibly updated entity in form state.
+    $entity = $this->getEntity($form_state);
+    $form = $this->form($form, $form_state, $entity);
+
+    // Retrieve and add the form actions array.
+    $actions = $this->actionsElement($form, $form_state);
+    if (!empty($actions)) {
+      $form['actions'] = $actions;
+    }
+
+    return $form;
+  }
+
+  /**
+   * Initialise the form state and the entity before the first form build.
+   */
+  protected function init(array &$form_state, EntityInterface $entity) {
+    // Add the controller to the form state so it can be easily accessed by
+    // module-provided form handlers there.
+    $form_state['controller'] = $this;
+    $this->setEntity($entity, $form_state);
+    $this->prepareEntity($entity);
+  }
+
+  /**
+   * Returns the actual form array to be built.
+   *
+   * @see Drupal\entity\EntityFormController::build()
+   */
+  public 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 function actionsElement(array $form, array &$form_state) {
+    $element = $this->actions($form, $form_state);
+
+    // We cannot delete an entity that has not been created yet.
+    if ($this->getEntity($form_state)->isNew()) {
+      unset($element['delete']);
+    }
+    elseif (isset($element['delete'])) {
+      // Move the delete action as last one, unless weights are explicitly
+      // provided.
+      $delete = $element['delete'];
+      unset($element['delete']);
+      $element['delete'] = $delete;
+    }
+
+    $count = 0;
+    foreach (element_children($element) as $action) {
+      $element[$action] += array(
+        '#type' => 'submit',
+        '#weight' => ++$count * 5,
+      );
+    }
+
+    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) {
+    return array(
+      // @todo Rename the action key from submit to save.
+      'submit' => array(
+        '#value' => t('Save'),
+        '#validate' => array(
+          array($this, 'validate'),
+        ),
+        '#submit' => array(
+          array($this, 'submit'),
+          array($this, 'save'),
+        ),
+      ),
+      'delete' => array(
+        '#value' => t('Delete'),
+        // No need to validate the form when deleting the entity.
+        '#submit' => array(
+          array($this, 'delete'),
+        ),
+      ),
+      // @todo Consider introducing a 'preview' action here, since it is used by
+      // many entity types.
+    );
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    // @todo Exploit the Property API to validate the values submitted for the
+    // entity properties.
+    $entity = $this->buildEntity($form, $form_state);
+    $info = $entity->entityInfo();
+
+    if (!empty($info['fieldable'])) {
+      field_attach_form_validate($entity->entityType(), $entity, $form, $form_state);
+    }
+
+    // @todo Remove this.
+    // Execute legacy global validation handlers.
+    unset($form_state['validate_handlers']);
+    form_execute_handlers('validate', $form, $form_state);
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::submit().
+   *
+   * This is the default entity object builder function. It is called before any
+   * other submit handler to build the new entity object to be passed to the
+   * following submit handlers. At this point of the form workflow the entity is
+   * validated and the form state can be updated, this way the subsequently
+   * invoked handlers can retrieve a regular entity object to act on.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function submit(array $form, array &$form_state) {
+    $entity = $this->buildEntity($form, $form_state);
+    $this->setEntity($entity, $form_state);
+    return $entity;
+  }
+
+  /**
+   * Form submission handler for the 'save' action.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function save(array $form, array &$form_state) {
+    // @todo Perform common save operations.
+  }
+
+  /**
+   * Form submission handler for the 'delete' action.
+   *
+   * @param array $form
+   *   An associative array containing the structure of the form.
+   * @param array $form_state
+   *   A reference to a keyed array containing the current state of the form.
+   */
+  public function delete(array $form, array &$form_state) {
+    // @todo Perform common delete operations.
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::getFormLangcode().
+   */
+  public function getFormLangcode($form_state) {
+    // @todo Introduce a new form language type (see hook_language_types_info())
+    // to be used as the default active form language, should it be missing, so
+    // so that entity forms can be used to submit multilingual values.
+    $language = $this->getEntity($form_state)->language();
+    return !empty($language->langcode) ? $language->langcode : NULL;
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::buildEntity().
+   */
+  public function buildEntity(array $form, array &$form_state) {
+    $entity = clone $this->getEntity($form_state);
+    // @todo Move entity_form_submit_build_entity() here.
+    // @todo Exploit the Property API to process the submitted entity property.
+    entity_form_submit_build_entity($entity->entityType(), $entity, $form, $form_state);
+    return $entity;
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::getEntity().
+   */
+  public function getEntity(array $form_state) {
+    return isset($form_state['entity']) ? $form_state['entity'] : NULL;
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::setEntity().
+   */
+  public function setEntity(EntityInterface $entity, array &$form_state) {
+    $form_state['entity'] = $entity;
+  }
+
+  /**
+   * Prepares the entity object before the form is built first.
+   */
+  protected function prepareEntity(EntityInterface $entity) {
+    // @todo Perform common prepare operations and add a hook.
+  }
+
+  /**
+   * Implements Drupal\entity\EntityFormControllerInterface::getOperation().
+   */
+  public function getOperation() {
+    return $this->operation;
+  }
+}
diff --git a/core/modules/entity/lib/Drupal/entity/EntityFormControllerInterface.php b/core/modules/entity/lib/Drupal/entity/EntityFormControllerInterface.php
new file mode 100644
index 0000000..6765d85
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/EntityFormControllerInterface.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\EntityFormControllerInterface.
+ */
+
+namespace Drupal\entity;
+
+/**
+ * Defines a common interface for entity form controller classes.
+ */
+interface EntityFormControllerInterface {
+
+  /**
+   * Constructs the object.
+   *
+   * @param string $operation
+   *   The name of the current operation.
+   */
+  public function __construct($operation);
+
+  /**
+   * Builds an entity form.
+   *
+   * This is the entity form builder which is invoked via drupal_build_form()
+   * to retrieve the form.
+   *
+   * @param array $form
+   *   A nested array form elements comprising the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   * @param string $entity_type
+   *   The type of the entity being edited.
+   * @param \Drupal\entity\EntityInterface $entity
+   *   The entity being edited.
+   *
+   * @return array
+   *   The array containing the complete form.
+   */
+  public function build(array $form, array &$form_state, EntityInterface $entity);
+
+  /**
+   * Returns the code identifying the active form language.
+   *
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return string
+   *   The form language code.
+   */
+  public function getFormLangcode($form_state);
+
+  /**
+   * Returns the operation identifying the form controller.
+   *
+   * @return string
+   *   The name of the operation.
+   */
+  public function getOperation();
+
+  /**
+   * Returns the form entity.
+   *
+   * The form entity which has been used for populating form element defaults.
+   *
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return \Drupal\entity\EntityInterface
+   *   The current form entity.
+   */
+  public function getEntity(array $form_state);
+
+  /**
+   * Sets the form entity.
+   *
+   * Sets the form entity which will be used for populating form element
+   * defaults. Usually, the form entity gets updated by
+   * \Drupal\entity\EntityFormControllerInterface::submit(), however this may
+   * be used to completely exchange the form entity, e.g. when preparing the
+   * rebuild of a multi-step form.
+   *
+   * @param \Drupal\entity\EntityInterface $entity
+   *   The entity the current form should operate upon.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   */
+  public function setEntity(EntityInterface $entity, array &$form_state);
+
+  /**
+   * Builds an updated entity object based upon the submitted form values.
+   *
+   * For building the updated entity object the form's entity is cloned and
+   * the submitted form values are copied to entity properties. The form's
+   * entity remains unchanged.
+   *
+   * @see \Drupal\entity\EntityFormControllerInterface::getEntity()
+   *
+   * @param array $form
+   *   A nested array form elements comprising the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   *
+   * @return \Drupal\entity\EntityInterface
+   *   An updated copy of the form's entity object.
+   */
+  public function buildEntity(array $form, array &$form_state);
+
+  /**
+   * Validates the submitted form values of the entity form.
+   *
+   * @param array $form
+   *   A nested array form elements comprising the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   */
+  public function validate(array $form, array &$form_state);
+
+  /**
+   * Updates the form's entity by processing this submission's values.
+   *
+   * Note: Before this can be safely invoked the entity form must have passed
+   * validation, i.e. only add this as form #submit handler if validation is
+   * added as well.
+   *
+   * @param array $form
+   *   A nested array form elements comprising the form.
+   * @param array $form_state
+   *   An associative array containing the current state of the form.
+   */
+  public function submit(array $form, array &$form_state);
+
+}
diff --git a/core/modules/entity/lib/Drupal/entity/EntityInterface.php b/core/modules/entity/lib/Drupal/entity/EntityInterface.php
index 699c424..7aedb48 100644
--- a/core/modules/entity/lib/Drupal/entity/EntityInterface.php
+++ b/core/modules/entity/lib/Drupal/entity/EntityInterface.php
@@ -204,5 +204,4 @@ interface EntityInterface {
    *   TRUE if the entity is the current revision, FALSE otherwise.
    */
   public function isCurrentRevision();
-
 }
diff --git a/core/modules/field/tests/modules/field_test/field_test.entity.inc b/core/modules/field/tests/modules/field_test/field_test.entity.inc
index 73787e9..9283bfe 100644
--- a/core/modules/field/tests/modules/field_test/field_test.entity.inc
+++ b/core/modules/field/tests/modules/field_test/field_test.entity.inc
@@ -335,7 +335,8 @@ function field_test_entity_form($form, &$form_state, $entity, $add = FALSE) {
  * Validate handler for field_test_entity_form().
  */
 function field_test_entity_form_validate($form, &$form_state) {
-  entity_form_field_validate('test_entity', $form, $form_state);
+  $pseudo_entity = entity_create('test_entity', $form_state['values']);
+  field_attach_form_validate('test_entity', $pseudo_entity, $form, $form_state);
 }
 
 /**
diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module
index a12be0e..5f84637 100644
--- a/core/modules/forum/forum.module
+++ b/core/modules/forum/forum.module
@@ -591,9 +591,9 @@ function forum_field_storage_pre_update($entity_type, $entity, &$skip_fields) {
 }
 
 /**
- * Implements hook_form_FORM_ID_alter() for taxonomy_form_vocabulary().
+ * Implements hook_form_BASE_FORM_ID_alter().
  */
-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 = config('forum.settings')->get('vocabulary');
   if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
     $form['help_forum_vocab'] = array(
@@ -610,9 +610,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 = config('forum.settings')->get('vocabulary');
   if (isset($form['vid']['#value']) && $form['vid']['#value'] == $vid) {
     // Hide multiple parents select from forum terms.
diff --git a/core/modules/menu/menu.module b/core/modules/menu/menu.module
index 7984fe0..b612b60 100644
--- a/core/modules/menu/menu.module
+++ b/core/modules/menu/menu.module
@@ -11,7 +11,10 @@
  * URLs to be added to the main site navigation menu.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
+
 use Symfony\Component\HttpFoundation\JsonResponse;
 
 /**
@@ -629,8 +632,9 @@ function _menu_parent_depth_limit($item) {
 function menu_form_node_form_alter(&$form, $form_state) {
   // Generate a list of possible parents (not including this link or descendants).
   // @todo This must be handled in a #process handler.
-  $link = $form['#node']->menu;
-  $type = $form['#node']->type;
+  $node = $form_state['controller']->getEntity($form_state);
+  $link = $node->menu;
+  $type = $node->type;
   // menu_parent_options() is goofy and can actually handle either a menu link
   // or a node type both as second argument. Pick based on whether there is
   // a link already (menu_node_prepare() sets mlid default to 0).
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..ccf8daa
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeFormController.php
@@ -0,0 +1,433 @@
+<?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 {
+
+  /**
+   * Prepares the node object.
+   *
+   * Fills in a few default values, and then invokes hook_prepare() on the node
+   * type module, and hook_node_prepare() on all modules.
+   *
+   * Overrides Drupal\entity\EntityFormController::prepareEntity().
+   */
+  protected function prepareEntity(EntityInterface $node) {
+    // Set up default values, if required.
+    $node_options = variable_get('node_options_' . $node->type, array('status', 'promote'));
+    // If this is a new node, fill in the default values.
+    if (!isset($node->nid) || isset($node->is_new)) {
+      foreach (array('status', 'promote', 'sticky') as $key) {
+        // Multistep node forms might have filled in something already.
+        if (!isset($node->$key)) {
+          $node->$key = (int) in_array($key, $node_options);
+        }
+      }
+      global $user;
+      $node->uid = $user->uid;
+      $node->created = REQUEST_TIME;
+    }
+    else {
+      $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
+      // Remove the log message from the original node entity.
+      $node->log = NULL;
+    }
+    // Always use the default revision setting.
+    $node->revision = in_array('revision', $node_options);
+
+    node_invoke($node, 'prepare');
+    module_invoke_all('node_prepare', $node);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $node) {
+    // Some special stuff when previewing a node.
+    if (isset($form_state['node_preview'])) {
+      $form['#prefix'] = $form_state['node_preview'];
+      $node->in_preview = TRUE;
+    }
+    else {
+      unset($node->in_preview);
+    }
+
+    // Override the default CSS class name, since the user-defined node type
+    // name in 'TYPE-node-form' potentially clashes with third-party class
+    // names.
+    $form['#attributes']['class'][0] = drupal_html_class('node-' . $node->type . '-form');
+
+    // Basic node information.
+    // These elements are just values so they are not even sent to the client.
+    foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) {
+      $form[$key] = array(
+        '#type' => 'value',
+        '#value' => isset($node->$key) ? $node->$key : NULL,
+      );
+    }
+
+    // Changed must be sent to the client, for later overwrite error checking.
+    $form['changed'] = array(
+      '#type' => 'hidden',
+      '#default_value' => isset($node->changed) ? $node->changed : NULL,
+    );
+
+    // Invoke hook_form() to get the node-specific bits. Can't use node_invoke()
+    // because hook_form() needs to be able to receive $form_state by reference.
+    // @todo hook_form() implementations are unable to add #validate or #submit
+    //   handlers to the form buttons below. Remove hook_form() entirely.
+    $function = node_type_get_base($node) . '_form';
+    if (function_exists($function) && ($extra = $function($node, $form_state))) {
+      $form = array_merge_recursive($form, $extra);
+    }
+    // If the node type has a title, and the node type form defined no special
+    // weight for it, we default to a weight of -5 for consistency.
+    if (isset($form['title']) && !isset($form['title']['#weight'])) {
+      $form['title']['#weight'] = -5;
+    }
+
+    if (module_exists('language')) {
+      $languages = language_list(LANGUAGE_ALL);
+      $language_options = array();
+      foreach ($languages as $langcode => $language) {
+        // Make locked languages appear special in the list.
+        $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name;
+      }
+
+      $form['langcode'] = array(
+        '#type' => 'select',
+        '#title' => t('Language'),
+        '#default_value' => $node->langcode,
+        '#options' => $language_options,
+        '#access' => !variable_get('node_type_language_hidden_' . $node->type, TRUE),
+      );
+    }
+    else {
+      $form['langcode'] = array(
+        '#type' => 'value',
+        '#value' => $node->langcode,
+      );
+    }
+
+    $form['additional_settings'] = array(
+      '#type' => 'vertical_tabs',
+      '#weight' => 99,
+    );
+
+    // Add a log field if the "Create new revision" option is checked, or if the
+    // current user has the ability to check that option.
+    $form['revision_information'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Revision information'),
+      '#collapsible' => TRUE,
+      // Collapsed by default when "Create new revision" is unchecked
+      '#collapsed' => !$node->revision,
+      '#group' => 'additional_settings',
+      '#attributes' => array(
+        'class' => array('node-form-revision-information'),
+      ),
+      '#attached' => array(
+        'js' => array(drupal_get_path('module', 'node') . '/node.js'),
+      ),
+      '#weight' => 20,
+      '#access' => $node->revision || user_access('administer nodes'),
+    );
+
+    $form['revision_information']['revision'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Create new revision'),
+      '#default_value' => $node->revision,
+      '#access' => user_access('administer nodes'),
+    );
+
+    // Check the revision log checkbox when the log textarea is filled in.
+    // This must not happen if "Create new revision" is enabled by default,
+    // since the state would auto-disable the checkbox otherwise.
+    if (!$node->revision) {
+      $form['revision_information']['revision']['#states'] = array(
+        'checked' => array(
+          'textarea[name="log"]' => array('empty' => FALSE),
+        ),
+      );
+    }
+
+    $form['revision_information']['log'] = array(
+      '#type' => 'textarea',
+      '#title' => t('Revision log message'),
+      '#rows' => 4,
+      '#default_value' => !empty($node->log) ? $node->log : '',
+      '#description' => t('Briefly describe the changes you have made.'),
+    );
+
+    // Node author information for administrators.
+    $form['author'] = array(
+      '#type' => 'fieldset',
+      '#access' => user_access('administer nodes'),
+      '#title' => t('Authoring information'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#group' => 'additional_settings',
+      '#attributes' => array(
+        'class' => array('node-form-author'),
+      ),
+      '#attached' => array(
+        'js' => array(
+          drupal_get_path('module', 'node') . '/node.js',
+          array(
+            'type' => 'setting',
+            'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))),
+          ),
+        ),
+      ),
+      '#weight' => 90,
+    );
+
+    $form['author']['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Authored by'),
+      '#maxlength' => 60,
+      '#autocomplete_path' => 'user/autocomplete',
+      '#default_value' => !empty($node->name) ? $node->name : '',
+      '#weight' => -1,
+      '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
+    );
+
+    $form['author']['date'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Authored on'),
+      '#maxlength' => 25,
+      '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => !empty($node->date) ? date_format(date_create($node->date), 'Y-m-d H:i:s O') : format_date($node->created, 'custom', 'Y-m-d H:i:s O'), '%timezone' => !empty($node->date) ? date_format(date_create($node->date), 'O') : format_date($node->created, 'custom', 'O'))),
+      '#default_value' => !empty($node->date) ? $node->date : '',
+    );
+
+    // Node options for administrators.
+    $form['options'] = array(
+      '#type' => 'fieldset',
+      '#access' => user_access('administer nodes'),
+      '#title' => t('Publishing options'),
+      '#collapsible' => TRUE,
+      '#collapsed' => TRUE,
+      '#group' => 'additional_settings',
+      '#attributes' => array(
+        'class' => array('node-form-options'),
+      ),
+      '#attached' => array(
+        'js' => array(drupal_get_path('module', 'node') . '/node.js'),
+      ),
+      '#weight' => 95,
+    );
+
+    $form['options']['status'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Published'),
+      '#default_value' => $node->status,
+    );
+
+    $form['options']['promote'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Promoted to front page'),
+      '#default_value' => $node->promote,
+    );
+
+    $form['options']['sticky'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Sticky at top of lists'),
+      '#default_value' => $node->sticky,
+    );
+
+    // 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.
+    $form += array('#submit' => array());
+
+    return parent::form($form, $form_state, $node);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    $element = parent::actions($form, $form_state);
+    $node = $this->getEntity($form_state);
+    $preview_mode = variable_get('node_preview_' . $node->type, DRUPAL_OPTIONAL);
+
+    $element['preview'] = array(
+      '#access' => $preview_mode != DRUPAL_DISABLED,
+      '#value' => t('Preview'),
+      '#validate' => array(
+        array($this, 'validate'),
+      ),
+      '#submit' => array(
+        array($this, 'submit'),
+        array($this, '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;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    $node = $this->buildEntity($form, $form_state);
+
+    if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) {
+      form_set_error('changed', t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.'));
+    }
+
+    // Validate the "authored by" field.
+    if (!empty($node->name) && !($account = user_load_by_name($node->name))) {
+      // The use of empty() is mandatory in the context of usernames
+      // as the empty string denotes the anonymous user. In case we
+      // are dealing with an anonymous user we set the user ID to 0.
+      form_set_error('name', t('The username %name does not exist.', array('%name' => $node->name)));
+    }
+
+    // Validate the "authored on" field.
+    if (!empty($node->date) && strtotime($node->date) === FALSE) {
+      form_set_error('date', t('You have to specify a valid date.'));
+    }
+
+    // Invoke hook_validate() for node type specific validation and
+    // hook_node_validate() for miscellaneous validation needed by modules.
+    // Can't use node_invoke() or module_invoke_all(), because $form_state must
+    // be receivable by reference.
+    $function = node_type_get_base($node) . '_validate';
+    if (function_exists($function)) {
+      $function($node, $form, $form_state);
+    }
+    foreach (module_implements('node_validate') as $module) {
+      $function = $module . '_node_validate';
+      $function($node, $form, $form_state);
+    }
+
+    parent::validate($form, $form_state);
+  }
+
+  /**
+   * Updates the node object by processing the submitted values.
+   *
+   * This function can be called by a "Next" button of a wizard to update the
+   * form state's entity with the current step's values before proceeding to the
+   * next step.
+   *
+   * Overrides Drupal\entity\EntityFormController::submit().
+   */
+  public function submit(array $form, array &$form_state) {
+    $this->submitNodeLanguage($form, $form_state);
+
+    // Build the node object from the submitted values.
+    $node = parent::submit($form, $form_state);
+
+    node_submit($node);
+    foreach (module_implements('node_submit') as $module) {
+      $function = $module . '_node_submit';
+      $function($node, $form, $form_state);
+    }
+
+    return $node;
+  }
+
+  /**
+   * Handle possible node language changes.
+   */
+  protected function submitNodeLanguage(array $form, array &$form_state) {
+    if (field_has_translation_handler('node', 'node')) {
+      $bundle = $form_state['values']['type'];
+      $node_language = $form_state['values']['langcode'];
+
+      foreach (field_info_instances('node', $bundle) as $instance) {
+        $field_name = $instance['field_name'];
+        $field = field_info_field($field_name);
+        $previous_langcode = $form[$field_name]['#language'];
+
+        // Handle a possible language change: new language values are inserted,
+        // previous ones are deleted.
+        if ($field['translatable'] && $previous_langcode != $node_language) {
+          $form_state['values'][$field_name][$node_language] = $form_state['values'][$field_name][$previous_langcode];
+          $form_state['values'][$field_name][$previous_langcode] = array();
+        }
+      }
+    }
+  }
+
+  /**
+   * Form submission handler for the 'preview' 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 preview(array $form, array &$form_state) {
+    drupal_set_title(t('Preview'), PASS_THROUGH);
+    $form_state['node_preview'] = node_preview($this->getEntity($form_state));
+    $form_state['rebuild'] = TRUE;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $node = $this->getEntity($form_state);
+    $insert = empty($node->nid);
+    $node->save();
+    $node_link = l(t('view'), 'node/' . $node->nid);
+    $watchdog_args = array('@type' => $node->type, '%title' => $node->label());
+    $t_args = array('@type' => node_type_get_name($node), '%title' => $node->label());
+
+    if ($insert) {
+      watchdog('content', '@type: added %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link);
+      drupal_set_message(t('@type %title has been created.', $t_args));
+    }
+    else {
+      watchdog('content', '@type: updated %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link);
+      drupal_set_message(t('@type %title has been updated.', $t_args));
+    }
+
+    if ($node->nid) {
+      $form_state['values']['nid'] = $node->nid;
+      $form_state['nid'] = $node->nid;
+      $form_state['redirect'] = 'node/' . $node->nid;
+    }
+    else {
+      // In the unlikely case something went wrong on save, the node will be
+      // rebuilt and node form redisplayed the same way as in preview.
+      drupal_set_message(t('The post could not be saved.'), 'error');
+      $form_state['rebuild'] = TRUE;
+    }
+
+    // Clear the page and block caches.
+    cache_invalidate(array('content' => TRUE));
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::delete().
+   */
+  public function delete(array $form, array &$form_state) {
+    $destination = array();
+    if (isset($_GET['destination'])) {
+      $destination = drupal_get_destination();
+      unset($_GET['destination']);
+    }
+    $node = $this->getEntity($form_state);
+    $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination));
+  }
+}
diff --git a/core/modules/node/node.api.php b/core/modules/node/node.api.php
index f4fb5f5..e5a8518 100644
--- a/core/modules/node/node.api.php
+++ b/core/modules/node/node.api.php
@@ -636,8 +636,8 @@ function hook_node_access($node, $op, $account, $langcode) {
 /**
  * Act on a node object about to be shown on the add/edit form.
  *
- * This hook is invoked from node_object_prepare() after the type-specific
- * hook_prepare() is invoked.
+ * This hook is invoked from NodeFormController::prepareEntity() after the
+ * type-specific hook_prepare() is invoked.
  *
  * @param Drupal\node\Node $node
  *   The node that is about to be shown on the add/edit form.
@@ -740,10 +740,10 @@ function hook_node_update_index(Drupal\node\Node $node) {
 /**
  * Perform node validation before a node is created or updated.
  *
- * This hook is invoked from node_validate(), after a user has has finished
- * editing the node and is previewing or submitting it. It is invoked at the
- * end of all the standard validation steps, and after the type-specific
- * hook_validate() is invoked.
+ * This hook is invoked from NodeFormController::validate(), after a user has
+ * has finished editing the node and is previewing or submitting it. It is
+ * invoked at the end of all the standard validation steps, and after the
+ * type-specific hook_validate() is invoked.
  *
  * To indicate a validation error, use form_set_error().
  *
@@ -1052,8 +1052,8 @@ function hook_delete(Drupal\node\Node $node) {
  * This hook is invoked only on the module that defines the node's content type
  * (use hook_node_prepare() to act on all node preparations).
  *
- * This hook is invoked from node_object_prepare() before the general
- * hook_node_prepare() is invoked.
+ * This hook is invoked from NodeFormController::prepareEntity() before the
+ * general hook_node_prepare() is invoked.
  *
  * @param Drupal\node\Node $node
  *   The node that is about to be shown on the add/edit form.
@@ -1219,10 +1219,10 @@ function hook_update(Drupal\node\Node $node) {
  * This hook is invoked only on the module that defines the node's content type
  * (use hook_node_validate() to act on all node validations).
  *
- * This hook is invoked from node_validate(), after a user has finished
- * editing the node and is previewing or submitting it. It is invoked at the end
- * of all the standard validation steps, and before hook_node_validate() is
- * invoked.
+ * This hook is invoked from NodeFormController::validate(), after a user has
+ * finished editing the node and is previewing or submitting it. It is invoked
+ * at the end of all the standard validation steps, and before
+ * hook_node_validate() is invoked.
  *
  * To indicate a validation error, use form_set_error().
  *
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index be15dee..c0c5058 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',
@@ -1026,81 +1029,6 @@ function node_load($nid = NULL, $vid = NULL, $reset = FALSE) {
 }
 
 /**
- * Prepares a node entity for editing.
- *
- * Fills in a few default values, and then invokes hook_prepare() on the node
- * type module, and hook_node_prepare() on all modules.
- *
- * @param Drupal\node\Node $node
- *   The node entity.
- */
-function node_object_prepare(Node $node) {
-  // Set up default values, if required.
-  $node_options = variable_get('node_options_' . $node->type, array('status', 'promote'));
-  // If this is a new node, fill in the default values.
-  if (!isset($node->nid) || isset($node->is_new)) {
-    foreach (array('status', 'promote', 'sticky') as $key) {
-      // Multistep node forms might have filled in something already.
-      if (!isset($node->$key)) {
-        $node->$key = (int) in_array($key, $node_options);
-      }
-    }
-    global $user;
-    $node->uid = $user->uid;
-    $node->created = REQUEST_TIME;
-  }
-  else {
-    $node->date = format_date($node->created, 'custom', 'Y-m-d H:i:s O');
-    // Remove the log message from the original node entity.
-    $node->log = NULL;
-  }
-  // Always use the default revision setting.
-  $node->revision = in_array('revision', $node_options);
-
-  node_invoke($node, 'prepare');
-  module_invoke_all('node_prepare', $node);
-}
-
-/**
- * Performs validation checks on the given node.
- *
- * @see node_form_validate()
- */
-function node_validate(Node $node, $form, &$form_state) {
-  $type = node_type_load($node->type);
-
-  if (isset($node->nid) && (node_last_changed($node->nid) > $node->changed)) {
-    form_set_error('changed', t('The content on this page has either been modified by another user, or you have already submitted modifications using this form. As a result, your changes cannot be saved.'));
-  }
-
-  // Validate the "authored by" field.
-  if (!empty($node->name) && !($account = user_load_by_name($node->name))) {
-    // The use of empty() is mandatory in the context of usernames
-    // as the empty string denotes the anonymous user. In case we
-    // are dealing with an anonymous user we set the user ID to 0.
-    form_set_error('name', t('The username %name does not exist.', array('%name' => $node->name)));
-  }
-
-  // Validate the "authored on" field.
-  if (!empty($node->date) && strtotime($node->date) === FALSE) {
-    form_set_error('date', t('You have to specify a valid date.'));
-  }
-
-  // Invoke hook_validate() for node type specific validation and
-  // hook_node_validate() for miscellaneous validation needed by modules. Can't
-  // use node_invoke() or module_invoke_all(), because $form_state must be
-  // receivable by reference.
-  $function = node_type_get_base($node) . '_validate';
-  if (function_exists($function)) {
-    $function($node, $form, $form_state);
-  }
-  foreach (module_implements('node_validate') as $module) {
-    $function = $module . '_node_validate';
-    $function($node, $form, $form_state);
-  }
-}
-
-/**
  * Prepares a node for saving by populating the author and creation date.
  */
 function node_submit($node) {
@@ -3654,21 +3582,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 76fd8b2..19b0cb7 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->label())), PASS_THROUGH);
-  return drupal_get_form($node->type . '_node_form', $node);
+  return entity_get_form($node);
 }
 
 /**
@@ -96,311 +98,12 @@ function node_add($node_type) {
     'langcode' => node_type_get_default_langcode($type)
   ));
   drupal_set_title(t('Create @name', array('@name' => $node_type->name)), PASS_THROUGH);
-  $output = drupal_get_form($type . '_node_form', $node);
+  $output = entity_get_form($node);
 
   return $output;
 }
 
 /**
- * Form validation handler for node_form().
- *
- * @see node_form_delete_submit()
- * @see node_form_build_preview()
- * @see node_form_submit()
- * @see node_form_submit_build_node()
- */
-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'];
-  foreach ($form_state['values'] as $key => $value) {
-    $node->{$key} = $value;
-  }
-  node_validate($node, $form, $form_state);
-  entity_form_field_validate('node', $form, $form_state);
-}
-
-/**
- * Form constructor for the node add/edit form.
- *
- * @see node_form_delete_submit()
- * @see node_form_build_preview()
- * @see node_form_validate()
- * @see node_form_submit()
- * @see node_form_submit_build_node()
- * @ingroup forms
- */
-function node_form($form, &$form_state, Node $node) {
-  global $user;
-
-  // During initial form build, add the node 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['node'])) {
-    node_object_prepare($node);
-    $form_state['node'] = $node;
-  }
-  else {
-    $node = $form_state['node'];
-  }
-
-  // Some special stuff when previewing a node.
-  if (isset($form_state['node_preview'])) {
-    $form['#prefix'] = $form_state['node_preview'];
-    $node->in_preview = TRUE;
-  }
-  else {
-    unset($node->in_preview);
-  }
-
-  // Override the default CSS class name, since the user-defined node type name
-  // in 'TYPE-node-form' potentially clashes with third-party class names.
-  $form['#attributes']['class'][0] = drupal_html_class('node-' . $node->type . '-form');
-
-  // Basic node information.
-  // These elements are just values so they are not even sent to the client.
-  foreach (array('nid', 'vid', 'uid', 'created', 'type') as $key) {
-    $form[$key] = array(
-      '#type' => 'value',
-      '#value' => isset($node->$key) ? $node->$key : NULL,
-    );
-  }
-
-  // Changed must be sent to the client, for later overwrite error checking.
-  $form['changed'] = array(
-    '#type' => 'hidden',
-    '#default_value' => isset($node->changed) ? $node->changed : NULL,
-  );
-  // Invoke hook_form() to get the node-specific bits. Can't use node_invoke(),
-  // because hook_form() needs to be able to receive $form_state by reference.
-  // @todo hook_form() implementations are unable to add #validate or #submit
-  //   handlers to the form buttons below. Remove hook_form() entirely.
-  $function = node_type_get_base($node) . '_form';
-  if (function_exists($function) && ($extra = $function($node, $form_state))) {
-    $form = array_merge_recursive($form, $extra);
-  }
-  // If the node type has a title, and the node type form defined no special
-  // weight for it, we default to a weight of -5 for consistency.
-  if (isset($form['title']) && !isset($form['title']['#weight'])) {
-    $form['title']['#weight'] = -5;
-  }
-  // @todo D8: Remove. Modules should access the node using $form_state['node'].
-  $form['#node'] = $node;
-
-  if (module_exists('language')) {
-    $languages = language_list(LANGUAGE_ALL);
-    $language_options = array();
-    foreach ($languages as $langcode => $language) {
-      // Make locked languages appear special in the list.
-      $language_options[$langcode] = $language->locked ? t('- @name -', array('@name' => $language->name)) : $language->name;
-    }
-    $form['langcode'] = array(
-      '#type' => 'select',
-      '#title' => t('Language'),
-      '#default_value' => $node->langcode,
-      '#options' => $language_options,
-      '#access' => !variable_get('node_type_language_hidden_' . $node->type, TRUE),
-    );
-  }
-  else {
-    $form['langcode'] = array(
-      '#type' => 'value',
-      '#value' => $node->langcode,
-    );
-  }
-
-  $form['additional_settings'] = array(
-    '#type' => 'vertical_tabs',
-    '#weight' => 99,
-  );
-
-  // Add a log field if the "Create new revision" option is checked, or if the
-  // current user has the ability to check that option.
-  $form['revision_information'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Revision information'),
-    '#collapsible' => TRUE,
-    // Collapsed by default when "Create new revision" is unchecked
-    '#collapsed' => !$node->revision,
-    '#group' => 'additional_settings',
-    '#attributes' => array(
-      'class' => array('node-form-revision-information'),
-    ),
-    '#attached' => array(
-      'js' => array(drupal_get_path('module', 'node') . '/node.js'),
-    ),
-    '#weight' => 20,
-    '#access' => $node->revision || user_access('administer nodes'),
-  );
-  $form['revision_information']['revision'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Create new revision'),
-    '#default_value' => $node->revision,
-    '#access' => user_access('administer nodes'),
-  );
-  // Check the revision log checkbox when the log textarea is filled in.
-  // This must not happen if "Create new revision" is enabled by default, since
-  // the state would auto-disable the checkbox otherwise.
-  if (!$node->revision) {
-    $form['revision_information']['revision']['#states'] = array(
-      'checked' => array(
-        'textarea[name="log"]' => array('empty' => FALSE),
-      ),
-    );
-  }
-  $form['revision_information']['log'] = array(
-    '#type' => 'textarea',
-    '#title' => t('Revision log message'),
-    '#rows' => 4,
-    '#default_value' => !empty($node->log) ? $node->log : '',
-    '#description' => t('Briefly describe the changes you have made.'),
-  );
-
-  // Node author information for administrators
-  $form['author'] = array(
-    '#type' => 'fieldset',
-    '#access' => user_access('administer nodes'),
-    '#title' => t('Authoring information'),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-    '#group' => 'additional_settings',
-    '#attributes' => array(
-      'class' => array('node-form-author'),
-    ),
-    '#attached' => array(
-      'js' => array(
-        drupal_get_path('module', 'node') . '/node.js',
-        array(
-          'type' => 'setting',
-          'data' => array('anonymous' => variable_get('anonymous', t('Anonymous'))),
-        ),
-      ),
-    ),
-    '#weight' => 90,
-  );
-  $form['author']['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Authored by'),
-    '#maxlength' => 60,
-    '#autocomplete_path' => 'user/autocomplete',
-    '#default_value' => !empty($node->name) ? $node->name : '',
-    '#weight' => -1,
-    '#description' => t('Leave blank for %anonymous.', array('%anonymous' => variable_get('anonymous', t('Anonymous')))),
-  );
-  $form['author']['date'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Authored on'),
-    '#maxlength' => 25,
-    '#description' => t('Format: %time. The date format is YYYY-MM-DD and %timezone is the time zone offset from UTC. Leave blank to use the time of form submission.', array('%time' => !empty($node->date) ? date_format(date_create($node->date), 'Y-m-d H:i:s O') : format_date($node->created, 'custom', 'Y-m-d H:i:s O'), '%timezone' => !empty($node->date) ? date_format(date_create($node->date), 'O') : format_date($node->created, 'custom', 'O'))),
-    '#default_value' => !empty($node->date) ? $node->date : '',
-  );
-
-  // Node options for administrators
-  $form['options'] = array(
-    '#type' => 'fieldset',
-    '#access' => user_access('administer nodes'),
-    '#title' => t('Publishing options'),
-    '#collapsible' => TRUE,
-    '#collapsed' => TRUE,
-    '#group' => 'additional_settings',
-    '#attributes' => array(
-      'class' => array('node-form-options'),
-    ),
-    '#attached' => array(
-      'js' => array(drupal_get_path('module', 'node') . '/node.js'),
-    ),
-    '#weight' => 95,
-  );
-  $form['options']['status'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Published'),
-    '#default_value' => $node->status,
-  );
-  $form['options']['promote'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Promoted to front page'),
-    '#default_value' => $node->promote,
-  );
-  $form['options']['sticky'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Sticky at top of lists'),
-    '#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';
-  }
-  $form += array('#submit' => array());
-
-  field_attach_form('node', $node, $form, $form_state, $node->langcode);
-  return $form;
-}
-
-/**
- * Form submission handler for the 'Delete' button for node_form().
- *
- * @see node_form_build_preview()
- * @see node_form_validate()
- * @see node_form_submit()
- * @see node_form_submit_build_node()
- */
-function node_form_delete_submit($form, &$form_state) {
-  $destination = array();
-  if (isset($_GET['destination'])) {
-    $destination = drupal_get_destination();
-    unset($_GET['destination']);
-  }
-  $node = $form['#node'];
-  $form_state['redirect'] = array('node/' . $node->nid . '/delete', array('query' => $destination));
-}
-
-/**
- * Form submission handler for the 'Preview' button for node_form().
- *
- * @see node_form_delete_submit()
- * @see node_form_validate()
- * @see node_form_submit()
- * @see node_form_submit_build_node()
- */
-function node_form_build_preview($form, &$form_state) {
-  $node = node_form_submit_build_node($form, $form_state);
-  $form_state['node_preview'] = node_preview($node);
-  $form_state['rebuild'] = TRUE;
-}
-
-/**
  * Generates a node preview.
  *
  * @param Drupal\node\Node $node
@@ -442,7 +145,6 @@ function node_preview(Node $node) {
       $output = theme('node_preview', array('node' => $node));
       unset($node->in_preview);
     }
-    drupal_set_title(t('Preview'), PASS_THROUGH);
 
     return $output;
   }
@@ -455,7 +157,7 @@ function node_preview(Node $node) {
  *   An associative array containing:
  *   - node: The node entity which is being previewed.
  *
- * @see node_preview()
+ * @see NodeFormController::preview()
  * @ingroup themeable
  */
 function theme_node_preview($variables) {
@@ -486,115 +188,11 @@ function theme_node_preview($variables) {
 }
 
 /**
- * Form submission handler that saves the node for node_form().
- *
- * @see node_form_delete_submit()
- * @see node_form_build_preview()
- * @see node_form_validate()
- * @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();
-  $node_link = l(t('view'), 'node/' . $node->nid);
-  $watchdog_args = array('@type' => $node->type, '%title' => $node->label());
-  $t_args = array('@type' => node_type_get_name($node), '%title' => $node->label());
-
-  if ($insert) {
-    watchdog('content', '@type: added %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link);
-    drupal_set_message(t('@type %title has been created.', $t_args));
-  }
-  else {
-    watchdog('content', '@type: updated %title.', $watchdog_args, WATCHDOG_NOTICE, $node_link);
-    drupal_set_message(t('@type %title has been updated.', $t_args));
-  }
-  if ($node->nid) {
-    $form_state['values']['nid'] = $node->nid;
-    $form_state['nid'] = $node->nid;
-    $form_state['redirect'] = 'node/' . $node->nid;
-  }
-  else {
-    // In the unlikely case something went wrong on save, the node will be
-    // rebuilt and node form redisplayed the same way as in preview.
-    drupal_set_message(t('The post could not be saved.'), 'error');
-    $form_state['rebuild'] = TRUE;
-  }
-  // Clear the page and block caches.
-  cache_invalidate(array('content' => TRUE));
-}
-
-/**
- * Handles possible node language changes.
- *
- */
-function node_field_language_form_submit($form, &$form_state) {
-  if (field_has_translation_handler('node', 'node')) {
-    $bundle = $form_state['values']['type'];
-    $node_language = $form_state['values']['langcode'];
-
-    foreach (field_info_instances('node', $bundle) as $instance) {
-      $field_name = $instance['field_name'];
-      $field = field_info_field($field_name);
-      $previous_langcode = $form[$field_name]['#language'];
-
-      // Handle a possible language change: New language values are inserted,
-      // previous ones are deleted.
-      if ($field['translatable'] && $previous_langcode != $node_language) {
-        $form_state['values'][$field_name][$node_language] = $form_state['values'][$field_name][$previous_langcode];
-        $form_state['values'][$field_name][$previous_langcode] = array();
-      }
-    }
-  }
-}
-
-/**
- * Updates the form state's node entity by processing this submission's values.
- *
- * This is the default builder function for the node form. It is called
- * during the "Save" and "Preview" submit handlers to retrieve the entity to
- * save or preview. This function can also be called by a "Next" button of a
- * wizard to update the form state's entity with the current step's values
- * before proceeding to the next step.
- *
- * @see node_form()
- * @see node_form_delete_submit()
- * @see node_form_build_preview()
- * @see node_form_validate()
- * @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_submit($node);
-  foreach (module_implements('node_submit') as $module) {
-    $function = $module . '_node_submit';
-    $function($node, $form, $form_state);
-  }
-  return $node;
-}
-
-/**
  * Page callback: Form constructor for node deletion confirmation form.
  *
  * @see node_menu()
  */
 function node_delete_confirm($form, &$form_state, $node) {
-  $form['#node'] = $node;
   // Always provide entity id in the same form key as in the entity edit form.
   $form['nid'] = array('#type' => 'value', '#value' => $node->nid);
   return confirm_form($form,
diff --git a/core/modules/node/tests/modules/node_access_test/node_access_test.module b/core/modules/node/tests/modules/node_access_test/node_access_test.module
index 5d557cc..af02c79 100644
--- a/core/modules/node/tests/modules/node_access_test/node_access_test.module
+++ b/core/modules/node/tests/modules/node_access_test/node_access_test.module
@@ -7,8 +7,10 @@
  * a special 'node test view' permission.
  */
 
-use Drupal\node\Node;
 use Drupal\entity\EntityFieldQuery;
+use Drupal\entity\EntityFormController;
+
+use Drupal\node\Node;
 
 /**
  * Implements hook_node_grants().
@@ -179,11 +181,12 @@ function node_access_entity_test_page() {
 function node_access_test_form_node_form_alter(&$form, $form_state) {
   // Only show this checkbox for NodeAccessBaseTableTestCase.
   if (variable_get('node_access_test_private')) {
+    $node = $form_state['controller']->getEntity($form_state);
     $form['private'] = array(
       '#type' => 'checkbox',
       '#title' => t('Private'),
       '#description' => t('Check here if this content should be set private and only shown to privileged users.'),
-      '#default_value' => isset($form['#node']->private) ? $form['#node']->private : FALSE,
+      '#default_value' => isset($node->private) ? $node->private : FALSE,
     );
   }
 }
diff --git a/core/modules/openid/openid.module b/core/modules/openid/openid.module
index 034c7c2..bad9f3a 100644
--- a/core/modules/openid/openid.module
+++ b/core/modules/openid/openid.module
@@ -5,6 +5,8 @@
  * Implement OpenID Relying Party support for Drupal
  */
 
+use Drupal\entity\EntityFormController;
+
 /**
  * Implements hook_menu().
  */
@@ -231,7 +233,8 @@ function openid_form_user_register_form_alter(&$form, &$form_state) {
       $timezone = current($ax_timezone_values);
     }
     if (in_array($timezone, timezone_identifiers_list())) {
-      $form['#user']->timezone = $timezone;
+      $account = $form_state['controller']->getEntity($form_state);
+      $account->timezone = $timezone;
     }
 
     $language = FALSE;
@@ -732,7 +735,8 @@ function openid_authentication($response) {
 
     $form_state['values'] = array();
     $form_state['values']['op'] = t('Create new account');
-    drupal_form_submit('user_register_form', $form_state);
+    $account = entity_create('user', array());
+    entity_form_submit($account, 'register', $form_state);
 
     if (empty($form_state['user'])) {
       module_invoke_all('openid_response', $response, NULL);
diff --git a/core/modules/overlay/overlay.module b/core/modules/overlay/overlay.module
index acfcc2c..bad2065 100644
--- a/core/modules/overlay/overlay.module
+++ b/core/modules/overlay/overlay.module
@@ -5,6 +5,8 @@
  * Displays the Drupal administration interface in an overlay.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
 
@@ -85,7 +87,7 @@ function overlay_theme() {
  * Implements hook_form_FORM_ID_alter().
  */
 function overlay_form_user_profile_form_alter(&$form, &$form_state) {
-  $account = $form['#user'];
+  $account = $form_state['controller']->getEntity($form_state);
   if (user_access('access overlay', $account)) {
     $form['overlay_control'] = array(
       '#type' => 'fieldset',
diff --git a/core/modules/path/path.module b/core/modules/path/path.module
index d01c623..e5ca124 100644
--- a/core/modules/path/path.module
+++ b/core/modules/path/path.module
@@ -5,7 +5,10 @@
  * Enables users to rename URLs.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
+
 use Drupal\taxonomy\Term;
 
 /**
@@ -99,11 +102,12 @@ function path_menu() {
  * @see path_form_element_validate()
  */
 function path_form_node_form_alter(&$form, $form_state) {
+  $node = $form_state['controller']->getEntity($form_state);
   $path = array();
-  if (!empty($form['#node']->nid)) {
-    $conditions = array('source' => 'node/' . $form['#node']->nid);
-    if ($form['#node']->langcode != LANGUAGE_NOT_SPECIFIED) {
-      $conditions['langcode'] = $form['#node']->langcode;
+  if (!empty($node->nid)) {
+    $conditions = array('source' => 'node/' . $node->nid);
+    if ($node->langcode != LANGUAGE_NOT_SPECIFIED) {
+      $conditions['langcode'] = $node->langcode;
     }
     $path = path_load($conditions);
     if ($path === FALSE) {
@@ -112,9 +116,9 @@ function path_form_node_form_alter(&$form, $form_state) {
   }
   $path += array(
     'pid' => NULL,
-    'source' => isset($form['#node']->nid) ? 'node/' . $form['#node']->nid : NULL,
+    'source' => isset($node->nid) ? 'node/' . $node->nid : NULL,
     'alias' => '',
-    'langcode' => isset($form['#node']->langcode) ? $form['#node']->langcode : LANGUAGE_NOT_SPECIFIED,
+    'langcode' => isset($node->langcode) ? $node->langcode : LANGUAGE_NOT_SPECIFIED,
   );
 
   $form['path'] = array(
@@ -232,18 +236,19 @@ 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());
+    $term = $form_state['controller']->getEntity($form_state);
+    $path = (isset($term->tid) ? path_load('taxonomy/term/' . $term->tid) : array());
     if ($path === FALSE) {
       $path = array();
     }
     $path += array(
       'pid' => NULL,
-      'source' => isset($form['#term']['tid']) ? 'taxonomy/term/' . $form['#term']['tid'] : NULL,
+      'source' => isset($term->tid) ? 'taxonomy/term/' . $term->tid : NULL,
       'alias' => '',
       'langcode' => LANGUAGE_NOT_SPECIFIED,
     );
diff --git a/core/modules/poll/poll.module b/core/modules/poll/poll.module
index 259ff14..38800e5 100644
--- a/core/modules/poll/poll.module
+++ b/core/modules/poll/poll.module
@@ -6,6 +6,8 @@
  * choice questions.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
 
 /**
@@ -336,6 +338,8 @@ function poll_form(Node $node, &$form_state) {
     drupal_get_path('module', 'poll') . '/poll.admin.css',
   );
 
+  $form['#entity_builders'][] = 'poll_node_form_submit';
+
   return $form;
 }
 
@@ -355,9 +359,10 @@ function poll_more_choices_submit($form, &$form_state) {
   }
   // Renumber the choices. This invalidates the corresponding key/value
   // associations in $form_state['input'], so clear that out. This requires
-  // poll_form() to rebuild the choices with the values in
-  // $form_state['node']->choice, which it does.
-  $form_state['node']->choice = array_values($form_state['values']['choice']);
+  // poll_form() to rebuild the choices with the values in $node->choice, which
+  // it does.
+  $node = $form_state['controller']->getEntity($form_state);
+  $node->choice = array_values($form_state['values']['choice']);
   unset($form_state['input']['choice']);
   $form_state['rebuild'] = TRUE;
 }
@@ -419,14 +424,14 @@ function poll_choice_js($form, $form_state) {
 }
 
 /**
- * Form submit handler for node_form().
+ * Entity builder for node_form().
  *
  * Upon preview and final submission, we need to renumber poll choices and
  * create a teaser output.
  */
-function poll_node_form_submit(&$form, &$form_state) {
+function poll_node_form_submit($entity_type, $entity, &$form, &$form_state) {
   // Renumber choices.
-  $form_state['values']['choice'] = array_values($form_state['values']['choice']);
+  $entity->choice = array_values($form_state['values']['choice']);
   $form_state['values']['teaser'] = poll_teaser((object) $form_state['values']);
 }
 
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index 142a366..c057156 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -1184,7 +1184,8 @@ function hook_page_alter(&$page) {
  * Perform alterations before a form is rendered.
  *
  * One popular use of this hook is to add form elements to the node form. When
- * altering a node form, the node entity can be accessed at $form['#node'].
+ * altering a node form, the node entity can be retrieved by invoking
+ * $form_state['controller']->getEntity($form_state).
  *
  * In addition to hook_form_alter(), which is called for all forms, there are
  * two more specific form hooks available. The first,
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 983c317..8371a10 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -5,7 +5,10 @@
  * Configuration system that lets administrators modify the workings of the site.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\Core\Utility\ModuleInfo;
+
 use Symfony\Component\HttpFoundation\JsonResponse;
 use Symfony\Component\HttpFoundation\Response;
 
@@ -2254,7 +2257,7 @@ function system_user_login(&$edit, $account) {
 function system_user_timezone(&$form, &$form_state) {
   global $user;
 
-  $account = $form['#user'];
+  $account = $form_state['controller']->getEntity($form_state);
   $form['timezone'] = array(
     '#type' => 'fieldset',
     '#title' => t('Locale settings'),
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 b89426c..44fc0eb 100644
--- a/core/modules/system/tests/modules/form_test/form_test.module
+++ b/core/modules/system/tests/modules/form_test/form_test.module
@@ -2132,8 +2132,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..b4c750e 100644
--- a/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
+++ b/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
@@ -5,6 +5,8 @@
  * Test module for Taxonomy hooks and functions not used in core.
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\taxonomy\Term;
 
 /**
@@ -57,18 +59,17 @@ function taxonomy_test_taxonomy_term_delete(Term $term) {
 }
 
 /**
- * Implements hook_form_alter().
+ * Implements hook_form_FORM_ID_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) {
+  $term = $form_state['controller']->getEntity($form_state);
+  $antonym = taxonomy_test_get_antonym($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..fa853b3
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermFormController.php
@@ -0,0 +1,203 @@
+<?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 {
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $term) {
+    $vocabulary = taxonomy_vocabulary_load($term->vid);
+
+    $parent = array_keys(taxonomy_term_load_parents($term->tid));
+    $form_state['taxonomy']['parent'] = $parent;
+    $form_state['taxonomy']['vocabulary'] = $vocabulary;
+
+    $form['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Name'),
+      '#default_value' => $term->name,
+      '#maxlength' => 255,
+      '#required' => TRUE,
+      '#weight' => -5,
+    );
+
+    $form['description'] = array(
+      '#type' => 'text_format',
+      '#title' => t('Description'),
+      '#default_value' => $term->description,
+      '#format' => $term->format,
+      '#weight' => 0,
+    );
+
+    $form['vocabulary_machine_name'] = array(
+      '#type' => 'value',
+      '#value' => isset($term->vocabulary_machine_name) ? $term->vocabulary_machine_name : $vocabulary->name,
+    );
+
+    $form['relations'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Relations'),
+      '#collapsible' => TRUE,
+      '#collapsed' => ($vocabulary->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE),
+      '#weight' => 10,
+    );
+
+    // taxonomy_get_tree and taxonomy_term_load_parents may contain large numbers of
+    // items so we check for taxonomy_override_selector before loading the
+    // full vocabulary. Contrib modules can then intercept before
+    // hook_form_alter to provide scalable alternatives.
+    if (!variable_get('taxonomy_override_selector', FALSE)) {
+      $parent = array_keys(taxonomy_term_load_parents($term->tid));
+      $children = taxonomy_get_tree($vocabulary->vid, $term->tid);
+
+      // A term can't be the child of itself, nor of its children.
+      foreach ($children as $child) {
+        $exclude[] = $child->tid;
+      }
+      $exclude[] = $term->tid;
+
+      $tree = taxonomy_get_tree($vocabulary->vid);
+      $options = array('<' . t('root') . '>');
+      if (empty($parent)) {
+        $parent = array(0);
+      }
+      foreach ($tree as $item) {
+        if (!in_array($item->tid, $exclude)) {
+          $options[$item->tid] = str_repeat('-', $item->depth) . $item->name;
+        }
+      }
+
+      $form['relations']['parent'] = array(
+        '#type' => 'select',
+        '#title' => t('Parent terms'),
+        '#options' => $options,
+        '#default_value' => $parent,
+        '#multiple' => TRUE,
+      );
+    }
+
+    $form['relations']['weight'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Weight'),
+      '#size' => 6,
+      '#default_value' => $term->weight,
+      '#description' => t('Terms are displayed in ascending order by weight.'),
+      '#required' => TRUE,
+    );
+
+    $form['vid'] = array(
+      '#type' => 'value',
+      '#value' => $vocabulary->vid,
+    );
+
+    $form['tid'] = array(
+      '#type' => 'value',
+      '#value' => $term->tid,
+    );
+
+    if (empty($term->tid)) {
+      $form_state['redirect'] = current_path();
+    }
+
+    return parent::form($form, $form_state, $term);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::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.'));
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::submit().
+   */
+  public function submit(array $form, array &$form_state) {
+    $term = parent::submit($form, $form_state);
+
+    // Prevent leading and trailing spaces in term names.
+    $term->name = trim($term->name);
+
+    // Convert text_format field into values expected by taxonomy_term_save().
+    $description = $form_state['values']['description'];
+    $term->description = $description['value'];
+    $term->format = $description['format'];
+
+    return $term;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $term = $this->getEntity($form_state);
+
+    $status = taxonomy_term_save($term);
+    switch ($status) {
+      case SAVED_NEW:
+        drupal_set_message(t('Created new term %term.', array('%term' => $term->name)));
+        watchdog('taxonomy', 'Created new term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit'));
+        break;
+      case SAVED_UPDATED:
+        drupal_set_message(t('Updated term %term.', array('%term' => $term->name)));
+        watchdog('taxonomy', 'Updated term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit'));
+        // Clear the page and block caches to avoid stale data.
+        cache_invalidate(array('content' => TRUE));
+        break;
+    }
+
+    $current_parent_count = count($form_state['values']['parent']);
+    $previous_parent_count = count($form_state['taxonomy']['parent']);
+    // Root doesn't count if it's the only parent.
+    if ($current_parent_count == 1 && isset($form_state['values']['parent'][0])) {
+      $current_parent_count = 0;
+      $form_state['values']['parent'] = array();
+    }
+
+    // If the number of parents has been reduced to one or none, do a check on the
+    // parents of every term in the vocabulary value.
+    if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) {
+      taxonomy_check_vocabulary_hierarchy($form_state['taxonomy']['vocabulary'], $form_state['values']);
+    }
+    // If we've increased the number of parents and this is a single or flat
+    // hierarchy, update the vocabulary immediately.
+    elseif ($current_parent_count > $previous_parent_count && $form_state['taxonomy']['vocabulary']->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE) {
+      $form_state['taxonomy']['vocabulary']->hierarchy = $current_parent_count == 1 ? TAXONOMY_HIERARCHY_SINGLE : TAXONOMY_HIERARCHY_MULTIPLE;
+      taxonomy_vocabulary_save($form_state['taxonomy']['vocabulary']);
+    }
+
+    $form_state['values']['tid'] = $term->tid;
+    $form_state['tid'] = $term->tid;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::delete().
+   */
+  public function delete(array $form, array &$form_state) {
+    $destination = array();
+    if (isset($_GET['destination'])) {
+      $destination = drupal_get_destination();
+      unset($_GET['destination']);
+    }
+    $term = $this->getEntity($form_state);
+    $form_state['redirect'] = array('taxonomy/term/' . $term->tid . '/delete', array('query' => $destination));
+  }
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
index d041d16..1f34e66 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.
+    // TermFormController::save() 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..1a1f882
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/VocabularyFormController.php
@@ -0,0 +1,139 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\taxonomy\VocabularyFormController.
+ */
+
+namespace Drupal\taxonomy;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Base form controller for vocabulary edit forms.
+ */
+class VocabularyFormController extends EntityFormController {
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $vocabulary) {
+
+    // Check whether we need a deletion confirmation form.
+    if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) {
+      return taxonomy_vocabulary_confirm_delete($form, $form_state, $form_state['values']['vid']);
+    }
+    $form['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Name'),
+      '#default_value' => $vocabulary->name,
+      '#maxlength' => 255,
+      '#required' => TRUE,
+    );
+    $form['machine_name'] = array(
+      '#type' => 'machine_name',
+      '#default_value' => $vocabulary->machine_name,
+      '#maxlength' => 255,
+      '#machine_name' => array(
+        'exists' => 'taxonomy_vocabulary_machine_name_load',
+      ),
+    );
+    $form['description'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Description'),
+      '#default_value' => $vocabulary->description,
+    );
+    // Set the hierarchy to "multiple parents" by default. This simplifies the
+    // vocabulary form and standardizes the term form.
+    $form['hierarchy'] = array(
+      '#type' => 'value',
+      '#value' => '0',
+    );
+
+    if (isset($vocabulary->vid)) {
+      $form['vid'] = array('#type' => 'value', '#value' => $vocabulary->vid);
+    }
+
+    return parent::form($form, $form_state, $vocabulary);
+  }
+
+  /**
+   * Returns an array of supported actions for the current entity form.
+   */
+  protected function actions(array $form, array &$form_state) {
+    // If we are displaying the delete confirmation skip the regular actions.
+    if (empty($form_state['confirm_delete'])) {
+      $actions = parent::actions($form, $form_state);
+      array_unshift($actions['delete']['#submit'], array($this, 'submit'));
+      return $actions;
+    }
+    else {
+      return array();
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::validate().
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+
+    // Make sure that the machine name of the vocabulary is not in the
+    // disallowed list (names that conflict with menu items, such as 'list'
+    // and 'add').
+    // During the deletion there is no 'machine_name' key.
+    if (isset($form_state['values']['machine_name'])) {
+      // Do not allow machine names to conflict with taxonomy path arguments.
+      $machine_name = $form_state['values']['machine_name'];
+      $disallowed = array('add', 'list');
+      if (in_array($machine_name, $disallowed)) {
+        form_set_error('machine_name', t('The machine-readable name cannot be "add" or "list".'));
+      }
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::submit().
+   */
+  public function submit(array $form, array &$form_state) {
+    // @todo We should not be calling taxonomy_vocabulary_confirm_delete() from
+    // within the form builder.
+    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 NULL;
+    }
+    else {
+      return parent::submit($form, $form_state);
+    }
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $vocabulary = $this->getEntity($form_state);
+
+    // Prevent leading and trailing spaces in vocabulary names.
+    $vocabulary->name = trim($vocabulary->name);
+
+    switch (taxonomy_vocabulary_save($vocabulary)) {
+      case SAVED_NEW:
+        drupal_set_message(t('Created new vocabulary %name.', array('%name' => $vocabulary->name)));
+        watchdog('taxonomy', 'Created new vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit'));
+        $form_state['redirect'] = 'admin/structure/taxonomy/' . $vocabulary->machine_name;
+        break;
+
+      case SAVED_UPDATED:
+        drupal_set_message(t('Updated vocabulary %name.', array('%name' => $vocabulary->name)));
+        watchdog('taxonomy', 'Updated vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit'));
+        $form_state['redirect'] = 'admin/structure/taxonomy';
+        break;
+    }
+
+    $form_state['values']['vid'] = $vocabulary->vid;
+    $form_state['vid'] = $vocabulary->vid;
+  }
+}
diff --git a/core/modules/taxonomy/taxonomy.admin.inc b/core/modules/taxonomy/taxonomy.admin.inc
index be020a6..4624a01 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.
@@ -102,140 +103,16 @@ function theme_taxonomy_overview_vocabularies($variables) {
 }
 
 /**
- * Form builder for the vocabulary editing form.
- *
- * @param Drupal\taxonomy\Vocabulary|null $vocabulary
- *   (optional) The taxonomy vocabulary entity to edit. If NULL or omitted, the
- *   form creates a new vocabulary.
- *
- * @ingroup forms
- * @see taxonomy_form_vocabulary_submit()
- * @see taxonomy_form_vocabulary_validate()
+ * Page callback: provides the vocabulary creation form.
  */
-function taxonomy_form_vocabulary($form, &$form_state, Vocabulary $vocabulary = NULL) {
-  // 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['vocabulary'])) {
-    // Create a new Vocabulary entity for the add form.
-    if (!isset($vocabulary)) {
-      $vocabulary = entity_create('taxonomy_vocabulary', array(
-        // Default the new vocabulary to the site's default language. This is
-        // the most likely default value until we have better flexible settings.
-        // @todo See http://drupal.org/node/258785 and followups.
-        'langcode' => language_default()->langcode,
-      ));
-    }
-    $form_state['vocabulary'] = $vocabulary;
-  }
-  else {
-    $vocabulary = $form_state['vocabulary'];
-  }
-
-  // @todo Legacy support. Modules are encouraged to access the entity using
-  //   $form_state. Remove in Drupal 8.
-  $form['#vocabulary'] = $form_state['vocabulary'];
-
-  // Check whether we need a deletion confirmation form.
-  if (isset($form_state['confirm_delete']) && isset($form_state['values']['vid'])) {
-    return taxonomy_vocabulary_confirm_delete($form, $form_state, $form_state['values']['vid']);
-  }
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Name'),
-    '#default_value' => $vocabulary->name,
-    '#maxlength' => 255,
-    '#required' => TRUE,
-  );
-  $form['machine_name'] = array(
-    '#type' => 'machine_name',
-    '#default_value' => $vocabulary->machine_name,
-    '#maxlength' => 255,
-    '#machine_name' => array(
-      'exists' => 'taxonomy_vocabulary_machine_name_load',
-    ),
-  );
-  $form['description'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Description'),
-    '#default_value' => $vocabulary->description,
-  );
-  // Set the hierarchy to "multiple parents" by default. This simplifies the
-  // vocabulary form and standardizes the term form.
-  $form['hierarchy'] = array(
-    '#type' => 'value',
-    '#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;
-}
-
-/**
- * Form validation handler for taxonomy_form_vocabulary().
- *
- * Makes sure that the machine name of the vocabulary is not in the
- * disallowed list (names that conflict with menu items, such as 'list'
- * and 'add').
- *
- * @see taxonomy_form_vocabulary()
- * @see taxonomy_form_vocabulary_submit()
- */
-function taxonomy_form_vocabulary_validate($form, &$form_state) {
-  // During the deletion there is no 'machine_name' key
-  if (isset($form_state['values']['machine_name'])) {
-    // Do not allow machine names to conflict with taxonomy path arguments.
-    $machine_name = $form_state['values']['machine_name'];
-    $disallowed = array('add', 'list');
-    if (in_array($machine_name, $disallowed)) {
-      form_set_error('machine_name', t('The machine-readable name cannot be "add" or "list".'));
-    }
-  }
-}
-
-/**
- * Form submission handler for taxonomy_form_vocabulary().
- *
- * @see taxonomy_form_vocabulary()
- * @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);
-
-  // Prevent leading and trailing spaces in vocabulary names.
-  $vocabulary->name = trim($vocabulary->name);
-
-  switch (taxonomy_vocabulary_save($vocabulary)) {
-    case SAVED_NEW:
-      drupal_set_message(t('Created new vocabulary %name.', array('%name' => $vocabulary->name)));
-      watchdog('taxonomy', 'Created new vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit'));
-      $form_state['redirect'] = 'admin/structure/taxonomy/' . $vocabulary->machine_name;
-      break;
-
-    case SAVED_UPDATED:
-      drupal_set_message(t('Updated vocabulary %name.', array('%name' => $vocabulary->name)));
-      watchdog('taxonomy', 'Updated vocabulary %name.', array('%name' => $vocabulary->name), WATCHDOG_NOTICE, l(t('edit'), 'admin/structure/taxonomy/' . $vocabulary->machine_name . '/edit'));
-      $form_state['redirect'] = 'admin/structure/taxonomy';
-      break;
-  }
-
-  $form_state['values']['vid'] = $vocabulary->vid;
-  $form_state['vid'] = $vocabulary->vid;
+function taxonomy_vocabulary_add() {
+  $vocabulary = entity_create('taxonomy_vocabulary', array(
+    // Default the new vocabulary to the site's default language. This is the
+    // most likely default value until we have better flexible settings.
+    // @todo See http://drupal.org/node/258785 and followups.
+    'langcode' => language_default()->langcode,
+  ));
+  return entity_get_form($vocabulary);
 }
 
 /**
@@ -259,7 +136,7 @@ function taxonomy_overview_terms($form, &$form_state, Vocabulary $vocabulary) {
     return taxonomy_vocabulary_confirm_reset_alphabetical($form, $form_state, $vocabulary->vid);
   }
 
-  $form['#vocabulary'] = $vocabulary;
+  $form_state['taxonomy']['vocabulary'] = $vocabulary;
   $form['#tree'] = TRUE;
   $form['#parent_fields'] = FALSE;
 
@@ -461,7 +338,7 @@ function taxonomy_overview_terms_submit($form, &$form_state) {
   // Sort term order based on weight.
   uasort($form_state['values'], 'drupal_sort_weight');
 
-  $vocabulary = $form['#vocabulary'];
+  $vocabulary = $form_state['taxonomy']['vocabulary'];
   // Update the current hierarchy type as we go.
   $hierarchy = TAXONOMY_HIERARCHY_DISABLED;
 
@@ -643,247 +520,11 @@ function theme_taxonomy_overview_terms($variables) {
 }
 
 /**
- * Form function for the term edit form.
- *
- * @param Drupal\taxonomy\Term|null $term
- *   (optional) The taxonomy term entity to edit. If NULL or omitted, the form
- *   creates a new term.
- * @param Drupal\taxonomy\Vocabulary|null $vocabulary
- *   (optional) A taxonomy vocabulary entity to create the term in. Required if
- *   the term is omitted.
- *
- * @ingroup forms
- * @see taxonomy_form_term_validate()
- * @see taxonomy_form_term_submit()
- */
-function taxonomy_form_term($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.
-  if (!isset($form_state['term'])) {
-    // Create a new Term entity for the add form.
-    if (!isset($term)) {
-      $term = entity_create('taxonomy_term', array(
-        'vid' => $vocabulary->vid,
-        'vocabulary_machine_name' => $vocabulary->machine_name,
-        // Default the new vocabulary to the site's default language. This is
-        // the most likely default value until we have better flexible settings.
-        // @todo See http://drupal.org/node/258785 and followups.
-        'langcode' => language_default()->langcode,
-      ));
-    }
-    if (!isset($vocabulary) && isset($term->vid)) {
-      $vocabulary = taxonomy_vocabulary_load($term->vid);
-    }
-    $form_state['term'] = $term;
-  }
-  else {
-    $term = $form_state['term'];
-    if (!isset($vocabulary) && isset($term->vid)) {
-      $vocabulary = taxonomy_vocabulary_load($term->vid);
-    }
-  }
-
-  $parent = array_keys(taxonomy_term_load_parents($term->tid));
-  $form['#term'] = (array) $term;
-  $form['#term']['parent'] = $parent;
-  $form['#vocabulary'] = $vocabulary;
-
-  $form['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Name'),
-    '#default_value' => $term->name,
-    '#maxlength' => 255,
-    '#required' => TRUE,
-    '#weight' => -5,
-  );
-  $form['description'] = array(
-    '#type' => 'text_format',
-    '#title' => t('Description'),
-    '#default_value' => $term->description,
-    '#format' => $term->format,
-    '#weight' => 0,
-  );
-
-  $form['vocabulary_machine_name'] = array(
-    '#type' => 'value',
-    '#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'),
-    '#collapsible' => TRUE,
-    '#collapsed' => ($vocabulary->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE),
-    '#weight' => 10,
-  );
-
-  // taxonomy_get_tree and taxonomy_term_load_parents may contain large numbers of
-  // items so we check for taxonomy_override_selector before loading the
-  // full vocabulary. Contrib modules can then intercept before
-  // hook_form_alter to provide scalable alternatives.
-  if (!variable_get('taxonomy_override_selector', FALSE)) {
-    $parent = array_keys(taxonomy_term_load_parents($term->tid));
-    $children = taxonomy_get_tree($vocabulary->vid, $term->tid);
-
-    // A term can't be the child of itself, nor of its children.
-    foreach ($children as $child) {
-      $exclude[] = $child->tid;
-    }
-    $exclude[] = $term->tid;
-
-    $tree = taxonomy_get_tree($vocabulary->vid);
-    $options = array('<' . t('root') . '>');
-    if (empty($parent)) {
-      $parent = array(0);
-    }
-    foreach ($tree as $item) {
-      if (!in_array($item->tid, $exclude)) {
-        $options[$item->tid] = str_repeat('-', $item->depth) . $item->name;
-      }
-    }
-    $form['relations']['parent'] = array(
-      '#type' => 'select',
-      '#title' => t('Parent terms'),
-      '#options' => $options,
-      '#default_value' => $parent,
-      '#multiple' => TRUE,
-    );
-
-  }
-  $form['relations']['weight'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Weight'),
-    '#size' => 6,
-    '#default_value' => $term->weight,
-    '#description' => t('Terms are displayed in ascending order by weight.'),
-    '#required' => TRUE,
-  );
-  $form['vid'] = array(
-    '#type' => 'value',
-    '#value' => $vocabulary->vid,
-  );
-  $form['tid'] = array(
-    '#type' => 'value',
-    '#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 {
-    $form_state['redirect'] = current_path();
-  }
-
-  return $form;
-}
-
-/**
- * Validation handler for the term form.
- *
- * @see taxonomy_form_term()
+ * Returns a rendered edit form to create a new term associated to the given vocabulary.
  */
-function taxonomy_form_term_validate($form, &$form_state) {
-  entity_form_field_validate('taxonomy_term', $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.'));
-  }
-}
-
-/**
- * Submit handler to insert or update a term.
- *
- * @see taxonomy_form_term()
- */
-function taxonomy_form_term_submit($form, &$form_state) {
-  $term = taxonomy_form_term_submit_build_taxonomy_term($form, $form_state);
-
-  $status = taxonomy_term_save($term);
-  switch ($status) {
-    case SAVED_NEW:
-      drupal_set_message(t('Created new term %term.', array('%term' => $term->name)));
-      watchdog('taxonomy', 'Created new term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit'));
-      break;
-    case SAVED_UPDATED:
-      drupal_set_message(t('Updated term %term.', array('%term' => $term->name)));
-      watchdog('taxonomy', 'Updated term %term.', array('%term' => $term->name), WATCHDOG_NOTICE, l(t('edit'), 'taxonomy/term/' . $term->tid . '/edit'));
-      // Clear the page and block caches to avoid stale data.
-      cache_invalidate(array('content' => TRUE));
-      break;
-  }
-
-  $current_parent_count = count($form_state['values']['parent']);
-  $previous_parent_count = count($form['#term']['parent']);
-  // Root doesn't count if it's the only parent.
-  if ($current_parent_count == 1 && isset($form_state['values']['parent'][0])) {
-    $current_parent_count = 0;
-    $form_state['values']['parent'] = array();
-  }
-
-  // If the number of parents has been reduced to one or none, do a check on the
-  // parents of every term in the vocabulary value.
-  if ($current_parent_count < $previous_parent_count && $current_parent_count < 2) {
-    taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']);
-  }
-  // If we've increased the number of parents and this is a single or flat
-  // hierarchy, update the vocabulary immediately.
-  elseif ($current_parent_count > $previous_parent_count && $form['#vocabulary']->hierarchy != TAXONOMY_HIERARCHY_MULTIPLE) {
-    $form['#vocabulary']->hierarchy = $current_parent_count == 1 ? TAXONOMY_HIERARCHY_SINGLE : TAXONOMY_HIERARCHY_MULTIPLE;
-    taxonomy_vocabulary_save($form['#vocabulary']);
-  }
-
-  $form_state['values']['tid'] = $term->tid;
-  $form_state['tid'] = $term->tid;
-}
-
-/**
- * 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);
-
-  // Prevent leading and trailing spaces in term names.
-  $term->name = trim($term->name);
-
-  // Convert text_format field into values expected by taxonomy_term_save().
-  $description = $form_state['values']['description'];
-  $term->description = $description['value'];
-  $term->format = $description['format'];
-  return $term;
-}
-
-/**
- * Form submission handler for the 'Delete' button for taxonomy_form_term().
- *
- * @see taxonomy_form_term_validate()
- * @see taxonomy_form_term_submit()
- */
-function taxonomy_form_term_delete_submit($form, &$form_state) {
-  $destination = array();
-  if (isset($_GET['destination'])) {
-    $destination = drupal_get_destination();
-    unset($_GET['destination']);
-  }
-  $term = $form['#term'];
-  $form_state['redirect'] = array('taxonomy/term/' . $term['tid'] . '/delete', array('query' => $destination));
+function taxonomy_term_add($vocabulary) {
+  $term = entity_create('taxonomy_term', array('vid' => $vocabulary->vid, 'vocabulary_machine_name' => $vocabulary->machine_name));
+  return entity_get_form($term);
 }
 
 /**
@@ -896,8 +537,7 @@ function taxonomy_term_confirm_delete($form, &$form_state, $term) {
   // Always provide entity id in the same form key as in the entity edit form.
   $form['tid'] = array('#type' => 'value', '#value' => $term->tid);
 
-  $form['#term'] = $term;
-  $form['#vocabulary'] = taxonomy_vocabulary_load($term->vid);;
+  $form_state['taxonomy']['vocabulary'] = taxonomy_vocabulary_load($term->vid);;
   $form['type'] = array('#type' => 'value', '#value' => 'term');
   $form['name'] = array('#type' => 'value', '#value' => $term->name);
   $form['vocabulary_machine_name'] = array('#type' => 'value', '#value' => $term->vocabulary_machine_name);
@@ -918,7 +558,7 @@ function taxonomy_term_confirm_delete($form, &$form_state, $term) {
  */
 function taxonomy_term_confirm_delete_submit($form, &$form_state) {
   taxonomy_term_delete($form_state['values']['tid']);
-  taxonomy_check_vocabulary_hierarchy($form['#vocabulary'], $form_state['values']);
+  taxonomy_check_vocabulary_hierarchy($form_state['taxonomy']['vocabulary'], $form_state['values']);
   drupal_set_message(t('Deleted term %name.', array('%name' => $form_state['values']['name'])));
   watchdog('taxonomy', 'Deleted term %name.', array('%name' => $form_state['values']['name']), WATCHDOG_NOTICE);
   if (!isset($_GET['destination'])) {
@@ -940,7 +580,7 @@ function taxonomy_vocabulary_confirm_delete($form, &$form_state, $vid) {
   // Always provide entity id in the same form key as in the entity edit form.
   $form['vid'] = array('#type' => 'value', '#value' => $vid);
 
-  $form['#vocabulary'] = $vocabulary;
+  $form_state['taxonomy']['vocabulary'] = $vocabulary;
   $form['#id'] = 'taxonomy_vocabulary_confirm_delete';
   $form['type'] = array('#type' => 'value', '#value' => 'vocabulary');
   $form['name'] = array('#type' => 'value', '#value' => $vocabulary->name);
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index a78bb49..e0f2832 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,
@@ -150,6 +153,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',
@@ -295,8 +301,7 @@ 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' => 'taxonomy_vocabulary_add',
     'access arguments' => array('administer taxonomy'),
     'type' => MENU_LOCAL_ACTION,
     'file' => 'taxonomy.admin.inc',
@@ -317,10 +322,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,
@@ -370,8 +375,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,
@@ -380,8 +385,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/translation/translation.module b/core/modules/translation/translation.module
index ba5c406..8eb602e 100644
--- a/core/modules/translation/translation.module
+++ b/core/modules/translation/translation.module
@@ -19,6 +19,8 @@
  *   date (0) or needs to be updated (1).
  */
 
+use Drupal\entity\EntityFormController;
+
 use Drupal\node\Node;
 
 /**
@@ -153,9 +155,8 @@ function translation_node_type_language_translation_enabled_validate($element, &
  * @see node_form()
  */
 function translation_form_node_form_alter(&$form, &$form_state) {
-  if (translation_supported_type($form['#node']->type)) {
-    $node = $form['#node'];
-
+  $node = $form_state['controller']->getEntity($form_state);
+  if (translation_supported_type($node->type)) {
     if (!empty($node->translation_source)) {
       // We are creating a translation. Add values and lock language field.
       $form['translation_source'] = array('#type' => 'value', '#value' => $node->translation_source);
@@ -379,10 +380,11 @@ function translation_node_update(Node $node) {
  *
  * Ensures that duplicate translations can't be created for the same source.
  */
-function translation_node_validate(Node $node, $form) {
+function translation_node_validate(Node $node, $form, &$form_state) {
   // Only act on translatable nodes with a tnid or translation_source.
-  if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form['#node']->translation_source->nid))) {
-    $tnid = !empty($node->tnid) ? $node->tnid : $form['#node']->translation_source->nid;
+  $form_node = $form_state['controller']->getEntity($form_state);
+  if (translation_supported_type($node->type) && (!empty($node->tnid) || !empty($form_node->translation_source->nid))) {
+    $tnid = !empty($node->tnid) ? $node->tnid : $form_node->translation_source->nid;
     $translations = translation_node_get_translations($tnid);
     if (isset($translations[$node->langcode]) && $translations[$node->langcode]->nid != $node->nid) {
       form_set_error('langcode', t('There is already a translation in this language.'));
diff --git a/core/modules/user/lib/Drupal/user/AccountFormController.php b/core/modules/user/lib/Drupal/user/AccountFormController.php
new file mode 100644
index 0000000..31acf0d
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/AccountFormController.php
@@ -0,0 +1,330 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\AccountFormController.
+ */
+
+namespace Drupal\user;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityFormController;
+
+/**
+ * Form controller for the user account forms.
+ */
+abstract class AccountFormController extends EntityFormController {
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $account) {
+    global $user;
+
+    $language_interface = language(LANGUAGE_TYPE_INTERFACE);
+    $register = empty($account->uid);
+    $admin = user_access('administer users');
+
+    // Account information.
+    $form['account'] = array(
+      '#type'   => 'container',
+      '#weight' => -10,
+    );
+
+    // Only show name field on registration form or user can change own username.
+    $form['account']['name'] = array(
+      '#type' => 'textfield',
+      '#title' => t('Username'),
+      '#maxlength' => USERNAME_MAX_LENGTH,
+      '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'),
+      '#required' => TRUE,
+      '#attributes' => array('class' => array('username'), 'autocomplete' => 'off'),
+      '#default_value' => (!$register ? $account->name : ''),
+      '#access' => ($register || ($user->uid == $account->uid && user_access('change own username')) || $admin),
+      '#weight' => -10,
+    );
+
+    // The mail field is NOT required if account originally had no mail set
+    // and the user performing the edit has 'administer users' permission.
+    // This allows users without e-mail address to be edited and deleted.
+    $form['account']['mail'] = array(
+      '#type' => 'email',
+      '#title' => t('E-mail address'),
+      '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
+      '#required' => !(empty($account->mail) && user_access('administer users')),
+      '#default_value' => (!$register ? $account->mail : ''),
+      '#attributes' => array('autocomplete' => 'off'),
+    );
+
+    // Display password field only for existing users or when user is allowed to
+    // assign a password during registration.
+    if (!$register) {
+      $form['account']['pass'] = array(
+        '#type' => 'password_confirm',
+        '#size' => 25,
+        '#description' => t('To change the current user password, enter the new password in both fields.'),
+      );
+
+      // To skip the current password field, the user must have logged in via a
+      // one-time link and have the token in the URL.
+      $pass_reset = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]);
+      $protected_values = array();
+      $current_pass_description = '';
+
+      // The user may only change their own password without their current
+      // password if they logged in via a one-time login link.
+      if (!$pass_reset) {
+        $protected_values['mail'] = $form['account']['mail']['#title'];
+        $protected_values['pass'] = t('Password');
+        $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
+        $current_pass_description = t('Required if you want to change the %mail or %pass below. !request_new.', array('%mail' => $protected_values['mail'], '%pass' => $protected_values['pass'], '!request_new' => $request_new));
+      }
+
+      // The user must enter their current password to change to a new one.
+      if ($user->uid == $account->uid) {
+        $form['account']['current_pass_required_values'] = array(
+          '#type' => 'value',
+          '#value' => $protected_values,
+        );
+
+        $form['account']['current_pass'] = array(
+          '#type' => 'password',
+          '#title' => t('Current password'),
+          '#size' => 25,
+          '#access' => !empty($protected_values),
+          '#description' => $current_pass_description,
+          '#weight' => -5,
+          '#attributes' => array('autocomplete' => 'off'),
+        );
+
+        $form_state['user'] = $account;
+        $form['#validate'][] = 'user_validate_current_pass';
+      }
+    }
+    elseif (!variable_get('user_email_verification', TRUE) || $admin) {
+      $form['account']['pass'] = array(
+        '#type' => 'password_confirm',
+        '#size' => 25,
+        '#description' => t('Provide a password for the new account in both fields.'),
+        '#required' => TRUE,
+      );
+    }
+
+    if ($admin) {
+      $status = isset($account->status) ? $account->status : 1;
+    }
+    else {
+      $status = $register ? variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS : $account->status;
+    }
+
+    $form['account']['status'] = array(
+      '#type' => 'radios',
+      '#title' => t('Status'),
+      '#default_value' => $status,
+      '#options' => array(t('Blocked'), t('Active')),
+      '#access' => $admin,
+    );
+
+    $roles = array_map('check_plain', user_roles(TRUE));
+    // The disabled checkbox subelement for the 'authenticated user' role
+    // must be generated separately and added to the checkboxes element,
+    // because of a limitation in Form API not supporting a single disabled
+    // checkbox within a set of checkboxes.
+    // @todo This should be solved more elegantly. See issue #119038.
+    $checkbox_authenticated = array(
+      '#type' => 'checkbox',
+      '#title' => $roles[DRUPAL_AUTHENTICATED_RID],
+      '#default_value' => TRUE,
+      '#disabled' => TRUE,
+    );
+    unset($roles[DRUPAL_AUTHENTICATED_RID]);
+
+    $form['account']['roles'] = array(
+      '#type' => 'checkboxes',
+      '#title' => t('Roles'),
+      '#default_value' => (!$register && isset($account->roles) ? array_keys($account->roles) : array()),
+      '#options' => $roles,
+      '#access' => $roles && user_access('administer permissions'),
+      DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated,
+    );
+
+    $form['account']['notify'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Notify user of new account'),
+      '#access' => $register && $admin,
+    );
+
+    // Signature.
+    $form['signature_settings'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Signature settings'),
+      '#weight' => 1,
+      '#access' => (!$register && variable_get('user_signatures', 0)),
+    );
+
+    $form['signature_settings']['signature'] = array(
+      '#type' => 'text_format',
+      '#title' => t('Signature'),
+      '#default_value' => isset($account->signature) ? $account->signature : '',
+      '#description' => t('Your signature will be publicly displayed at the end of your comments.'),
+      '#format' => isset($account->signature_format) ? $account->signature_format : NULL,
+    );
+
+    // Picture/avatar.
+    $form['picture'] = array(
+      '#type' => 'fieldset',
+      '#title' => t('Picture'),
+      '#weight' => 1,
+      '#access' => (!$register && variable_get('user_pictures', 0)),
+    );
+
+    $form['picture']['picture'] = array(
+      '#type' => 'value',
+      '#value' => isset($account->picture) ? $account->picture : NULL,
+    );
+
+    $form['picture']['picture_current'] = array(
+      '#markup' => theme('user_picture', array('account' => $account)),
+    );
+
+    $form['picture']['picture_delete'] = array(
+      '#type' => 'checkbox',
+      '#title' => t('Delete picture'),
+      '#access' => !empty($account->picture->fid),
+      '#description' => t('Check this box to delete your current picture.'),
+    );
+
+    $form['picture']['picture_upload'] = array(
+      '#type' => 'file',
+      '#title' => t('Upload picture'),
+      '#size' => 48,
+      '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')),
+    );
+
+    $form['#validate'][] = 'user_validate_picture';
+
+    if (module_exists('language') && language_multilingual()) {
+      $languages = language_list();
+
+      // If the user is being created, we set the user language to the page language.
+      $user_preferred_language = $register ? $language_interface : user_preferred_language($account);
+
+      $names = array();
+      foreach ($languages as $langcode => $item) {
+        $names[$langcode] = $item->name;
+      }
+
+      // Is default the interface language?
+      $interface_language_is_default = language_negotiation_method_get_first(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT;
+      $form['language'] = array(
+        '#type' => 'fieldset',
+        '#title' => t('Language settings'),
+        // Display language selector when either creating a user on the admin
+        // interface or editing a user account.
+        '#access' => !$register || user_access('administer users'),
+      );
+
+      $form['language']['preferred_langcode'] = array(
+        '#type' => (count($names) <= 5 ? 'radios' : 'select'),
+        '#title' => t('Language'),
+        '#default_value' => $user_preferred_language->langcode,
+        '#options' => $names,
+        '#description' => $interface_language_is_default ? t("This account's preferred language for e-mails and site presentation.") : t("This account's preferred language for e-mails."),
+      );
+    }
+    else {
+      $form['language'] = array(
+        '#type' => 'container',
+      );
+
+      $form['language']['preferred_langcode'] = array(
+        '#type' => 'value',
+        '#value' => language_default()->langcode,
+      );
+    }
+
+    // User entities contain both a langcode property (for identifying the
+    // language of the entity data) and a preferred_langcode property (see
+    // above). Rather than provide a UI forcing the user to choose both
+    // separately, assume that the user profile data is in the user's preferred
+    // language. This element provides that synchronization. For use-cases where
+    // this synchronization is not desired, a module can alter or remove this
+    // element.
+    $form['language']['langcode'] = array(
+      '#type' => 'value',
+      '#value_callback' => '_user_language_selector_langcode_value',
+      // For the synchronization to work, this element must have a larger weight
+      // than the preferred_langcode element. Set a large weight here in case
+      // a module alters the weight of the other element.
+      '#weight' => 100,
+    );
+
+    return parent::form($form, $form_state, $account);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::submit().
+   */
+  public function validate(array $form, array &$form_state) {
+    parent::validate($form, $form_state);
+
+    $account = $this->getEntity($form_state);
+    // Validate new or changing username.
+    if (isset($form_state['values']['name'])) {
+      if ($error = user_validate_name($form_state['values']['name'])) {
+        form_set_error('name', $error);
+      }
+      // Cast the user ID as an integer. It might have been set to NULL, which
+      // could lead to unexpected results.
+      else {
+        $name_taken = (bool) db_select('users')
+        ->fields('users', array('uid'))
+        ->condition('uid', (int) $account->uid, '<>')
+        ->condition('name', db_like($form_state['values']['name']), 'LIKE')
+        ->range(0, 1)
+        ->execute()
+        ->fetchField();
+
+        if ($name_taken) {
+          form_set_error('name', t('The name %name is already taken.', array('%name' => $form_state['values']['name'])));
+        }
+      }
+    }
+
+    $mail = $form_state['values']['mail'];
+
+    if (!empty($mail)) {
+      $mail_taken = (bool) db_select('users')
+      ->fields('users', array('uid'))
+      ->condition('uid', (int) $account->uid, '<>')
+      ->condition('mail', db_like($mail), 'LIKE')
+      ->range(0, 1)
+      ->execute()
+      ->fetchField();
+
+      if ($mail_taken) {
+        // Format error message dependent on whether the user is logged in or not.
+        if ($GLOBALS['user']->uid) {
+          form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $mail)));
+        }
+        else {
+          form_set_error('mail', t('The e-mail address %email is already registered. <a href="@password">Have you forgotten your password?</a>', array('%email' => $mail, '@password' => url('user/password'))));
+        }
+      }
+    }
+
+    // Make sure the signature isn't longer than the size of the database field.
+    // Signatures are disabled by default, so make sure it exists first.
+    if (isset($form_state['values']['signature'])) {
+      // Move text format for user signature into 'signature_format'.
+      $form_state['values']['signature_format'] = $form_state['values']['signature']['format'];
+      // Move text value for user signature into 'signature'.
+      $form_state['values']['signature'] = $form_state['values']['signature']['value'];
+
+      $user_schema = drupal_get_schema('users');
+      if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) {
+        form_set_error('signature', t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length'])));
+      }
+    }
+  }
+}
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..d32cf4a
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/ProfileFormController.php
@@ -0,0 +1,62 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\ProfileFormController.
+ */
+
+namespace Drupal\user;
+
+use Drupal\entity\EntityInterface;
+
+/**
+ * Form controller for the profile forms.
+ */
+class ProfileFormController extends AccountFormController {
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    $element = parent::actions($form, $form_state);
+    $account = $this->getEntity($form_state);
+
+    // @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;
+  }
+
+  /**
+   * Overrides 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);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::save().
+   */
+  public function save(array $form, array &$form_state) {
+    $account = $this->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..9c88aec
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/RegisterFormController.php
@@ -0,0 +1,155 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\user\RegisterFormController.
+ */
+
+namespace Drupal\user;
+
+use Drupal\entity\EntityInterface;
+
+/**
+ * Form controller for the user register forms.
+ */
+class RegisterFormController extends AccountFormController {
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::form().
+   */
+  public function form(array $form, array &$form_state, EntityInterface $account) {
+    global $user;
+
+    $admin = user_access('administer users');
+
+    // Pass access information to the submit handler. Running an access check
+    // inside the submit function interferes with form processing and breaks
+    // hook_form_alter().
+    $form['administer_users'] = array(
+      '#type' => 'value',
+      '#value' => $admin,
+    );
+
+    // If we aren't admin but already logged on, go to the user page instead.
+    if (!$admin && $user->uid) {
+      drupal_goto('user/' . $user->uid);
+    }
+
+    $form['#attached']['library'][] = array('system', 'jquery.cookie');
+    $form['#attributes']['class'][] = 'user-info-from-cookie';
+
+    // Start with the default user account fields.
+    $form = parent::form($form, $form_state, $account);
+
+    // Attach field widgets, and hide the ones where the 'user_register_form'
+    // setting is not on.
+    field_attach_form('user', $account, $form, $form_state);
+    foreach (field_info_instances('user', 'user') as $field_name => $instance) {
+      if (empty($instance['settings']['user_register_form'])) {
+        $form[$field_name]['#access'] = FALSE;
+      }
+    }
+
+    if ($admin) {
+      // Redirect back to page which initiated the create request; usually
+      // admin/people/create.
+      $form_state['redirect'] = current_path();
+    }
+
+    return $form;
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::actions().
+   */
+  protected function actions(array $form, array &$form_state) {
+    $element = parent::actions($form, $form_state);
+    $element['submit']['#value'] = t('Create new account');
+    return $element;
+  }
+
+  /**
+   * Overrides 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);
+  }
+
+  /**
+   * Overrides Drupal\entity\EntityFormController::submit().
+   */
+  public function save(array $form, array &$form_state) {
+    $account = $this->getEntity($form_state);
+    $pass = $account->pass;
+    $admin = $form_state['values']['administer_users'];
+    $notify = !empty($form_state['values']['notify']);
+
+    $account->save();
+
+    // Terminate if an error occurred while saving the account.
+    if ($status =! SAVED_NEW) {
+      drupal_set_message(t("Error saving user account."), 'error');
+      $form_state['redirect'] = '';
+      return;
+    }
+    $form_state['user'] = $account;
+    $form_state['values']['uid'] = $account->uid;
+
+    watchdog('user', 'New user: %name (%email).', array('%name' => $form_state['values']['name'], '%email' => $form_state['values']['mail']), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));
+
+    // Add plain text password into user account to generate mail tokens.
+    $account->password = $pass;
+
+    // New administrative account without notification.
+    $uri = entity_uri('user', $account);
+    if ($admin && !$notify) {
+      drupal_set_message(t('Created a new user account for <a href="@url">%name</a>. No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
+    }
+    // No e-mail verification required; log in user immediately.
+    elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) {
+      _user_mail_notify('register_no_approval_required', $account);
+      $form_state['uid'] = $account->uid;
+      user_login_submit(array(), $form_state);
+      drupal_set_message(t('Registration successful. You are now logged in.'));
+      $form_state['redirect'] = '';
+    }
+    // No administrator approval required.
+    elseif ($account->status || $notify) {
+      if (empty($account->mail) && $notify) {
+        drupal_set_message(t('The new user <a href="@url">%name</a> was created without an email address, so no welcome message was sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
+      }
+      else {
+        $op = $notify ? 'register_admin_created' : 'register_no_approval_required';
+        _user_mail_notify($op, $account);
+        if ($notify) {
+          drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user <a href="@url">%name</a>.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
+        }
+        else {
+          drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));
+          $form_state['redirect'] = '';
+        }
+      }
+    }
+    // Administrator approval required.
+    else {
+      _user_mail_notify('register_pending_approval', $account);
+      drupal_set_message(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.<br />In the meantime, a welcome message with further instructions has been sent to your e-mail address.'));
+      $form_state['redirect'] = '';
+    }
+  }
+}
diff --git a/core/modules/user/tests/user_form_test.module b/core/modules/user/tests/user_form_test.module
index 4e907f3..5702c53 100644
--- a/core/modules/user/tests/user_form_test.module
+++ b/core/modules/user/tests/user_form_test.module
@@ -27,7 +27,7 @@ function user_form_test_menu() {
  */
 function user_form_test_current_password($form, &$form_state, $account) {
   $account->user_form_test_field = '';
-  $form['#user'] = $account;
+  $form_state['user'] = $account;
 
   $form['user_form_test_field'] = array(
     '#type' => 'textfield',
@@ -35,7 +35,7 @@ function user_form_test_current_password($form, &$form_state, $account) {
     '#description' => t('A field that would require a correct password to change.'),
     '#required' => TRUE,
   );
-  
+
   $form['current_pass'] = array(
     '#type' => 'password',
     '#title' => t('Current password'),
diff --git a/core/modules/user/user.admin.inc b/core/modules/user/user.admin.inc
index 58b0218..fac9723 100644
--- a/core/modules/user/user.admin.inc
+++ b/core/modules/user/user.admin.inc
@@ -11,7 +11,8 @@ function user_admin($callback_arg = '') {
   switch ($op) {
     case t('Create new account'):
     case 'create':
-      $build['user_register'] = drupal_get_form('user_register_form');
+      $account = entity_create('user', array());
+      $build['user_register'] = entity_get_form($account, '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 1f72aa6..d798e1a 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;
@@ -147,6 +149,10 @@ function user_entity_info() {
     'user' => array(
       'label' => t('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',
@@ -399,7 +405,7 @@ function user_validate_name($name) {
 /**
  * Validates an image uploaded by a user.
  *
- * @see user_account_form()
+ * @see AccountFormController::form()
  */
 function user_validate_picture(&$form, &$form_state) {
   // If required, validate the uploaded picture.
@@ -701,242 +707,6 @@ function user_user_view($account) {
 }
 
 /**
- * Helper function to add default user account fields to user registration and edit form.
- *
- * @see user_account_form_validate()
- * @see user_validate_current_pass()
- * @see user_validate_picture()
- * @see user_validate_mail()
- */
-function user_account_form(&$form, &$form_state) {
-  global $user;
-  $language_interface = language(LANGUAGE_TYPE_INTERFACE);
-
-  $account = $form['#user'];
-  $register = ($form['#user']->uid > 0 ? FALSE : TRUE);
-
-  $admin = user_access('administer users');
-
-  $form['#validate'][] = 'user_account_form_validate';
-
-  // Account information.
-  $form['account'] = array(
-    '#type'   => 'container',
-    '#weight' => -10,
-  );
-  // Only show name field on registration form or user can change own username.
-  $form['account']['name'] = array(
-    '#type' => 'textfield',
-    '#title' => t('Username'),
-    '#maxlength' => USERNAME_MAX_LENGTH,
-    '#description' => t('Spaces are allowed; punctuation is not allowed except for periods, hyphens, apostrophes, and underscores.'),
-    '#required' => TRUE,
-    '#attributes' => array('class' => array('username'), 'autocomplete' => 'off'),
-    '#default_value' => (!$register ? $account->name : ''),
-    '#access' => ($register || ($user->uid == $account->uid && user_access('change own username')) || $admin),
-    '#weight' => -10,
-  );
-
-  // The mail field is NOT required if account originally had no mail set
-  // and the user performing the edit has 'administer users' permission.
-  // This allows users without e-mail address to be edited and deleted.
-  $form['account']['mail'] = array(
-    '#type' => 'email',
-    '#title' => t('E-mail address'),
-    '#description' => t('A valid e-mail address. All e-mails from the system will be sent to this address. The e-mail address is not made public and will only be used if you wish to receive a new password or wish to receive certain news or notifications by e-mail.'),
-    '#required' => !(empty($account->mail) && user_access('administer users')),
-    '#default_value' => (!$register ? $account->mail : ''),
-    '#attributes' => array('autocomplete' => 'off'),
-  );
-
-  // Display password field only for existing users or when user is allowed to
-  // assign a password during registration.
-  if (!$register) {
-    $form['account']['pass'] = array(
-      '#type' => 'password_confirm',
-      '#size' => 25,
-      '#description' => t('To change the current user password, enter the new password in both fields.'),
-    );
-    // To skip the current password field, the user must have logged in via a
-    // one-time link and have the token in the URL.
-    $pass_reset = isset($_SESSION['pass_reset_' . $account->uid]) && isset($_GET['pass-reset-token']) && ($_GET['pass-reset-token'] == $_SESSION['pass_reset_' . $account->uid]);
-    $protected_values = array();
-    $current_pass_description = '';
-    // The user may only change their own password without their current
-    // password if they logged in via a one-time login link.
-    if (!$pass_reset) {
-      $protected_values['mail'] = $form['account']['mail']['#title'];
-      $protected_values['pass'] = t('Password');
-      $request_new = l(t('Request new password'), 'user/password', array('attributes' => array('title' => t('Request new password via e-mail.'))));
-      $current_pass_description = t('Required if you want to change the %mail or %pass below. !request_new.', array('%mail' => $protected_values['mail'], '%pass' => $protected_values['pass'], '!request_new' => $request_new));
-    }
-    // The user must enter their current password to change to a new one.
-    if ($user->uid == $account->uid) {
-      $form['account']['current_pass_required_values'] = array(
-        '#type' => 'value',
-        '#value' => $protected_values,
-      );
-      $form['account']['current_pass'] = array(
-        '#type' => 'password',
-        '#title' => t('Current password'),
-        '#size' => 25,
-        '#access' => !empty($protected_values),
-        '#description' => $current_pass_description,
-        '#weight' => -5,
-        '#attributes' => array('autocomplete' => 'off'),
-      );
-      $form['#validate'][] = 'user_validate_current_pass';
-    }
-  }
-  elseif (!variable_get('user_email_verification', TRUE) || $admin) {
-    $form['account']['pass'] = array(
-      '#type' => 'password_confirm',
-      '#size' => 25,
-      '#description' => t('Provide a password for the new account in both fields.'),
-      '#required' => TRUE,
-    );
-  }
-
-  if ($admin) {
-    $status = isset($account->status) ? $account->status : 1;
-  }
-  else {
-    $status = $register ? variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) == USER_REGISTER_VISITORS : $account->status;
-  }
-  $form['account']['status'] = array(
-    '#type' => 'radios',
-    '#title' => t('Status'),
-    '#default_value' => $status,
-    '#options' => array(t('Blocked'), t('Active')),
-    '#access' => $admin,
-  );
-
-  $roles = array_map('check_plain', user_roles(TRUE));
-  // The disabled checkbox subelement for the 'authenticated user' role
-  // must be generated separately and added to the checkboxes element,
-  // because of a limitation in Form API not supporting a single disabled
-  // checkbox within a set of checkboxes.
-  // @todo This should be solved more elegantly. See issue #119038.
-  $checkbox_authenticated = array(
-    '#type' => 'checkbox',
-    '#title' => $roles[DRUPAL_AUTHENTICATED_RID],
-    '#default_value' => TRUE,
-    '#disabled' => TRUE,
-  );
-  unset($roles[DRUPAL_AUTHENTICATED_RID]);
-  $form['account']['roles'] = array(
-    '#type' => 'checkboxes',
-    '#title' => t('Roles'),
-    '#default_value' => (!$register && isset($account->roles) ? array_keys($account->roles) : array()),
-    '#options' => $roles,
-    '#access' => $roles && user_access('administer permissions'),
-    DRUPAL_AUTHENTICATED_RID => $checkbox_authenticated,
-  );
-
-  $form['account']['notify'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Notify user of new account'),
-    '#access' => $register && $admin,
-  );
-
-  // Signature.
-  $form['signature_settings'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Signature settings'),
-    '#weight' => 1,
-    '#access' => (!$register && variable_get('user_signatures', 0)),
-  );
-
-  $form['signature_settings']['signature'] = array(
-    '#type' => 'text_format',
-    '#title' => t('Signature'),
-    '#default_value' => isset($account->signature) ? $account->signature : '',
-    '#description' => t('Your signature will be publicly displayed at the end of your comments.'),
-    '#format' => isset($account->signature_format) ? $account->signature_format : NULL,
-  );
-
-  // Picture/avatar.
-  $form['picture'] = array(
-    '#type' => 'fieldset',
-    '#title' => t('Picture'),
-    '#weight' => 1,
-    '#access' => (!$register && variable_get('user_pictures', 0)),
-  );
-  $form['picture']['picture'] = array(
-    '#type' => 'value',
-    '#value' => isset($account->picture) ? $account->picture : NULL,
-  );
-  $form['picture']['picture_current'] = array(
-    '#markup' => theme('user_picture', array('account' => $account)),
-  );
-  $form['picture']['picture_delete'] = array(
-    '#type' => 'checkbox',
-    '#title' => t('Delete picture'),
-    '#access' => !empty($account->picture->fid),
-    '#description' => t('Check this box to delete your current picture.'),
-  );
-  $form['picture']['picture_upload'] = array(
-    '#type' => 'file',
-    '#title' => t('Upload picture'),
-    '#size' => 48,
-    '#description' => t('Your virtual face or picture. Pictures larger than @dimensions pixels will be scaled down.', array('@dimensions' => variable_get('user_picture_dimensions', '85x85'))) . ' ' . filter_xss_admin(variable_get('user_picture_guidelines', '')),
-  );
-  $form['#validate'][] = 'user_validate_picture';
-
-  if (module_exists('language') && language_multilingual()) {
-    $languages = language_list();
-
-    // If the user is being created, we set the user language to the page language.
-    $user_preferred_language = $register ? $language_interface : user_preferred_language($account);
-
-    $names = array();
-    foreach ($languages as $langcode => $item) {
-      $names[$langcode] = $item->name;
-    }
-    // Is default the interface language?
-    $interface_language_is_default = language_negotiation_method_get_first(LANGUAGE_TYPE_INTERFACE) != LANGUAGE_NEGOTIATION_DEFAULT;
-    $form['language'] = array(
-      '#type' => 'fieldset',
-      '#title' => t('Language settings'),
-      // Display language selector when either creating a user on the admin
-      // interface or editing a user account.
-      '#access' => !$register || user_access('administer users'),
-    );
-    $form['language']['preferred_langcode'] = array(
-      '#type' => (count($names) <= 5 ? 'radios' : 'select'),
-      '#title' => t('Language'),
-      '#default_value' => $user_preferred_language->langcode,
-      '#options' => $names,
-      '#description' => $interface_language_is_default ? t("This account's preferred language for e-mails and site presentation.") : t("This account's preferred language for e-mails."),
-    );
-  }
-  else {
-    $form['language'] = array(
-      '#type' => 'container',
-    );
-    $form['language']['preferred_langcode'] = array(
-      '#type' => 'value',
-      '#value' => language_default()->langcode,
-    );
-  }
-
-  // User entities contain both a langcode property (for identifying the
-  // language of the entity data) and a preferred_langcode property (see above).
-  // Rather than provide a UI forcing the user to choose both separately,
-  // assume that the user profile data is in the user's preferred language. This
-  // element provides that synchronization. For use-cases where this
-  // synchronization is not desired, a module can alter or remove this element.
-  $form['language']['langcode'] = array(
-    '#type' => 'value',
-    '#value_callback' => '_user_language_selector_langcode_value',
-    // For the synchronization to work, this element must have a larger weight
-    // than the preferred_langcode element. Set a large weight here in case
-    // a module alters the weight of the other element.
-    '#weight' => 100,
-  );
-}
-
-/**
  * Sets the value of the user register and profile forms' langcode element.
  */
 function _user_language_selector_langcode_value($element, $input, &$form_state) {
@@ -948,12 +718,12 @@ function _user_language_selector_langcode_value($element, $input, &$form_state)
 }
 
 /**
- * Form validation handler for the current password on the user_account_form().
+ * Form validation handler for the current password on the user account form.
  *
- * @see user_account_form()
+ * @see AccountFormController::form()
  */
 function user_validate_current_pass(&$form, &$form_state) {
-  $account = $form['#user'];
+  $account = $form_state['user'];
   foreach ($form_state['values']['current_pass_required_values'] as $key => $name) {
     // This validation only works for required textfields (like mail) or
     // form values like password_confirm that have their own validation
@@ -971,72 +741,6 @@ function user_validate_current_pass(&$form, &$form_state) {
   }
 }
 
-/**
- * Form validation handler for user_account_form().
- *
- * @see user_account_form()
- */
-function user_account_form_validate($form, &$form_state) {
-  $account = $form['#user'];
-  // Validate new or changing username.
-  if (isset($form_state['values']['name'])) {
-    if ($error = user_validate_name($form_state['values']['name'])) {
-      form_set_error('name', $error);
-    }
-    // Cast the user ID as an integer. It might have been set to NULL, which
-    // could lead to unexpected results.
-    else {
-      $name_taken = (bool) db_select('users')
-        ->fields('users', array('uid'))
-        ->condition('uid', (int) $account->uid, '<>')
-        ->condition('name', db_like($form_state['values']['name']), 'LIKE')
-        ->range(0, 1)
-        ->execute()
-        ->fetchField();
-
-      if ($name_taken) {
-        form_set_error('name', t('The name %name is already taken.', array('%name' => $form_state['values']['name'])));
-      }
-    }
-  }
-
-  $mail = $form_state['values']['mail'];
-
-  if (!empty($mail)) {
-    $mail_taken = (bool) db_select('users')
-      ->fields('users', array('uid'))
-      ->condition('uid', (int) $account->uid, '<>')
-      ->condition('mail', db_like($mail), 'LIKE')
-      ->range(0, 1)
-      ->execute()
-      ->fetchField();
-
-    if ($mail_taken) {
-      // Format error message dependent on whether the user is logged in or not.
-      if ($GLOBALS['user']->uid) {
-        form_set_error('mail', t('The e-mail address %email is already taken.', array('%email' => $mail)));
-      }
-      else {
-        form_set_error('mail', t('The e-mail address %email is already registered. <a href="@password">Have you forgotten your password?</a>', array('%email' => $mail, '@password' => url('user/password'))));
-      }
-    }
-  }
-
-  // Make sure the signature isn't longer than the size of the database field.
-  // Signatures are disabled by default, so make sure it exists first.
-  if (isset($form_state['values']['signature'])) {
-    // Move text format for user signature into 'signature_format'.
-    $form_state['values']['signature_format'] = $form_state['values']['signature']['format'];
-    // Move text value for user signature into 'signature'.
-    $form_state['values']['signature'] = $form_state['values']['signature']['value'];
-
-    $user_schema = drupal_get_schema('users');
-    if (drupal_strlen($form_state['values']['signature']) > $user_schema['fields']['signature']['length']) {
-      form_set_error('signature', t('The signature is too long: it must be %max characters or less.', array('%max' => $user_schema['fields']['signature']['length'])));
-    }
-  }
-}
-
 function user_login_block($form) {
   $form['#action'] = url(current_path(), array('query' => drupal_get_destination(), 'external' => FALSE));
   $form['#id'] = 'user-login-form';
@@ -1419,6 +1123,11 @@ function user_is_logged_in() {
   return (bool) $GLOBALS['user']->uid;
 }
 
+function user_register() {
+  $account = entity_create('user', array());
+  return entity_get_form($account, 'register');
+}
+
 function user_register_access() {
   return user_is_anonymous() && variable_get('user_register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
 }
@@ -1498,8 +1207,7 @@ function user_menu() {
 
   $items['user/register'] = array(
     'title' => 'Create new account',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('user_register_form'),
+    'page callback' => 'user_register',
     'access callback' => 'user_register_access',
     'type' => MENU_LOCAL_TASK,
   );
@@ -1652,8 +1360,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,
@@ -3496,157 +3204,6 @@ function user_form_field_ui_field_edit_form_submit($form, &$form_state) {
 }
 
 /**
- * Form builder; the user registration form.
- *
- * @ingroup forms
- * @see user_account_form()
- * @see user_account_form_validate()
- * @see user_register_submit()
- */
-function user_register_form($form, &$form_state) {
-  global $user;
-
-  $admin = user_access('administer users');
-
-  // Pass access information to the submit handler. Running an access check
-  // inside the submit function interferes with form processing and breaks
-  // hook_form_alter().
-  $form['administer_users'] = array(
-     '#type' => 'value',
-     '#value' => $admin,
-  );
-
-  // If we aren't admin but already logged on, go to the user page instead.
-  if (!$admin && $user->uid) {
-    drupal_goto('user/' . $user->uid);
-  }
-
-  $form['#user'] = entity_create('user', array());
-
-  $form['#attached']['library'][] = array('system', 'jquery.cookie');
-  $form['#attributes']['class'][] = 'user-info-from-cookie';
-
-  // Start with the default user account fields.
-  user_account_form($form, $form_state);
-
-  // Attach field widgets, and hide the ones where the 'user_register_form'
-  // setting is not on.
-  field_attach_form('user', $form['#user'], $form, $form_state);
-  foreach (field_info_instances('user', 'user') as $field_name => $instance) {
-    if (empty($instance['settings']['user_register_form'])) {
-      $form[$field_name]['#access'] = FALSE;
-    }
-  }
-
-  if ($admin) {
-    // Redirect back to page which initiated the create request;
-    // usually admin/people/create.
-    $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()
- */
-function user_register_submit($form, &$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();
-  }
-  $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.
-  if ($status =! SAVED_NEW) {
-    drupal_set_message(t("Error saving user account."), 'error');
-    $form_state['redirect'] = '';
-    return;
-  }
-  $form_state['user'] = $account;
-  $form_state['values']['uid'] = $account->uid;
-
-  watchdog('user', 'New user: %name (%email).', array('%name' => $form_state['values']['name'], '%email' => $form_state['values']['mail']), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));
-
-  // Add plain text password into user account to generate mail tokens.
-  $account->password = $pass;
-
-  // New administrative account without notification.
-  $uri = entity_uri('user', $account);
-  if ($admin && !$notify) {
-    drupal_set_message(t('Created a new user account for <a href="@url">%name</a>. No e-mail has been sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
-  }
-  // No e-mail verification required; log in user immediately.
-  elseif (!$admin && !variable_get('user_email_verification', TRUE) && $account->status) {
-    _user_mail_notify('register_no_approval_required', $account);
-    $form_state['uid'] = $account->uid;
-    user_login_submit(array(), $form_state);
-    drupal_set_message(t('Registration successful. You are now logged in.'));
-    $form_state['redirect'] = '';
-  }
-  // No administrator approval required.
-  elseif ($account->status || $notify) {
-    if (empty($account->mail) && $notify) {
-      drupal_set_message(t('The new user <a href="@url">%name</a> was created without an email address, so no welcome message was sent.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
-    }
-    else {
-      $op = $notify ? 'register_admin_created' : 'register_no_approval_required';
-      _user_mail_notify($op, $account);
-      if ($notify) {
-        drupal_set_message(t('A welcome message with further instructions has been e-mailed to the new user <a href="@url">%name</a>.', array('@url' => url($uri['path'], $uri['options']), '%name' => $account->name)));
-      }
-      else {
-        drupal_set_message(t('A welcome message with further instructions has been sent to your e-mail address.'));
-        $form_state['redirect'] = '';
-      }
-    }
-  }
-  // Administrator approval required.
-  else {
-    _user_mail_notify('register_pending_approval', $account);
-    drupal_set_message(t('Thank you for applying for an account. Your account is currently pending approval by the site administrator.<br />In the meantime, a welcome message with further instructions has been sent to your e-mail address.'));
-    $form_state['redirect'] = '';
-  }
-}
-
-/**
  * Implements hook_modules_installed().
  */
 function user_modules_installed($modules) {
diff --git a/core/modules/user/user.pages.inc b/core/modules/user/user.pages.inc
index 434de72..0ac062b 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\HttpFoundation\Request;
 use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
@@ -204,86 +206,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) {
@@ -293,7 +215,8 @@ function user_edit_cancel_submit($form, &$form_state) {
     unset($_GET['destination']);
   }
   // Note: We redirect from user/uid/edit to user/uid/cancel to make the tabs disappear.
-  $form_state['redirect'] = array("user/" . $form['#user']->uid . "/cancel", array('query' => $destination));
+  $account = $form_state['controller']->getEntity($form_state);
+  $form_state['redirect'] = array("user/" . $account->uid . "/cancel", array('query' => $destination));
 }
 
 /**
