From fdd3d8b7e2393b1353db73d9c2834dc9c28c9380 Mon Sep 17 00:00:00 2001
From: Frederic G. MARAND <fgm@osinet.fr>
Date: Fri, 24 Aug 2012 14:51:13 +0200
Subject: [PATCH] Issue #1067120 and #1026616: entity render controller. Merged patch for bot.

- comment / node / term / user render controllers implemented
- Comment entity now has a $nid property
- ::view() and ::buildContent() been implemented
- new entity_view() and EntityRenderController::create()
- original procedural functions have been converted to entity_view().
- vocabularies and file can also be arguments of entity_view().
- addressed most issues in http://drupal.org/node/1026616#comment-6393754
- remaining to address: view multiple.
---
 core/modules/comment/comment.module                |  101 +-------------
 .../modules/comment/lib/Drupal/comment/Comment.php |    5 +
 .../lib/Drupal/comment/CommentRenderController.php |   79 +++++++++++
 core/modules/entity/entity.module                  |   14 ++
 .../lib/Drupal/entity/EntityRenderController.php   |  143 ++++++++++++++++++++
 .../entity/EntityRenderControllerInterface.php     |   48 +++++++
 core/modules/field/field.multilingual.inc          |    1 -
 .../node/lib/Drupal/node/NodeRenderController.php  |   58 ++++++++
 .../lib/Drupal/node/Tests/NodeBuildContentTest.php |    8 +-
 core/modules/node/node.module                      |  105 +--------------
 core/modules/search/search.api.php                 |    2 +-
 core/modules/system/system.module                  |    6 +
 .../modules/taxonomy_test/taxonomy_test.module     |   30 ++++
 .../lib/Drupal/taxonomy/TermRenderController.php   |   42 ++++++
 .../lib/Drupal/taxonomy/Tests/HooksTest.php        |   15 ++-
 core/modules/taxonomy/taxonomy.module              |   86 +++++++-----
 core/modules/taxonomy/taxonomy.pages.inc           |   17 ++-
 .../user/lib/Drupal/user/UserRenderController.php  |   24 ++++
 core/modules/user/user.module                      |   57 +--------
 19 files changed, 539 insertions(+), 302 deletions(-)
 create mode 100644 core/modules/comment/lib/Drupal/comment/CommentRenderController.php
 create mode 100644 core/modules/entity/lib/Drupal/entity/EntityRenderController.php
 create mode 100644 core/modules/entity/lib/Drupal/entity/EntityRenderControllerInterface.php
 create mode 100644 core/modules/node/lib/Drupal/node/NodeRenderController.php
 create mode 100644 core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php
 create mode 100644 core/modules/user/lib/Drupal/user/UserRenderController.php

diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module
index aef6209..4f8d7e1 100644
--- a/core/modules/comment/comment.module
+++ b/core/modules/comment/comment.module
@@ -119,6 +119,7 @@ function comment_entity_info() {
         'uuid' => 'uuid',
       ),
       'bundles' => array(),
