Commit e9ca778b authored by webchick's avatar webchick

Issue #1026616 by fgm, Berdir, Dave Reid, fago: Implement an entity render controller.

parent b149df70
......@@ -27,6 +27,8 @@
* The class has to implement the
* Drupal\Core\Entity\EntityStorageControllerInterface interface. Leave blank
* to use the Drupal\Core\Entity\DatabaseStorageController implementation.
* - render controller class: The name of the class that is used to render
* the entities. Deafaults to Drupal\Core\Entity\EntityRenderController.
* - 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
......
......@@ -49,6 +49,7 @@ function entity_get_info($entity_type = NULL) {
'entity class' => 'Drupal\Core\Entity\Entity',
'controller class' => 'Drupal\Core\Entity\DatabaseStorageController',
'list controller class' => 'Drupal\Core\Entity\EntityListController',
'render controller class' => 'Drupal\Core\Entity\EntityRenderController',
'form controller class' => array(
'default' => 'Drupal\Core\Entity\EntityFormController',
),
......@@ -316,47 +317,6 @@ function entity_get_controller($entity_type) {
return $controllers[$entity_type];
}
/**
* Invokes hook_entity_prepare_view().
*
* If adding a new entity similar to nodes, comments or users, you should
* invoke this function during the ENTITY_build_content() or
* ENTITY_view_multiple() phases of rendering to allow other modules to alter
* the objects during this phase. This is needed for situations where
* information needs to be loaded outside of ENTITY_load() - particularly
* when loading entities into one another - i.e. a user object into a node, due
* to the potential for unwanted side-effects such as caching and infinite
* recursion. By convention, entity_prepare_view() is called after
* field_attach_prepare_view() to allow entity level hooks to act on content
* loaded by field API.
*
* @param $entity_type
* The type of entity, i.e. 'node', 'user'.
* @param $entities
* The entity objects which are being prepared for view, keyed by object ID.
*
* @see hook_entity_prepare_view()
*/
function entity_prepare_view($entity_type, $entities) {
// To ensure hooks are only run once per entity, check for an
// entity_view_prepared flag and only process items without it.
// @todo: resolve this more generally for both entity and field level hooks.
$prepare = array();
foreach ($entities as $id => $entity) {
if (empty($entity->entity_view_prepared)) {
// Add this entity to the items to be prepared.
$prepare[$id] = $entity;
// Mark this item as prepared.
$entity->entity_view_prepared = TRUE;
}
}
if (!empty($prepare)) {
module_invoke_all('entity_prepare_view', $prepare, $entity_type);
}
}
/**
* Returns the label of an entity.
*
......@@ -562,3 +522,57 @@ function entity_list_controller($entity_type) {
$class = $entity_info['list controller class'];
return new $class($entity_type, $storage);
}
/**
* Returns an entity render controller for a given entity type.
*
* @param string $entity_type
* The type of the entity.
*
* @return Drupal\Core\Entity\EntityRenderControllerInterface
* An entity render controller.
*
* @see hook_entity_info()
*/
function entity_render_controller($entity_type) {
$info = entity_get_info($entity_type);
$class = $info['render controller class'];
return new $class($entity_type);
}
/**
* Returns the render array for an entity.
*
* @param Drupal\Core\Entity\EntityInterface $entity
* The entity to be rendered.
* @param string $view_mode
* The view mode that should be used to display the entity.
* @param string $langcode
* (optional) For which language the entity should be rendered, defaults to
* the current content language.
*
* @return array
* A render array for the entity.
*/
function entity_view(EntityInterface $entity, $view_mode, $langcode = NULL) {
return entity_render_controller($entity->entityType())->view($entity, $view_mode, $langcode);
}
/**
* Returns the render array for the provided entities.
*
* @param array $entities
* The entities to be rendered, must be of the same type.
* @param string $view_mode
* The view mode that should be used to display the entity.
* @param string $langcode
* (optional) For which language the entity should be rendered, defaults to
* the current content language.
*
* @return array
* A render array for the entities, indexed by the same keys as the
* entities array passed in $entities.
*/
function entity_view_multiple(array $entities, $view_mode, $langcode = NULL) {
return entity_render_controller(reset($entities)->entityType())->viewMultiple($entities, $view_mode, $langcode);
}
<?php
/**
* @file
* Definition of Drupal\Core\Entity\EntityRenderController.
*/
namespace Drupal\Core\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;
}
/**
* Implements Drupal\Core\Entity\EntityRenderControllerInterface::buildContent().
*/
public function buildContent(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
// Allow modules to change the view mode.
$context = array('langcode' => $langcode);
$prepare = array();
foreach ($entities as $key => $entity) {
// Remove previously built content, if exists.
$entity->content = array();
drupal_alter('entity_view_mode', $view_mode, $entity, $context);
$entity->content['#view_mode'] = $view_mode;
$prepare[$view_mode][$key] = $entity;
}
// Prepare and build field content, grouped by view mode.
foreach ($prepare as $view_mode => $prepare_entities) {
$call = array();
// To ensure hooks are only run once per entity, check for an
// entity_view_prepared flag and only process items without it.
foreach ($prepare_entities as $entity) {
if (empty($entity->entity_view_prepared)) {
// Add this entity to the items to be prepared.
$call[$entity->id()] = $entity;
// Mark this item as prepared.
$entity->entity_view_prepared = TRUE;
}
}
if (!empty($call)) {
field_attach_prepare_view($this->entityType, $call, $view_mode, $langcode);
module_invoke_all('entity_prepare_view', $call, $this->entityType);
}
foreach ($entities as $entity) {
$entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
}
}
}
/**
* Provides entity-specific defaults to the build process.
*
* @param Drupal\Core\Entity\EntityInterface $entity
* The entity for which the defaults should be provided.
* @param string $view_mode
* The view mode that should be used.
* @param string $langcode
* (optional) For which language the entity should be prepared, defaults to
* the current content language.
*
* @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.
*
* @param array $build
* The render array that is being created.
* @param Drupal\Core\Entity\EntityInterface $entity
* The entity to be prepared.
* @param string $view_mode
* The view mode that should be used to prepare the entity.
* @param string $langcode
* (optional) For which language the entity should be prepared, defaults to
* the current content language.
*/
protected function alterBuild(array &$build, EntityInterface $entity, $view_mode, $langcode = NULL) { }
/**
* Implements Drupal\Core\Entity\EntityRenderControllerInterface::view().
*/
public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
$buildList = $this->viewMultiple(array($entity), $view_mode, $langcode);
return $buildList[0];
}
/**
* Implements Drupal\Core\Entity\EntityRenderControllerInterface::viewMultiple().
*/
public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
if (!isset($langcode)) {
$langcode = language(LANGUAGE_TYPE_CONTENT)->langcode;
}
$this->buildContent($entities, $view_mode, $langcode);
$view_hook = "{$this->entityType}_view";
$build = array('#sorted' => TRUE);
$weight = 0;
foreach ($entities as $key => $entity) {
$entity_view_mode = isset($entity->content['#view_mode']) ? $entity->content['#view_mode'] : $view_mode;
module_invoke_all($view_hook, $entity, $entity_view_mode, $langcode);
module_invoke_all('entity_view', $entity, $entity_view_mode, $langcode);
$build[$key] = $entity->content;
// We don't need duplicate rendering info in $entity->content.
unset($entity->content);
$build[$key] += $this->getBuildDefaults($entity, $entity_view_mode, $langcode);
$this->alterBuild($build[$key], $entity, $entity_view_mode, $langcode);
$build[$key]['#weight'] = $weight++;
// Allow modules to modify the structured entity.
drupal_alter(array($view_hook, 'entity_view'), $build[$key], $entity);
}
return $build;
}
}
<?php
/**
* @file
* Definition of Drupal\Core\Entity\EntityRenderControllerInterface.
*/
namespace Drupal\Core\Entity;
/**
* Defines a common interface for entity view controller classes.
*/
interface EntityRenderControllerInterface {
/**
* Build the structured $content property on the entity.
*
* @param array $entities
* The entities, implementing EntityInterface, whose content is being built.
* @param string $view_mode
* (optional) The view mode that should be used to build the entity.
* @param string $langcode
* (optional) For which language the entity should be build, defaults to
* the current content language.
*
* @return array
* The content array.
*/
public function buildContent(array $entities = array(), $view_mode = 'full', $langcode = NULL);
/**
* Returns the render array for the provided entity.
*
* @param Drupal\Core\Entity\EntityInterface $entity
* The entity to render.
* @param string $view_mode
* (optional) The view mode that should be used to render the entity.
* @param string $langcode
* (optional) For which language the entity should be rendered, defaults to
* the current content language.
*
* @return array
* A render array for 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);
/**
* Returns the render array for the provided entities.
*
* @param array $entities
* An array of entities implementing EntityInterface to view.
* @param string $view_mode
* (optional) The view mode that should be used to render the entity.
* @param string $langcode
* (optional) For which language the entity should be rendered, defaults to
* the current content language.
*
* @return
* A render array for the entities, indexed by the same keys as the
* entities array passed in $entities.
*
* @throws \InvalidArgumentException
* Can be thrown when the set of parameters is inconsistent, like when
* trying to view Comments and passing a Node which is not the one the
* comments belongs to, or not passing one, and having the comments node not
* be available for loading.
*/
public function viewMultiple(array $entities = array(), $view_mode = 'full', $langcode = NULL);
}
......@@ -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'),
......@@ -768,7 +769,7 @@ function comment_node_page_additions(Node $node) {
if ($cids = comment_get_thread($node, $mode, $comments_per_page)) {
$comments = comment_load_multiple($cids);
comment_prepare_thread($comments);
$build = comment_view_multiple($comments, $node);
$build = comment_view_multiple($comments);
$build['pager']['#theme'] = 'pager';
$additions['comments'] = $build;
}
......@@ -968,8 +969,6 @@ function comment_prepare_thread(&$comments) {
*
* @param Drupal\comment\Comment $comment
* The 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
......@@ -979,105 +978,8 @@ function comment_prepare_thread(&$comments) {
* @return
* 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);
function comment_view(Comment $comment, $view_mode = 'full', $langcode = NULL) {
return entity_view($comment, $view_mode, $langcode);
}
/**
......@@ -1146,12 +1048,8 @@ function comment_links(Comment $comment, Node $node) {
*
* @param $comments
* An array of comments as returned by comment_load_multiple().
* @param Drupal\node\Node $node
* The node the comments are attached to.
* @param $view_mode
* View mode, e.g. 'full', 'teaser'...
* @param $weight
* An integer representing the weight of the first comment in the list.
* @param $langcode
* A string indicating the language field values are to be shown in. If no
* language is provided the current content language is used.
......@@ -1161,19 +1059,8 @@ function comment_links(Comment $comment, Node $node) {
*
* @see drupal_render()
*/
function comment_view_multiple($comments, Node $node, $view_mode = 'full', $weight = 0, $langcode = NULL) {
field_attach_prepare_view('comment', $comments, $view_mode, $langcode);
entity_prepare_view('comment', $comments, $langcode);
$build = array(
'#sorted' => TRUE,
);
foreach ($comments as $comment) {
$build[$comment->cid] = comment_view($comment, $node, $view_mode, $langcode);
$build[$comment->cid]['#weight'] = $weight;
$weight++;
}
return $build;
function comment_view_multiple($comments, $view_mode = 'full', $langcode = NULL) {
return entity_view_multiple($comments, $view_mode, $langcode);
}
/**
......@@ -1411,7 +1298,7 @@ function comment_node_update_index(Node $node, $langcode) {
if ($node->comment && $cids = comment_get_thread($node, $mode, $comments_per_page)) {
$comments = comment_load_multiple($cids);
comment_prepare_thread($comments);
$build = comment_view_multiple($comments, $node, $langcode);
$build = comment_view_multiple($comments, $langcode);
return drupal_render($build);
}
}
......@@ -1639,7 +1526,7 @@ function comment_get_display_ordinal($cid, $node_type) {
else {
// For threaded comments, the c.thread column is used for ordering. We can
// use the sorting code for comparison, but must remove the trailing slash.
// See comment_view_multiple().
// See CommentRenderController.
$query->where('SUBSTRING(c1.thread, 1, (LENGTH(c1.thread) -1)) < SUBSTRING(c2.thread, 1, (LENGTH(c2.thread) -1))');
}
......@@ -1687,7 +1574,6 @@ function comment_edit_page(Comment $comment) {
function comment_preview(Comment $comment) {
global $user;
$preview_build = array();
$node = node_load($comment->nid);
if (!form_get_errors()) {
$comment_body = field_get_items('comment', $comment, 'comment_body');
......@@ -1714,7 +1600,7 @@ function comment_preview(Comment $comment) {
$comment->created = !empty($comment->created) ? $comment->created : REQUEST_TIME;
$comment->changed = REQUEST_TIME;
$comment->in_preview = TRUE;
$comment_build = comment_view($comment, $node);
$comment_build = comment_view($comment);
$comment_build['#weight'] = -100;
$preview_build['comment_preview'] = $comment_build;
......@@ -1724,11 +1610,11 @@ function comment_preview(Comment $comment) {
$build = array();
$comment = comment_load($comment->pid);
if ($comment && $comment->status == COMMENT_PUBLISHED) {
$build = comment_view($comment, $node);
$build = comment_view($comment);
}
}
else {
$build = node_view($node);
$build = node_view(node_load($comment->nid));
}
$preview_build['comment_output_below'] = $build;
......
......@@ -68,7 +68,7 @@ function comment_reply(Node $node, $pid = NULL) {
$comment->node_type = 'comment_node_' . $node->type;
field_attach_load('comment', array($comment->cid => $comment));
$comment->name = $comment->uid ? $comment->registered_name : $comment->name;
$build['comment_parent'] = comment_view($comment, $node);
$build['comment_parent'] = comment_view($comment);
}
else {
drupal_set_message(t('The comment you are replying to does not exist.'), 'error');
......
......@@ -36,6 +36,11 @@ class Comment extends Entity implements ContentEntityInterface {
*/
public $pid;
/**
* The ID of the node to which the comment is attached.
*/
public $nid;
/**
* The comment language code.
*
......
<?php
/**
* @file
* Definition of Drupal\comment\CommentRenderController.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRenderController;
/**
* Render controller for comments.
*/
class CommentRenderController extends EntityRenderController {
/**
* Overrides Drupal\Core\Entity\EntityRenderController::buildContent().
*
* In addition to modifying the content key on entities, this implementation
* will also set the node key which all comments carry.
*/
public function buildContent(array $entities = array(), $view_mode = 'full', $langcode = NULL) {
$return = array();
if (empty($entities)) {
return $return;
}
parent::buildContent($entities, $view_mode, $langcode);
foreach ($entities as $entity) {
$node = node_load($entity->nid);
if (!$node) {
throw new \InvalidArgumentException(t('Invalid node for comment.'));
}
$entity->content['#node'] = $node;
$entity->content['#theme'] = 'comment__node_' . $node->bundle();
$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, $node),
'#attributes' => array('class' => array('links', 'inline')),
);
}
}
}
/**
* Overrides Drupal\Core\Entity\EntityRenderController::alterBuild().
*/
protected function alterBuild(array &$build, EntityInterface $comment, $view_mode, $langcode = NULL) {
parent::alterBuild($build, $comment, $view_mode, $langcode);
if (empty($comment->in_preview)) {
$prefix = '';
$is_threaded = isset($comment->divs)
&& variable_get('comment_default_mode_' . $comment->bundle(), 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);
}
}
}
}
......@@ -39,7 +39,7 @@ function testCommentRebuild() {
// Add the property to the content array and then see if it still exists on build.
$comment_loaded->content['test_property'] = array('#value' => $this->randomString());
$built_content = comment_view($comment_loaded, $this->node);
$built_content = comment_view($comment_loaded);
// This means that the content was rebuilt as the added test property no longer exists.
$this->assertFalse(isset($built_content['test_property']), 'Comment content was emptied before being built.');
......
......@@ -304,7 +304,6 @@ function field_language($entity_type, $entity, $field_name = NULL, $langcode = N
$id = $entity->id();
$bundle = $entity->bundle();
$langcode = field_valid_language($langcode, FALSE);
if (!isset($display_langcodes[$entity_type][$id][$langcode])) {
$display_langcode = array();
......
......@@ -100,6 +100,12 @@ function file_entity_info() {
'label' => 'filename',
'uuid' => 'uuid',
),
'view modes' => array(
'full' => array(
'label' => t('File default'),
'custom settings' => FALSE,
),
),
'static cache' => FALSE,
),
);
......
<?php
/**
* @file
* Definition of Drupal\node\NodeRenderController.
*/
namespace Drupal\node;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRenderController;
/**
* Render controller for nodes.
*/