Commit 22883148 authored by catch's avatar catch

Issue #1852966 by yched, Stalski, zuuperman, swentel: Rework entity display...

Issue #1852966 by yched, Stalski, zuuperman, swentel: Rework entity display settings around EntityDisplay config entity.
parent 53de7398
......@@ -278,6 +278,29 @@ function hook_entity_view_mode_alter(&$view_mode, Drupal\Core\Entity\EntityInter
}
}
/**
* Alters the settings used for displaying an entity.
*
* @param \Drupal\entity\Plugin\Core\Entity\EntityDisplay $display
* The entity_display object that will be used to display the entity
* components.
* @param array $context
* An associative array containing:
* - entity_type: The entity type, e.g., 'node' or 'user'.
* - bundle: The bundle, e.g., 'page' or 'article'.
* - view_mode: The view mode, e.g. 'full', 'teaser'...
*/
function hook_entity_display_alter(Drupal\field\Plugin\Core\Entity\EntityDisplay $display, array $context) {
// Leave field labels out of the search index.
if ($context['entity_type'] == 'node' && $context['view_mode'] == 'search_index') {
foreach ($display->content as $name => &$properties) {
if (isset($properties['label'])) {
$properties['label'] = 'hidden';
}
}
}
}
/**
* Define custom entity properties.
*
......
......@@ -563,6 +563,128 @@ function entity_view_multiple(array $entities, $view_mode, $langcode = NULL) {
return entity_render_controller(reset($entities)->entityType())->viewMultiple($entities, $view_mode, $langcode);
}
/**
* Returns the entity_display object associated to a bundle and view mode.
*
* Use this function when assigning suggested display options for a component
* in a given view mode. Note that they will only be actually used at render
* time if the view mode itself is configured to use dedicated display settings
* for the bundle; if not, the 'default' display is used instead.
*
* The function reads the entity_display object from the current configuration,
* or returns a ready-to-use empty one if configuration entry exists yet for
* this bundle and view mode. This streamlines manipulation of display objects
* by always returning a consistent object that reflects the current state of
* the configuration.
*
* Example usage:
* - Set the 'body' field to be displayed and the 'field_image' field to be
* hidden on article nodes in the 'default' display.
* @code
* entity_get_display('article', 'node', 'default')
* ->setComponent('body', array(
* 'type' => 'text_summary_or_trimmed',
* 'settings' => array('trim_length' => '200')
* 'weight' => 1,
* ))
* ->removeComponent('field_image')
* ->save();
* @endcode
*
* @param string $entity_type
* The entity type.
* @param string $bundle
* The bundle.
* @param string $view_mode
* The view mode, or 'default' to retrieve the 'default' display object for
* this bundle.
*
* @return \Drupal\entity\Plugin\Core\Entity\EntityDisplay
* The display object associated to the view mode.
*/
function entity_get_display($entity_type, $bundle, $view_mode) {
// Try loading the display from configuration.
$display = entity_load('entity_display', $entity_type . '.' . $bundle . '.' . $view_mode);
// If not found, create a fresh display object. We do not preemptively create
// new entity_display configuration entries for each existing entity type and
// bundle whenever a new view mode becomes available. Instead, configuration
// entries are only created when a display object is explicitly configured
// and saved.
if (!$display) {
$display = entity_create('entity_display', array(
'targetEntityType' => $entity_type,
'bundle' => $bundle,
'viewMode' => $view_mode,
));
}
return $display;
}
/**
* Returns the entity_display object used to render an entity.
*
* Depending on the configuration of the view mode for the bundle, this can be
* either the display object associated to the view mode, or the 'default'
* display.
*
* This function should only be used internally when rendering an entity. When
* assigning suggested display options for a component in a given view mode,
* entity_get_display() should be used instead, in order to avoid inadvertently
* modifying the output of other view modes that might happen to use the
* 'default' display too. Those options will then be effectively applied only
* if the view mode is configured to use them.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity being rendered.
* @param string $view_mode
* The view mode being rendered.
*
* @return \Drupal\entity\Plugin\Core\Entity\EntityDisplay
* The display object that should be used to render the entity.
*
* @see entity_get_render_display().
*/
function entity_get_render_display(EntityInterface $entity, $view_mode) {
$entity_type = $entity->entityType();
$bundle = $entity->bundle();
// Determine the display to use for rendering this entity. Depending on the
// configuration of the view mode for this bundle, this will be either the
// display associated to the view mode, or the 'default' display.
$view_mode_settings = field_view_mode_settings($entity_type, $bundle);
$render_view_mode = !empty($view_mode_settings[$view_mode]['custom_settings']) ? $view_mode : 'default';
$display = entity_get_display($entity_type, $bundle, $render_view_mode);
$display->originalViewMode = $view_mode;
return $display;
}
/**
* Adjusts weights and visibility of components in displayed entities.
*
* This is used as a #pre_render callback.
*/
function _entity_view_pre_render($elements) {
$display = $elements['#entity_display'];
$extra_fields = field_info_extra_fields($display->targetEntityType, $display->bundle, 'display');
foreach (array_keys($extra_fields) as $name) {
if (isset($elements[$name]) && (!isset($elements[$name]['#access']) || $elements[$name]['#access'])) {
if ($options = $display->getComponent($name)) {
$elements[$name]['#weight'] = $options['weight'];
}
else {
$elements[$name]['#access'] = FALSE;
}
}
}
return $elements;
}
/**
* Returns the entity query object for this entity type.
*
......
......@@ -30,37 +30,64 @@ public function buildContent(array $entities = array(), $view_mode = 'full', $la
// Allow modules to change the view mode.
$context = array('langcode' => $langcode);
$prepare = array();
foreach ($entities as $key => $entity) {
$view_modes = array();
$displays = array();
foreach ($entities as $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;
$view_modes[$view_mode][$entity->id()] = $entity;
$bundle = $entity->bundle();
// Load the corresponding display settings if not stored yet.
if (!isset($displays[$view_mode][$bundle])) {
// Get the display object to use for rendering the entity..
$display = entity_get_render_display($entity, $view_mode);
// Let modules alter the display.
// Note: if config entities get a static cache at some point, the
// objects should be cloned before running drupal_alter().
$display_context = array(
'entity_type' => $this->entityType,
'bundle' => $bundle,
'view_mode' => $view_mode,
);
drupal_alter('entity_display', $display, $display_context);
$displays[$view_mode][$bundle] = $display;
}
// Assigning weights to 'extra fields' is done in a pre_render callback.
$entity->content['#pre_render'] = array('_entity_view_pre_render');
$entity->content['#entity_display'] = $displays[$view_mode][$bundle];
}
// Prepare and build field content, grouped by view mode.
foreach ($prepare as $view_mode => $prepare_entities) {
$call = array();
foreach ($view_modes as $view_mode => $view_mode_entities) {
$call_prepare = 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)) {
// entity_view_prepared flag and only process relevant entities.
foreach ($view_mode_entities as $entity) {
if (empty($entity->entity_view_prepared) || $entity->entity_view_prepared != $view_mode) {
// Add this entity to the items to be prepared.
$call[$entity->id()] = $entity;
$call_prepare[$entity->id()] = $entity;
// Mark this item as prepared.
$entity->entity_view_prepared = TRUE;
// Mark this item as prepared for this view mode.
$entity->entity_view_prepared = $view_mode;
}
}
if (!empty($call)) {
field_attach_prepare_view($this->entityType, $call, $view_mode, $langcode);
module_invoke_all('entity_prepare_view', $call, $this->entityType);
if (!empty($call_prepare)) {
field_attach_prepare_view($this->entityType, $call_prepare, $displays[$view_mode], $langcode);
module_invoke_all('entity_prepare_view', $call_prepare, $this->entityType);
}
foreach ($entities as $entity) {
$entity->content += field_attach_view($this->entityType, $entity, $view_mode, $langcode);
foreach ($view_mode_entities as $entity) {
$entity->content += field_attach_view($this->entityType, $entity, $displays[$view_mode][$entity->bundle()], $langcode);
}
}
}
......
......@@ -376,15 +376,15 @@ function _comment_body_field_create($info) {
'bundle' => 'comment_node_' . $info->type,
'settings' => array('text_processing' => 1),
'required' => TRUE,
'display' => array(
'default' => array(
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
),
),
);
field_create_instance($instance);
entity_get_display('comment', 'comment_node_' . $info->type, 'default')
->setComponent('comment_body', array(
'label' => 'hidden',
'type' => 'text_default',
'weight' => 0,
))
->save();
}
}
......
......@@ -23,9 +23,8 @@ public function buildContent(array $entities = array(), $view_mode = 'full', $la
foreach ($entities as $entity) {
// Add the message extra field, if enabled.
$bundle = $entity->bundle();
$entity_view_mode = $entity->content['#view_mode'];
$fields = field_extra_fields_get_display($this->entityType, $bundle, $entity_view_mode);
$fields = field_extra_fields_get_display($entity, $entity_view_mode);
if (!empty($entity->message) && !empty($fields['message']['visible'])) {
$entity->content['message'] = array(
'#type' => 'item',
......
......@@ -29,7 +29,7 @@ class EditorSelectionTest extends DrupalUnitTestBase {
*
* @var array
*/
public static $modules = array('system', 'field_test', 'field', 'number', 'text', 'edit', 'edit_test');
public static $modules = array('system', 'entity', 'field_test', 'field', 'number', 'text', 'edit', 'edit_test');
public static function getInfo() {
return array(
......@@ -103,24 +103,26 @@ function createFieldWithInstance($field_name, $type, $cardinality, $label, $inst
'label' => $label,
'settings' => $widget_settings,
),
'display' => array(
'default' => array(
'label' => 'above',
'type' => $formatter_type,
'settings' => $formatter_settings
),
),
);
field_create_instance($this->$instance);
entity_get_display('test_entity', 'test_bundle', 'default')
->setComponent($field_name, array(
'label' => 'above',
'type' => $formatter_type,
'settings' => $formatter_settings
))
->save();
}
/**
* Retrieves the FieldInstance object for the given field and returns the
* editor that Edit selects.
*/
function getSelectedEditor($items, $field_name, $display = 'default') {
function getSelectedEditor($items, $field_name, $view_mode = 'default') {
$options = entity_get_display('test_entity', 'test_bundle', $view_mode)->getComponent($field_name);
$field_instance = field_info_instance('test_entity', $field_name, 'test_bundle');
return $this->editorSelector->getEditor($field_instance['display'][$display]['type'], $field_instance, $items);
return $this->editorSelector->getEditor($options['type'], $field_instance, $items);
}
/**
......
......@@ -60,13 +60,14 @@ function testEmailField() {
'placeholder' => 'example@example.com',
),
),
'display' => array(
'full' => array(
'type' => 'email_mailto',
),
),
);
field_create_instance($this->instance);
// Create a display for the full view mode.
entity_get_display('test_entity', 'test_bundle', 'full')
->setComponent($this->field['field_name'], array(
'type' => 'email_mailto',
))
->save();
// Display creation form.
$this->drupalGet('test-entity/add/test_bundle');
......@@ -87,7 +88,8 @@ function testEmailField() {
// Verify that a mailto link is displayed.
$entity = field_test_entity_test_load($id);
$entity->content = field_attach_view('test_entity', $entity, 'full');
$display = entity_get_display($entity->entityType(), $entity->bundle(), 'full');
$entity->content = field_attach_view('test_entity', $entity, $display);
$this->drupalSetContent(drupal_render($entity->content));
$this->assertLinkByHref('mailto:test@example.com');
}
......
<?php
/**
* @file
* Install, update and uninstall functions for the entity module.
*/
use Drupal\Component\Uuid\Uuid;
/**
* Returns the raw configuration object for an EntityDisplay entity.
*
* The function returns the existing configuration entry if it exists, or
* creates a fresh structure.
*
* @param string $entity_type
* The entity type.
* @param string $bundle
* The bundle name.
* @param string $view_mode
* The view mode.
*
* @return \Drupal\Core\Config\Config
* The configuration object.
*/
function _update_8000_entity_get_display($entity_type, $bundle, $view_mode) {
$id = $entity_type . '.' . $bundle . '.' . $view_mode;
$config = config("entity.display.$id");
if ($config->get()) {
return $config;
}
// Initialize a fresh structure.
$uuid = new Uuid();
$properties = array(
'id' => $id,
'uuid' => $uuid->generate(),
'targetEntityType' => $entity_type,
'bundle' => $bundle,
'viewMode' => $view_mode,
'content' => array(),
);
foreach ($properties as $key => $value) {
$config->set($key, $value);
}
return $config;
}
<?php
/**
* @file
* Contains \Drupal\entity\Plugin\Core\Entity\EntityDisplay.
*/
namespace Drupal\entity\Plugin\Core\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Annotation\Plugin;
use Drupal\Core\Annotation\Translation;
/**
* Configuration entity that contains display options for all components of a
* rendered entity in a given view mode..
*
* @Plugin(
* id = "entity_display",
* label = @Translation("Entity display"),
* module = "entity",
* controller_class = "Drupal\Core\Config\Entity\ConfigStorageController",
* config_prefix = "entity.display",
* entity_keys = {
* "id" = "id",
* "uuid" = "uuid"
* }
* )
*/
class EntityDisplay extends ConfigEntityBase {
/**
* Unique ID for the config entity.
*
* @var string
*/
public $id;
/**
* Unique UUID for the config entity.
*
* @var string
*/
public $uuid;
/**
* Entity type to be displayed.
*
* @var string
*/
public $targetEntityType;
/**
* Bundle to be displayed.
*
* @var string
*/
public $bundle;
/**
* View mode to be displayed.
*
* @var string
*/
public $viewMode;
/**
* List of component display options, keyed by component name.
*
* @var array
*/
protected $content = array();
/**
* The original view mode that was requested (case of view modes being
* configured to fall back to the 'default' display).
*
* @var string
*/
public $originalViewMode;
/**
* The formatter objects used for this display, keyed by field name.
*
* @var array
*/
protected $formatters = array();
/**
* Overrides \Drupal\Core\Config\Entity\ConfigEntityBase::__construct().
*/
public function __construct(array $values, $entity_type) {
// @todo See http://drupal.org/node/1825044#comment-6847792: contact.module
// currently produces invalid entities with a NULL bundle in some cases.
// Add the validity checks back when http://drupal.org/node/1856556 is
// fixed.
// if (!isset($values['targetEntityType']) || !isset($values['bundle']) || !isset($values['viewMode'])) {
// throw new \InvalidArgumentException('Missing required properties for an EntiyDisplay entity.');
// }
parent::__construct($values, $entity_type);
$this->originalViewMode = $this->viewMode;
}
/**
* Overrides \Drupal\Core\Entity\Entity::id().
*/
public function id() {
return $this->targetEntityType . '.' . $this->bundle . '.' . $this->viewMode;
}
/**
* Overrides \Drupal\config\ConfigEntityBase::save().
*/
public function save() {
// Build an ID if none is set.
if (empty($this->id)) {
$this->id = $this->id();
}
return parent::save();
}
/**
* Overrides \Drupal\config\ConfigEntityBase::getExportProperties();
*/
public function getExportProperties() {
$names = array(
'id',
'uuid',
'targetEntityType',
'bundle',
'viewMode',
'content',
);
$properties = array();
foreach ($names as $name) {
$properties[$name] = $this->get($name);
}
return $properties;
}
/**
* Creates a duplicate of the EntityDisplay object on a different view mode.
*
* The new object necessarily has the same $targetEntityType and $bundle
* properties than the original one.
*
* @param $view_mode
* The view mode for the new object.
*
* @return \Drupal\entity\Plugin\Core\Entity\EntityDisplay
* The new object.
*/
public function createCopy($view_mode) {
$display = $this->createDuplicate();
$display->viewMode = $display->originalViewMode = $view_mode;
return $display;
}
/**
* Gets the display options for all components.
*
* @return array
* The array of display options, keyed by component name.
*/
public function getComponents() {
$result = array();
foreach ($this->content as $name => $options) {
if (!isset($options['visible']) || $options['visible'] === TRUE) {
unset($options['visible']);
$result[$name] = $options;
}
}
return $result;
}
/**
* Gets the display options set for a component.
*
* @param string $name
* The name of the component.
*
* @return array|null
* The display options for the component, or NULL if the component is not
* displayed.
*/
public function getComponent($name) {
// We always store 'extra fields', whether they are visible or hidden.
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, 'display');
if (isset($extra_fields[$name])) {
// If we have explicit settings, return an array or NULL depending on
// visibility.
if (isset($this->content[$name])) {
if ($this->content[$name]['visible']) {
return array(
'weight' => $this->content[$name]['weight'],
);
}
else {
return NULL;
}
}
// If no explicit settings for the extra field, look at the default
// visibility in its definition.
$definition = $extra_fields[$name];
if (!isset($definition['visible']) || $definition['visible'] == TRUE) {
return array(
'weight' => $definition['weight']
);
}
else {
return NULL;
}
}
if (isset($this->content[$name])) {
return $this->content[$name];
}
}
/**
* Sets the display options for a component.
*
* @param string $name
* The name of the component.
* @param array $options
* The display options.
*
* @return \Drupal\entity\Plugin\Core\Entity\EntityDisplay
* The EntityDisplay object.
*/
public function setComponent($name, array $options = array()) {
// If no weight specified, make sure the field sinks at the bottom.
if (!isset($options['weight'])) {
$max = $this->getHighestWeight();
$options['weight'] = isset($max) ? $max + 1 : 0;
}
if ($instance = field_info_instance($this->targetEntityType, $name, $this->bundle)) {
$field = field_info_field($instance['field_name']);
$options = drupal_container()->get('plugin.manager.field.formatter')->prepareConfiguration($field['type'], $options);
// Clear the persisted formatter, if any.
unset($this->formatters[$name]);
}
// We always store 'extra fields', whether they are visible or hidden.
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, 'display');
if (isset($extra_fields[$name])) {
$options['visible'] = TRUE;
}
$this->content[$name] = $options;
return $this;
}
/**
* Sets a component to be hidden.
*
* @param string $name
* The name of the component.
*
* @return \Drupal\entity\Plugin\Core\Entity\EntityDisplay
* The EntityDisplay object.
*/
public function removeComponent($name) {
$extra_fields = field_info_extra_fields($this->targetEntityType, $this->bundle, 'display');
if (isset($extra_fields[$name])) {
// 'Extra fields' are exposed in hooks and can appear at any given time.
// Therefore we store extra fields that are explicitly being hidden, so
// that we can differenciate with those that are simply not configured
// yet.
$this->content[$name] = array(
'visible' => FALSE,
);
}
else {
unset($this->content[$name]);
unset($this->formatters[$name]);
}
return $this;
}