+      'render controller class' => 'Drupal\comment\CommentRenderController',
       'view modes' => array(
         'full' => array(
           'label' => t('Full comment'),
@@ -977,104 +978,8 @@ function comment_prepare_thread(&$comments) {
  *   An array as expected by drupal_render().
  */
 function comment_view(Comment $comment, Node $node, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Populate $comment->content with a render() array.
-  comment_build_content($comment, $node, $view_mode, $langcode);
-
-  $build = $comment->content;
-  // We don't need duplicate rendering info in comment->content.
-  unset($comment->content);
-
-  $build += array(
-    '#theme' => 'comment__node_' . $node->type,
-    '#comment' => $comment,
-    '#node' => $node,
-    '#view_mode' => $view_mode,
-    '#language' => $langcode,
-  );
-
-  if (empty($comment->in_preview)) {
-    $prefix = '';
-    $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
-
-    // Add 'new' anchor if needed.
-    if (!empty($comment->first_new)) {
-      $prefix .= "<a id=\"new\"></a>\n";
-    }
-
-    // Add indentation div or close open divs as needed.
-    if ($is_threaded) {
-      $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
-    }
-
-    // Add anchor for each comment.
-    $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
-    $build['#prefix'] = $prefix;
-
-    // Close all open divs.
-    if ($is_threaded && !empty($comment->divs_final)) {
-      $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
-    }
-  }
-
-  // Allow modules to modify the structured comment.
-  drupal_alter(array('comment_view', 'entity_view'), $build, $comment);
-
-  return $build;
-}
-
-/**
- * Builds a structured array representing the comment's content.
- *
- * The content built for the comment (field values, comments, file attachments
- * or other comment components) will vary depending on the $view_mode parameter.
- *
- * @param Drupal\comment\Comment $comment
- *   A comment object.
- * @param Drupal\node\Node $node
- *   The node the comment is attached to.
- * @param $view_mode
- *   View mode, e.g. 'full', 'teaser'...
- * @param $langcode
- *   (optional) A language code to use for rendering. Defaults to the global
- *   content language of the current request.
- */
-function comment_build_content(Comment $comment, Node $node, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Remove previously built content, if exists.
-  $comment->content = array();
-
-  // Allow modules to change the view mode.
-  $context = array('langcode' => $langcode);
-  drupal_alter('entity_view_mode', $view_mode, $comment, $context);
-
-  // Build fields content.
-  field_attach_prepare_view('comment', array($comment->cid => $comment), $view_mode, $langcode);
-  entity_prepare_view('comment', array($comment->cid => $comment), $langcode);
-  $comment->content += field_attach_view('comment', $comment, $view_mode, $langcode);
-
-  $comment->content['links'] = array(
-    '#theme' => 'links__comment',
-    '#pre_render' => array('drupal_pre_render_links'),
-    '#attributes' => array('class' => array('links', 'inline')),
-  );
-  if (empty($comment->in_preview)) {
-    $comment->content['links']['comment'] = array(
-      '#theme' => 'links__comment__comment',
-      '#links' => comment_links($comment, $node),
-      '#attributes' => array('class' => array('links', 'inline')),
-    );
-  }
-
-  // Allow modules to make their own additions to the comment.
-  module_invoke_all('comment_view', $comment, $view_mode, $langcode);
-  module_invoke_all('entity_view', $comment, $view_mode, $langcode);
+  $comment->node = $node;
+  return entity_view($comment, $view_mode, $langcode);
 }
 
 /**
diff --git a/core/modules/comment/lib/Drupal/comment/Comment.php b/core/modules/comment/lib/Drupal/comment/Comment.php
index a7bacd4..6656f0c 100644
--- a/core/modules/comment/lib/Drupal/comment/Comment.php
+++ b/core/modules/comment/lib/Drupal/comment/Comment.php
@@ -36,6 +36,11 @@ class Comment extends Entity {
   public $pid;
 
   /**
+   * The ID of the node to which the comment is attached.
+   */
+  public $nid;
+
+  /**
    * The comment language code.
    *
    * @var string
diff --git a/core/modules/comment/lib/Drupal/comment/CommentRenderController.php b/core/modules/comment/lib/Drupal/comment/CommentRenderController.php
new file mode 100644
index 0000000..b1b56fa
--- /dev/null
+++ b/core/modules/comment/lib/Drupal/comment/CommentRenderController.php
@@ -0,0 +1,79 @@
+<?php
+namespace Drupal\comment;
+
+use Drupal\comment\Comment;
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityRenderController;
+use Drupal\node\Node;
+
+class CommentRenderController extends EntityRenderController {
+
+  public function buildContent(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    parent::buildContent($entity, $view_mode, $langcode);
+
+    if (!isset($entity->node)) {
+      $node = node_load($entity->nid);
+      if (empty($node)) {
+        throw new \InvalidArgumentException(t('Invalid node for comment'));
+      }
+      $entity->node = $node;
+    }
+
+    parent::prepareView($entity, $view_mode, $langcode);
+
+    $entity->content['links'] = array(
+        '#theme' => 'links__comment',
+        '#pre_render' => array('drupal_pre_render_links'),
+        '#attributes' => array('class' => array('links', 'inline')),
+    );
+    if (empty($entity->in_preview)) {
+      $entity->content['links'][$this->entityType] = array(
+        '#theme' => 'links__comment__comment',
+        // The "node" property is specified to be present, so no need to check.
+        '#links' => comment_links($entity, $entity->node),
+        '#attributes' => array('class' => array('links', 'inline')),
+      );
+    }
+    return $entity->content;
+  }
+
+  protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
+    $return = parent::getBuildDefaults($entity, $view_mode, $langcode);
+    $node = $entity->node;
+    $return = array_merge($return, array(
+      '#theme' => 'comment__node_' . $node->type,
+      '#node' => $node,
+    ));
+    return $return;
+  }
+
+  protected function prepareBuild(array $build, EntityInterface $comment, $view_mode, $langcode = NULL) {
+    if (empty($comment->in_preview)) {
+      $prefix = '';
+      // property 'node' has been initialized in EntityViewController::normalizeArguments().
+      $node = $comment->node;
+      $is_threaded = isset($comment->divs) && variable_get('comment_default_mode_' . $node->type, COMMENT_MODE_THREADED) == COMMENT_MODE_THREADED;
+
+      // Add 'new' anchor if needed.
+      if (!empty($comment->first_new)) {
+        $prefix .= "<a id=\"new\"></a>\n";
+      }
+
+      // Add indentation div or close open divs as needed.
+      if ($is_threaded) {
+        $prefix .= $comment->divs <= 0 ? str_repeat('</div>', abs($comment->divs)) : "\n" . '<div class="indented">';
+      }
+
+      // Add anchor for each comment.
+      $prefix .= "<a id=\"comment-$comment->cid\"></a>\n";
+      $build['#prefix'] = $prefix;
+
+      // Close all open divs.
+      if ($is_threaded && !empty($comment->divs_final)) {
+        $build['#suffix'] = str_repeat('</div>', $comment->divs_final);
+      }
+    }
+
+    return $build;
+  }
+}
diff --git a/core/modules/entity/entity.module b/core/modules/entity/entity.module
index 74f2fcf..05041df 100644
--- a/core/modules/entity/entity.module
+++ b/core/modules/entity/entity.module
@@ -1,5 +1,7 @@
 <?php
 
+use Drupal\entity\EntityRenderController;
+
 /**
  * @file
  * Entity API for handling entities like nodes or users.
@@ -210,6 +212,18 @@ function entity_load_by_uuid($entity_type, $uuid, $reset = FALSE) {
 }
 
 /**
+ * Return the render array for an entity.
+ *
+ * @param EntityInterface $entity
+ * @param string $view_mode
+ * @param string $langcode
+ */
+function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) {
+  return EntityRenderController::create($entity->entityType())
+    ->view($entity, $view_mode, $langcode);
+}
+
+/**
  * Loads multiple entities from the database.
  *
  * This function should be used whenever you need to load more than one entity
diff --git a/core/modules/entity/lib/Drupal/entity/EntityRenderController.php b/core/modules/entity/lib/Drupal/entity/EntityRenderController.php
new file mode 100644
index 0000000..3cec13d
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/EntityRenderController.php
@@ -0,0 +1,143 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\EntityRenderController.
+ */
+
+namespace Drupal\entity;
+
+/**
+ * Base class for entity view controllers.
+ */
+class EntityRenderController implements EntityRenderControllerInterface {
+
+  /**
+   * The type of entities for which this controller is instantiated.
+   *
+   * @var string
+   */
+  protected $entityType;
+
+  public function __construct($entity_type) {
+    $this->entityType = $entity_type;
+  }
+
+  /**
+   * @see \Drupal\entity\EntityRenderControllerInterface::buildContent()
+   */
+  public function buildContent(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    if (!isset($langcode)) {
+      $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
+    }
+    // Remove previously built content, if exists.
+    $entity->content = array();
+
+    // Allow modules to change the view mode.
+    $context = array('langcode' => $langcode);
+    drupal_alter('entity_view_mode', $view_mode, $entity, $context);
+    return $entity->content;
+  }
+
+  /**
+   * Factory method to returns an entity render controller.
+   *
+   * @see hook_entity_info()
+   *
+   * @param $entity_type
+   *   The type of the entity.
+   *
+   * @return Drupal\entity\EntityViewControllerInterface
+   *   An entity view controller instance. If the entity does not define a render
+   *   controller class, this function will return a default render controller
+   *   with no specific behaviors, just building from the entity hooks.
+   */
+  public static function create($entity_type) {
+    $info = entity_get_info($entity_type);
+
+    $class = isset($info['render controller class'])
+    ? $info['render controller class']
+    : 'Drupal\entity\EntityRenderController';
+
+    return new $class($entity_type);
+  }
+
+  /**
+   * Build fields content.
+   *
+   * In case of a multiple view, "{$entity}_view_multiple"() already ran the
+   * 'prepare_view' step. An internal flag prevents the operation from running
+   * twice.
+   *
+   * @param EntityInterface $entity
+   * @param string $view_mode
+   * @param string $langcode
+   */
+  protected function prepareView(EntityInterface $entity, $view_mode, $langcode) {
+    $entry = array($entity->id() => $entity);
+    field_attach_prepare_view($this->entityType, $entry, $view_mode, $langcode);
+    entity_prepare_view($this->entityType, $entry, $langcode);
+    $entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
+  }
+
+  /**
+   * Provide entity-specific defaults to the build process.
+   *
+   * @param EntityInterface $entity
+   * @param string $view_mode
+   * @param string $langcode
+   *
+   * @return array
+   */
+  protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
+    $return = array(
+      '#theme' => $this->entityType,
+      "#{$this->entityType}" => $entity,
+      '#view_mode' => $view_mode,
+      '#langcode' => $langcode,
+    );
+
+    return $return;
+  }
+
+  /**
+   * Specific per-entity building.
+   *
+   * This method is typically not invoked by the child reimplementations, but
+   * simply replaced.
+   *
+   * @param array $build
+   * @param EntityInterface $entity
+   * @param string $view_mode
+   * @param string $langcode
+   *
+   * @return array
+   *   The build array.
+   */
+  protected function prepareBuild(array $build, EntityInterface $entity, $view_mode, $langcode = NULL) {
+    return $build;
+  }
+
+  /**
+   * @see \Drupal\entity\EntityRenderControllerInterface::view()
+   */
+  public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    $this->buildContent($entity, $view_mode, $langcode);
+
+    $view_hook = "{$this->entityType}_view";
+    module_invoke_all($view_hook, $entity, $view_mode, $langcode);
+    module_invoke_all('entity_view', $entity, $view_mode, $langcode);
+
+    $build = $entity->content;
+    // We don't need duplicate rendering info in $entity->content.
+    unset($entity->content);
+
+    $build += $this->getBuildDefaults($entity, $view_mode, $langcode);
+    $build = $this->prepareBuild($build, $entity, $view_mode, $langcode);
+
+    // Allow modules to modify the structured comment.
+    drupal_alter(array($view_hook, 'entity_view'), $build, $entity);
+
+    return $build;
+  }
+}
diff --git a/core/modules/entity/lib/Drupal/entity/EntityRenderControllerInterface.php b/core/modules/entity/lib/Drupal/entity/EntityRenderControllerInterface.php
new file mode 100644
index 0000000..d4e3761
--- /dev/null
+++ b/core/modules/entity/lib/Drupal/entity/EntityRenderControllerInterface.php
@@ -0,0 +1,48 @@
+<?php
+
+/**
+ * @file
+ * Definition of Drupal\entity\EntityRenderControllerInterface.
+ */
+
+namespace Drupal\entity;
+
+/**
+ * Defines a common interface for entity view controller classes.
+ */
+interface EntityRenderControllerInterface {
+  /**
+   * Main Entity view method.
+   *
+   * @param EntityInterface $entity
+   *   The entity to view.
+   * @param string $view_mode
+   *   The view mode to use when building that entity. All core entities include
+   *   at least a default "full" view mode.
+   * @param string $langcode
+   *   The language for which to view the entity.
+   *
+   * @throws \InvalidArgumentException
+   *   Can be thrown when the set of parameters is inconsistent, like when
+   *   trying to view a Comment and passing a Node which is not the one the
+   *   comment belongs to, or not passing one, and having the comment node not
+   *   be available for loading.
+   */
+  public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL);
+
+  /**
+   * Build the structured $content property on the entity.
+   *
+   * @param EntityInterface $entity
+   *   The entity whose content is being built.
+   * @param string $view_mode
+   *   The view mode to use when building that entity. All core entities include
+   *   at least a default "full" view mode.
+   * @param string $langcode
+   *   The language for which to built the content of the entity.
+   *
+   * @return array
+   *   The content array.
+   */
+  public function buildContent(EntityInterface $entity, $view_mode = 'full', $langcode = NULL);
+}
diff --git a/core/modules/field/field.multilingual.inc b/core/modules/field/field.multilingual.inc
index e3fbd2d..cba12bd 100644
--- a/core/modules/field/field.multilingual.inc
+++ b/core/modules/field/field.multilingual.inc
@@ -298,7 +298,6 @@ function field_language($entity_type, $entity, $field_name = NULL, $langcode = N
   $id = $entity->bundle();
   $bundle = $entity->bundle();
   $langcode = field_valid_language($langcode, FALSE);
-
   if (!isset($display_langcodes[$entity_type][$id][$langcode])) {
     $display_langcode = array();
 
diff --git a/core/modules/node/lib/Drupal/node/NodeRenderController.php b/core/modules/node/lib/Drupal/node/NodeRenderController.php
new file mode 100644
index 0000000..a435dfa
--- /dev/null
+++ b/core/modules/node/lib/Drupal/node/NodeRenderController.php
@@ -0,0 +1,58 @@
+<?php
+namespace Drupal\node;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityRenderController;
+
+class NodeRenderController extends EntityRenderController {
+
+  public function buildContent(EntityInterface $entity, $view_mode  = 'full', $langcode = NULL) {
+    parent::buildContent($entity, $view_mode, $langcode);
+
+    // The 'view' hook can be implemented to overwrite the default function
+    // to display nodes.
+    if (node_hook($entity, 'view')) {
+      $entity = node_invoke($entity, 'view', $view_mode, $langcode);
+    }
+
+    parent::prepareView($entity, $view_mode, $langcode);
+
+    $entity->content['links'] = array(
+      '#theme' => 'links__node',
+      '#pre_render' => array('drupal_pre_render_links'),
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
+
+    // Always display a read more link on teasers because we have no way
+    // to know when a teaser view is different than a full view.
+    $links = array();
+    if ($view_mode == 'teaser') {
+      $node_title_stripped = strip_tags($entity->label());
+      $links['node-readmore'] = array(
+        'title' => t('Read more<span class="element-invisible"> about @title</span>', array('@title' => $node_title_stripped)),
+        'href' => 'node/' . $entity->nid,
+        'html' => TRUE,
+        'attributes' => array('rel' => 'tag', 'title' => $node_title_stripped),
+      );
+    }
+
+    $entity->content['links']['node'] = array(
+      '#theme' => 'links__node__node',
+      '#links' => $links,
+      '#attributes' => array('class' => array('links', 'inline')),
+    );
+    return $entity->content;
+  }
+
+  protected function prepareBuild(array $build, EntityInterface $entity, $view_mode, $langcode = NULL) {
+    // Add contextual links for this node, except when the node is already being
+    // displayed on its own page. Modules may alter this behavior (for example,
+    // to restrict contextual links to certain view modes) by implementing
+    // hook_node_view_alter().
+    if (!empty($entity->nid) && !($view_mode == 'full' && node_is_page($entity))) {
+      $build['#contextual_links']['node'] = array('node', array($entity->nid));
+    }
+
+    return $build;
+  }
+}
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeBuildContentTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeBuildContentTest.php
index 2b0dbeb..d1bff1c 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeBuildContentTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeBuildContentTest.php
@@ -7,6 +7,8 @@
 
 namespace Drupal\node\Tests;
 
+use Drupal\entity\EntityRenderController;
+
 /**
  * Test to ensure that a node's content is always rebuilt.
  */
@@ -27,8 +29,10 @@ class NodeBuildContentTest extends NodeTestBase {
     $node = $this->drupalCreateNode();
 
     // Set a property in the content array so we can test for its existence later on.
-    $node->content['test_content_property'] = array('#value' => $this->randomString());
-    $content = node_build_content($node);
+    $node->content['test_content_property'] = array(
+      '#value' => $this->randomString(),
+    );
+    $content = EntityRenderController::create('node')->buildContent($node);
 
     // If the property doesn't exist it means the node->content was rebuilt.
     $this->assertFalse(isset($content['test_content_property']), t('Node content was emptied prior to being built.'));
diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index 349aab5..41ed0a8 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -215,6 +215,7 @@ function node_entity_info() {
         'bundle' => 'type',
       ),
       'bundles' => array(),
+      'render controller class' => 'Drupal\node\NodeRenderController',
       'view modes' => array(
         'full' => array(
           'label' => t('Full content'),
@@ -1122,53 +1123,6 @@ function node_revision_delete($revision_id) {
 /**
  * Generates an array for rendering the given node.
  *
- * @param Drupal\node\Node $node
- *   A node entity.
- * @param $view_mode
- *   (optional) View mode, e.g., 'full', 'teaser'... Defaults to 'full.'
- * @param $langcode
- *   (optional) A language code to use for rendering. Defaults to the global
- *   content language of the current request.
- *
- * @return
- *   An array as expected by drupal_render().
- */
-function node_view(Node $node, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Populate $node->content with a render() array.
-  node_build_content($node, $view_mode, $langcode);
-
-  $build = $node->content;
-  // We don't need duplicate rendering info in node->content.
-  unset($node->content);
-
-  $build += array(
-    '#theme' => 'node',
-    '#node' => $node,
-    '#view_mode' => $view_mode,
-    '#langcode' => $langcode,
-  );
-
-  // Add contextual links for this node, except when the node is already being
-  // displayed on its own page. Modules may alter this behavior (for example,
-  // to restrict contextual links to certain view modes) by implementing
-  // hook_node_view_alter().
-  if (!empty($node->nid) && !($view_mode == 'full' && node_is_page($node))) {
-    $build['#contextual_links']['node'] = array('node', array($node->nid));
-  }
-
-  // Allow modules to modify the structured node.
-  drupal_alter(array('node_view', 'entity_view'), $build, $node);
-
-  return $build;
-}
-
-/**
- * Builds a structured array representing the node's content.
- *
  * The content built for the node (field values, comments, file attachments or
  * other node components) will vary depending on the $view_mode parameter.
  *
@@ -1193,59 +1147,12 @@ function node_view(Node $node, $view_mode = 'full', $langcode = NULL) {
  * @param $langcode
  *   (optional) A language code to use for rendering. Defaults to the global
  *   content language of the current request.
+ *
+ * @return
+ *   An array as expected by drupal_render().
  */
-function node_build_content(Node $node, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Remove previously built content, if exists.
-  $node->content = array();
-
-  // Allow modules to change the view mode.
-  $context = array('langcode' => $langcode);
-  drupal_alter('entity_view_mode', $view_mode, $node, $context);
-
-  // The 'view' hook can be implemented to overwrite the default function
-  // to display nodes.
-  if (node_hook($node, 'view')) {
-    $node = node_invoke($node, 'view', $view_mode, $langcode);
-  }
-
-  // Build fields content.
-  // In case of a multiple view, node_view_multiple() already ran the
-  // 'prepare_view' step. An internal flag prevents the operation from running
-  // twice.
-  field_attach_prepare_view('node', array($node->nid => $node), $view_mode, $langcode);
-  entity_prepare_view('node', array($node->nid => $node), $langcode);
-  $node->content += field_attach_view('node', $node, $view_mode, $langcode);
-
-  // Always display a read more link on teasers because we have no way
-  // to know when a teaser view is different than a full view.
-  $links = array();
-  $node->content['links'] = array(
-    '#theme' => 'links__node',
-    '#pre_render' => array('drupal_pre_render_links'),
-    '#attributes' => array('class' => array('links', 'inline')),
-  );
-  if ($view_mode == 'teaser') {
-    $node_title_stripped = strip_tags($node->label());
-    $links['node-readmore'] = array(
-      'title' => t('Read more<span class="element-invisible"> about @title</span>', array('@title' => $node_title_stripped)),
-      'href' => 'node/' . $node->nid,
-      'html' => TRUE,
-      'attributes' => array('rel' => 'tag', 'title' => $node_title_stripped),
-    );
-  }
-  $node->content['links']['node'] = array(
-    '#theme' => 'links__node__node',
-    '#links' => $links,
-    '#attributes' => array('class' => array('links', 'inline')),
-  );
-
-  // Allow modules to make their own additions to the node.
-  module_invoke_all('node_view', $node, $view_mode, $langcode);
-  module_invoke_all('entity_view', $node, $view_mode, $langcode);
+function node_view(Node $node, $view_mode = 'full', $langcode = NULL) {
+  return entity_view($node, $view_mode, $langcode);
 }
 
 /**
diff --git a/core/modules/search/search.api.php b/core/modules/search/search.api.php
index 4c24475..aeb5639 100644
--- a/core/modules/search/search.api.php
+++ b/core/modules/search/search.api.php
@@ -351,7 +351,7 @@ function hook_update_index() {
     variable_set('node_cron_last', $node->changed);
 
     // Render the node.
-    node_build_content($node, 'search_index');
+    $build = node_view($node, 'search_index');
     $node->rendered = drupal_render($node->content);
 
     $text = '<h1>' . check_plain($node->label()) . '</h1>' . $node->rendered;
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index de7241f..009a6ac 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -285,6 +285,12 @@ function system_entity_info() {
         'uuid' => 'uuid',
       ),
       'static cache' => FALSE,
+      'view modes' => array(
+        'full' => array(
+          'label' => t('File default'),
+          'custom settings' => FALSE,
+        ),
+      ),
     ),
   );
 }
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 36a08af..63f6406 100644
--- a/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
+++ b/core/modules/system/tests/modules/taxonomy_test/taxonomy_test.module
@@ -3,6 +3,8 @@
 /**
  * @file
  * Test module for Taxonomy hooks and functions not used in core.
+ *
+ * @see Drupal\taxonomy\Tests\TaxonomyHooksTestCase::testTaxonomyTermHooks()
  */
 
 use Drupal\taxonomy\Term;
@@ -57,6 +59,34 @@ function taxonomy_test_taxonomy_term_delete(Term $term) {
 }
 
 /**
+ * Implements hook_taxonomy_term_view().
+ */
+function taxonomy_test_taxonomy_term_view($term, $view_mode, $langcode) {
+  if ($view_mode == 'full') {
+    $term->content['taxonomy_test_term_view_check'] = array(
+      '#prefix' => '<div>',
+      '#markup' => t('The antonym is %antonym', array('%antonym' => $term->antonym)),
+      '#suffix' => '</div>',
+      '#weight' => 10,
+    );
+  }
+}
+
+/**
+ * Implements hook_entity_view().
+ */
+function taxonomy_test_entity_view($entity, $view_mode, $langcode) {
+  if ($entity->entityType() == 'taxonomy_term' && $view_mode == 'full') {
+    $entity->content['taxonomy_test_entity_view_check'] = array(
+      '#prefix' => '<div>',
+      '#markup' => t('The antonym is %antonym', array('%antonym' => $entity->antonym)),
+      '#suffix' => '</div>',
+      '#weight' => 20,
+    );
+  }
+}
+
+/**
  * Implements hook_form_FORM_ID_alter().
  */
 function taxonomy_test_form_taxonomy_term_form_alter(&$form, $form_state, $form_id) {
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php
new file mode 100644
index 0000000..671eeca
--- /dev/null
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/TermRenderController.php
@@ -0,0 +1,42 @@
+<?php
+namespace Drupal\taxonomy;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityRenderController;
+
+class TermRenderController extends EntityRenderController {
+
+  public function buildContent(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    parent::buildContent($entity, $view_mode, $langcode);
+
+    // Try to add in the core taxonomy pieces like description and nodes.
+    $settings = field_view_mode_settings($this->entityType, $entity->bundle());
+    $fields = field_extra_fields_get_display($this->entityType, $entity->bundle(), $view_mode);
+    if (!empty($entity->description) && isset($fields['description']) && $fields['description']['visible']) {
+      $entity->content['description'] = array(
+        '#markup' => check_markup($entity->description, $entity->format, '', TRUE),
+        '#weight' => $fields['description']['weight'],
+        '#prefix' => '<div class="taxonomy-term-description">',
+        '#suffix' => '</div>',
+      );
+    }
+
+    parent::prepareView($entity, $view_mode, $langcode);
+    return $entity->content;
+  }
+
+  protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
+    $return = parent::getBuildDefaults($entity, $view_mode, $langcode);
+
+    // TODO: rename "term" to "taxonomy_term" in theme_taxonomy_term().
+    $return['#term'] = $return["#{$this->entityType}"];
+    unset($return[$this->entityType]);
+
+    return $return;
+  }
+
+  protected function prepareBuild(array $build, EntityInterface $node, $view_mode, $langcode = NULL) {
+    $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/taxonomy.css';
+    return $build;
+  }
+}
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/HooksTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/HooksTest.php
index 3dd43b1..42c9693 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/HooksTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/HooksTest.php
@@ -35,7 +35,12 @@ class HooksTest extends TaxonomyTestBase {
   }
 
   /**
-   * Test that hooks are run correctly on creating, editing and deleting a term.
+   * Test hooks on CRUD of terms.
+   *
+   * Test that hooks are run correctly on creating, editing, viewing, and
+   * deleting a term.
+   *
+   * @see taxonomy_test.module
    */
   function testTaxonomyTermHooks() {
     $vocabulary = $this->createVocabulary();
@@ -60,6 +65,14 @@ class HooksTest extends TaxonomyTestBase {
     $term = taxonomy_term_load($term->tid);
     $this->assertEqual($edit['antonym'], $term->antonym, 'Antonym was successfully edited.');
 
+    // View the term and ensure that hook_taxonomy_term_view() and
+    // hook_entity_view() are invoked.
+    $term = taxonomy_term_load($term->tid);
+    module_load_include('inc', 'taxonomy', 'taxonomy.pages');
+    $term_build = taxonomy_term_page($term);
+    $this->assertFalse(empty($term_build['taxonomy_terms'][$term->tid]['taxonomy_test_term_view_check']), 'hook_taxonomy_term_view() was invoked when viewing the term.');
+    $this->assertFalse(empty($term_build['taxonomy_terms'][$term->tid]['taxonomy_test_entity_view_check']), 'hook_entity_view() was invoked when viewing the term.');
+
     // Delete the term.
     taxonomy_term_delete($term->tid);
     $antonym = db_query('SELECT tid FROM {taxonomy_term_antonym} WHERE tid = :tid', array(':tid' => $term->tid))->fetchField();
diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module
index a772e02..e57aae8 100644
--- a/core/modules/taxonomy/taxonomy.module
+++ b/core/modules/taxonomy/taxonomy.module
@@ -129,6 +129,7 @@ function taxonomy_entity_info() {
         'bundle' => 'machine_name',
       ),
       'bundles' => array(),
+      'render controller class' => 'Drupal\taxonomy\TermRenderController',
       'view modes' => array(
         // @todo View mode for display as a field (when attached to nodes etc).
         'full' => array(
@@ -162,6 +163,12 @@ function taxonomy_entity_info() {
       'label' => 'name',
     ),
     'fieldable' => FALSE,
+    'view modes' => array(
+      'full' => array(
+        'label' => t('Taxonomy vocabulary default'),
+        'custom settings' => FALSE,
+      ),
+    ),
   );
 
   return $return;
@@ -569,13 +576,51 @@ function taxonomy_term_delete_multiple(array $tids) {
 }
 
 /**
+ * Constructs a drupal_render() style array from an array of loaded terms.
+ *
+ * @param array $terms
+ *   An array of taxonomy terms as returned by taxonomy_term_load_multiple().
+ * @param string $view_mode
+ *   View mode, e.g. 'full', 'teaser'...
+ * @param int $weight
+ *   An integer representing the weight of the first node in the list.
+ * @param string $langcode
+ *   (optional) A language code to use for rendering. Defaults to the global
+ *   content language of the current request.
+ *
+ * @return array
+ *   An array in the format expected by drupal_render().
+ */
+function taxonomy_term_view_multiple(array $terms, $view_mode = 'teaser', $weight = 0, $langcode = NULL) {
+  field_attach_prepare_view('taxonomy_term', $terms, $view_mode, $langcode);
+  entity_prepare_view('taxonomy_term', $terms, $langcode);
+  $build = array();
+  foreach ($terms as $term) {
+    $build['taxonomy_terms'][$term->tid] = taxonomy_term_view($term, $view_mode, $langcode);
+    $build['taxonomy_terms'][$term->tid]['#weight'] = $weight;
+    $weight++;
+  }
+  $build['taxonomy_terms']['#sorted'] = TRUE;
+  return $build;
+}
+
+/**
  * Generate an array for rendering the given term.
  *
+ * The content built for the taxonomy term (field values, file attachments or
+ * other term components) will vary depending on the $view_mode parameter.
+ *
+ * Drupal core defines the following view modes for terms, with the following
+ * default use cases:
+ *   - full (default): term is displayed on its own page (taxonomy/term/123)
+ * Contributed modules might define additional view modes, or use existing
+ * view modes in additional contexts.
+ *
  * @param Drupal\taxonomy\Term $term
  *   A taxonomy term entity.
- * @param $view_mode
+ * @param string $view_mode
  *   View mode, e.g. 'full', 'teaser'...
- * @param $langcode
+ * @param string $langcode
  *   (optional) A language code to use for rendering. Defaults to the global
  *   content language of the current request.
  *
@@ -583,42 +628,7 @@ function taxonomy_term_delete_multiple(array $tids) {
  *   An array as expected by drupal_render().
  */
 function taxonomy_term_view(Term $term, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Allow modules to change the view mode.
-  $context = array('langcode' => $langcode);
-  drupal_alter('entity_view_mode', $view_mode, $term, $context);
-
-  field_attach_prepare_view('taxonomy_term', array($term->tid => $term), $view_mode, $langcode);
-  entity_prepare_view('taxonomy_term', array($term->tid => $term), $langcode);
-
-  $build = array(
-    '#theme' => 'taxonomy_term',
-    '#term' => $term,
-    '#view_mode' => $view_mode,
-    '#language' => $langcode,
-  );
-
-  $build += field_attach_view('taxonomy_term', $term, $view_mode, $langcode);
-
-  // Add term description if the term has one.
-  if (!empty($term->description)) {
-    $build['description'] = array(
-      '#markup' => check_markup($term->description, $term->format, '', TRUE),
-      '#weight' => 0,
-      '#prefix' => '<div class="taxonomy-term-description">',
-      '#suffix' => '</div>',
-    );
-  }
-
-  $build['#attached']['css'][] = drupal_get_path('module', 'taxonomy') . '/taxonomy.css';
-
-  // Allow modules to modify the structured term.
-  drupal_alter(array('taxonomy_term_view', 'entity_view'), $build, $term);
-
-  return $build;
+  return entity_view($term, $view_mode, $langcode);
 }
 
 /**
diff --git a/core/modules/taxonomy/taxonomy.pages.inc b/core/modules/taxonomy/taxonomy.pages.inc
index 597b95d..c7be1ad 100644
--- a/core/modules/taxonomy/taxonomy.pages.inc
+++ b/core/modules/taxonomy/taxonomy.pages.inc
@@ -35,21 +35,24 @@ function taxonomy_term_page(Term $term) {
   drupal_set_breadcrumb($breadcrumb);
   drupal_add_feed('taxonomy/term/' . $term->tid . '/feed', 'RSS - ' . $term->label());
 
-  $build = array();
+  // If there is a menu link to this term, the link becomes the last part
+  // of the active trail, and the link name becomes the page title.
+  // Thus, we must explicitly set the page title to be the node title.
+  $uri = $term->uri();
 
-  $build['term_heading'] = array(
-    '#prefix' => '<div class="term-listing-heading">',
-    '#suffix' => '</div>',
-    'term' => taxonomy_term_view($term, 'full'),
-  );
+  // Set the term path as the canonical URL to prevent duplicate content.
+  drupal_add_html_head_link(array('rel' => 'canonical', 'href' => url($uri['path'], $uri['options'])), TRUE);
+  // Set the non-aliased path as a default shortlink.
+  drupal_add_html_head_link(array('rel' => 'shortlink', 'href' => url($uri['path'], array_merge($uri['options'], array('alias' => TRUE)))), TRUE);
 
+  $build = taxonomy_term_view_multiple(array($term->tid => $term), 'full');
   if ($nids = taxonomy_select_nodes($term->tid, TRUE, variable_get('default_nodes_main', 10))) {
     $nodes = node_load_multiple($nids);
     $build += node_view_multiple($nodes);
     $build['pager'] = array(
       '#theme' => 'pager',
       '#weight' => 5,
-     );
+    );
   }
   else {
     $build['no_content'] = array(
diff --git a/core/modules/user/lib/Drupal/user/UserRenderController.php b/core/modules/user/lib/Drupal/user/UserRenderController.php
new file mode 100644
index 0000000..320ed0e
--- /dev/null
+++ b/core/modules/user/lib/Drupal/user/UserRenderController.php
@@ -0,0 +1,24 @@
+<?php
+namespace Drupal\user;
+
+use Drupal\entity\EntityInterface;
+use Drupal\entity\EntityRenderController;
+
+class UserRenderController extends EntityRenderController {
+
+  public function buildContent(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
+    parent::buildContent($entity, $view_mode, $langcode);
+    parent::prepareView($entity, $view_mode, $langcode);
+    return $entity->content;
+  }
+
+  protected function getBuildDefaults(EntityInterface $entity, $view_mode, $langcode) {
+    $return = parent::getBuildDefaults($entity, $view_mode, $langcode);
+
+    // @todo rename "theme_user_profile" to "theme_user", 'account' to 'user'.
+    $return['#theme'] = 'user_profile';
+    $return['#account'] = $return['#user'];
+
+    return $return;
+  }
+}
diff --git a/core/modules/user/user.module b/core/modules/user/user.module
index 44cb266..4a151ab 100644
--- a/core/modules/user/user.module
+++ b/core/modules/user/user.module
@@ -170,6 +170,7 @@ function user_entity_info() {
           ),
         ),
       ),
+      'render controller class' => 'Drupal\user\UserRenderController',
       'view modes' => array(
         'full' => array(
           'label' => t('User account'),
@@ -2072,61 +2073,7 @@ function user_view_page($account) {
  *   An array as expected by drupal_render().
  */
 function user_view($account, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Retrieve all profile fields and attach to $account->content.
-  user_build_content($account, $view_mode, $langcode);
-
-  $build = $account->content;
-  // We don't need duplicate rendering info in account->content.
-  unset($account->content);
-
-  $build += array(
-    '#theme' => 'user_profile',
-    '#account' => $account,
-    '#view_mode' => $view_mode,
-    '#language' => $langcode,
-  );
-
-  // Allow modules to modify the structured user.
-  drupal_alter(array('user_view', 'entity_view'), $build, $account);
-
-  return $build;
-}
-
-/**
- * Builds a structured array representing the profile content.
- *
- * @param $account
- *   A user object.
- * @param $view_mode
- *   View mode, e.g. 'full'.
- * @param $langcode
- *   (optional) A language code to use for rendering. Defaults to the global
- *   content language of the current request.
- */
-function user_build_content($account, $view_mode = 'full', $langcode = NULL) {
-  if (!isset($langcode)) {
-    $langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
-  }
-
-  // Remove previously built content, if exists.
-  $account->content = array();
-
-  // Allow modules to change the view mode.
-  $context = array('langcode' => $langcode);
-  drupal_alter('entity_view_mode', $view_mode, $account, $context);
-
-  // Build fields content.
-  field_attach_prepare_view('user', array($account->uid => $account), $view_mode, $langcode);
-  entity_prepare_view('user', array($account->uid => $account), $langcode);
-  $account->content += field_attach_view('user', $account, $view_mode, $langcode);
-
-  // Populate $account->content with a render() array.
-  module_invoke_all('user_view', $account, $view_mode, $langcode);
-  module_invoke_all('entity_view', $account, $view_mode, $langcode);
+  return entity_view($account, $view_mode, $langcode);
 }
 
 /**
-- 
1.7.4.1

