Commit 7dcaa395 authored by catch's avatar catch

Issue #2167267 by yched, andypost: Remove deprecated field_attach_*_view().

parent f5ba64c9
......@@ -7,9 +7,48 @@
namespace Drupal\Core\Entity\Display;
use Drupal\Core\Entity\ContentEntityInterface;
/**
* Provides a common interface for entity view displays.
*/
interface EntityViewDisplayInterface extends EntityDisplayInterface {
/**
* Returns a renderable array for the components of an entity.
*
* See the buildMultiple() method for details.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity being displayed.
*
* @return array
* A renderable array for the entity.
*
* @see \Drupal\Core\Entity\Display\EntityViewDisplayInterface::buildMultiple()
*/
public function build(ContentEntityInterface $entity);
/**
* Returns a renderable array for the components of a set of entities.
*
* This only includes the components handled by the Display object, but
* excludes 'extra fields', that are typically rendered through specific,
* ad-hoc code in EntityViewBuilderInterface::buildContent() or in
* hook_entity_view() implementations.
*
* hook_entity_display_build_alter() is invoked on each entity, allowing 3rd
* party code to alter the render array.
*
* @param \Drupal\Core\Entity\ContentEntityInterface[] $entities
* The entities being displayed.
*
* @return array
* A renderable array for the entities, indexed by the same keys as the
* $entities array parameter.
*
* @see hook_entity_display_build_alter()
*/
public function buildMultiple(array $entities);
}
......@@ -89,16 +89,18 @@ public static function createInstance(ContainerInterface $container, EntityTypeI
* {@inheritdoc}
*/
public function buildContent(array $entities, array $displays, $view_mode, $langcode = NULL) {
field_attach_prepare_view($this->entityTypeId, $entities, $displays, $langcode);
// Initialize the field item attributes for the fields set to be displayed.
foreach ($entities as $entity) {
// The entity can include fields that aren't displayed, and the display
// can include components that aren't fields, so we want to iterate the
// intersection of $entity->getProperties() and $display->getComponents().
// However, the entity can have many more fields than are displayed, so we
// avoid the cost of calling $entity->getProperties() by iterating the
// intersection as follows.
$entities_by_bundle = array();
foreach ($entities as $id => $entity) {
// Remove previously built content, if exists.
$entity->content = array(
'#view_mode' => $view_mode,
);
// Initialize the field item attributes for the fields being displayed.
// The entity can include fields that are not displayed, and the display
// can include components that are not fields, so we want to act on the
// intersection. However, the entity can have many more fields than are
// displayed, so we avoid the cost of calling $entity->getProperties()
// by iterating the intersection as follows.
foreach ($displays[$entity->bundle()]->getComponents() as $name => $options) {
if ($entity->hasField($name)) {
foreach ($entity->get($name) as $item) {
......@@ -106,16 +108,19 @@ public function buildContent(array $entities, array $displays, $view_mode, $lang
}
}
}
// Group the entities by bundle.
$entities_by_bundle[$entity->bundle()][$id] = $entity;
}
// Invoke hook_entity_prepare_view().
module_invoke_all('entity_prepare_view', $this->entityTypeId, $entities, $displays, $view_mode);
foreach ($entities as $entity) {
// Remove previously built content, if exists.
$entity->content = array(
'#view_mode' => $view_mode,
);
$entity->content += field_attach_view($entity, $displays[$entity->bundle()], $langcode);
// Let the displays build their render arrays.
foreach ($entities_by_bundle as $bundle => $bundle_entities) {
$build = $displays[$bundle]->buildMultiple($bundle_entities);
foreach ($bundle_entities as $id => $entity) {
$entity->content += $build[$id];
}
}
}
......@@ -231,6 +236,9 @@ public function viewMultiple(array $entities = array(), $view_mode = 'full', $la
$this->alterBuild($build[$key], $entity, $display, $entity_view_mode, $langcode);
// Assign the weights configured in the display.
// @todo: Once https://drupal.org/node/1875974 provides the missing API,
// only do it for 'extra fields', since other components have been taken
// care of in EntityViewDisplay::buildMultiple().
foreach ($display->getComponents() as $name => $options) {
if (isset($build[$key][$name])) {
$build[$key][$name]['#weight'] = $options['weight'];
......
......@@ -15,9 +15,9 @@ interface EntityViewBuilderInterface {
/**
* Build the structured $content property on the entity.
*
* @param array $entities
* The entities, implementing EntityInterface, whose content is being built.
* @param \Drupal\Core\Entity\EntityViewDisplayInterface[] $displays
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* The entities whose content is being built.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface[] $displays
* The array of entity view displays holding the display options
* configured for the entity components, keyed by bundle name.
* @param string $view_mode
......
......@@ -12,9 +12,8 @@
/**
* Defines a FieldFormatter annotation object.
*
* Formatters handle the display of field values. Formatter hooks are typically
* called by the Field Attach API field_attach_prepare_view() and
* field_attach_view() functions.
* Formatters handle the display of field values. They are typically
* instantiated and invoked by an EntityDisplay object.
*
* Additional annotation keys for formatters can be defined in
* hook_field_formatter_info_alter().
......
......@@ -48,22 +48,20 @@ public function settingsSummary();
* field that displays properties of the referenced entities such as name or
* type.
*
* This method operates on multiple entities. The $entities and $items
* parameters are arrays keyed by entity ID. For performance reasons,
* information for all involved entities should be loaded in a single query
* where possible.
* This method operates on multiple entities. The $entities_items parameter
* is an array keyed by entity ID. For performance reasons, information for
* all involved entities should be loaded in a single query where possible.
*
* Changes or additions to field values are done by alterings the $items
* parameter by reference.
* Changes or additions to field values are done by directly altering the
* items.
*
* @param array $entities_items
* Array of field values (Drupal\Core\Field\FieldItemListInterface),
* keyed by entity ID.
* @param \Drupal\Core\Field\FieldItemListInterface[] $entities_items
* Array of field values, keyed by entity ID.
*/
public function prepareView(array $entities_items);
/**
* Builds a renderable array for one field on one entity instance.
* Builds a renderable array for a fully themed field.
*
* @param \Drupal\Core\Field\FieldItemListInterface $items
* The field values to be rendered.
......@@ -81,7 +79,7 @@ public function view(FieldItemListInterface $items);
*
* @return array
* A renderable array for $items, as an array of child elements keyed by
* numeric indexes starting from 0.
* consecutive numeric indexes starting from 0.
*/
public function viewElements(FieldItemListInterface $items);
......
......@@ -2,11 +2,12 @@
/**
* @file
* Contains \Drupal\datetime\Tests\DatetimeFieldTest.
* Contains \Drupal\datetime\Tests\DateTimeFieldTest.
*/
namespace Drupal\datetime\Tests;
use Drupal\entity\Entity\EntityViewDisplay;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Datetime\DrupalDateTime;
......@@ -455,10 +456,8 @@ protected function renderTestEntity($id, $view_mode = 'full', $reset = TRUE) {
entity_get_controller('entity_test')->resetCache(array($id));
}
$entity = entity_load('entity_test', $id);
$display = entity_get_display('entity_test', $entity->bundle(), 'full');
field_attach_prepare_view('entity_test', array($entity->id() => $entity), array($entity->bundle() => $display), $view_mode);
$entity->content = field_attach_view($entity, $display, $view_mode);
$display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
$entity->content = $display->build($entity);
$output = drupal_render($entity->content);
$this->drupalSetContent($output);
$this->verbose($output);
......
......@@ -111,8 +111,8 @@ function testEmailField() {
// Verify that a mailto link is displayed.
$entity = entity_load('entity_test', $id);
$display = entity_get_display($entity->getEntityTypeId(), $entity->bundle(), 'full');
$entity->content = field_attach_view($entity, $display);
$this->drupalSetContent(drupal_render($entity->content));
$content = $display->build($entity);
$this->drupalSetContent(drupal_render($content));
$this->assertLinkByHref('mailto:test@example.com');
}
}
......@@ -9,6 +9,7 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\entity\EntityDisplayBase;
/**
......@@ -197,4 +198,62 @@ public function getRenderer($field_name) {
return $formatter;
}
/**
* {@inheritdoc}
*/
public function build(ContentEntityInterface $entity) {
$build = $this->buildMultiple(array($entity));
return $build[0];
}
/**
* {@inheritdoc}
*/
public function buildMultiple(array $entities) {
$build = array();
foreach ($entities as $key => $entity) {
$build[$key] = array();
}
// Run field formatters.
foreach ($this->getFieldDefinitions() as $field_name => $definition) {
if ($formatter = $this->getRenderer($field_name)) {
// Group items across all entities and pass them to the formatter's
// prepareView() method.
$grouped_items = array();
foreach ($entities as $id => $entity) {
$items = $entity->get($field_name);
$items->filterEmptyItems();
$grouped_items[$id] = $items;
}
$formatter->prepareView($grouped_items);
// Then let the formatter build the output for each entity.
foreach ($entities as $key => $entity) {
$items = $entity->get($field_name);
$build[$key] += $formatter->view($items);
}
}
}
foreach ($entities as $key => $entity) {
// Assign the configured weights.
foreach ($this->getComponents() as $name => $options) {
if (isset($build[$key][$name])) {
$build[$key][$name]['#weight'] = $options['weight'];
}
}
// Let other modules alter the renderable array.
$context = array(
'entity' => $entity,
'view_mode' => $this->originalMode,
'display' => $this,
);
\Drupal::moduleHandler()->alter('entity_display_build', $build[$key], $context);
}
return $build;
}
}
......@@ -19,7 +19,7 @@
* should expose them using this hook. The user-defined settings (weight,
* visible) are automatically applied on rendered forms and displayed entities
* in a #pre_render callback added by field_attach_form() and
* field_attach_view().
* EntityViewBuilder::viewMultiple().
*
* @see hook_field_extra_fields_alter()
*
......@@ -357,46 +357,6 @@ function hook_field_attach_extract_form_values(\Drupal\Core\Entity\EntityInterfa
}
}
/**
* Perform alterations on field_attach_view() or field_view_field().
*
* This hook is invoked after the field module has performed the operation.
*
* @param $output
* The structured content array tree for all of the entity's fields.
* @param $context
* An associative array containing:
* - entity: The entity with fields to render.
* - view_mode: View mode; for example, 'full' or 'teaser'.
* - display_options: Either a view mode string or an array of display
* options. If this hook is being invoked from field_attach_view(), the
* 'display_options' element is set to the view mode string. If this hook
* is being invoked from field_view_field(), this element is set to the
* $display_options argument and the view_mode element is set to '_custom'.
* See field_view_field() for more information on what its $display_options
* argument contains.
* - langcode: The language code used for rendering.
*
* @deprecated as of Drupal 8.0. Use the entity system instead.
*/
function hook_field_attach_view_alter(&$output, $context) {
// Append RDF term mappings on displayed taxonomy links.
foreach (element_children($output) as $field_name) {
$element = &$output[$field_name];
if ($element['#field_type'] == 'entity_reference' && $element['#formatter'] == 'entity_reference_label') {
foreach ($element['#items'] as $delta => $item) {
$term = $item->entity;
if (!empty($term->rdf_mapping['rdftype'])) {
$element[$delta]['#options']['attributes']['typeof'] = $term->rdf_mapping['rdftype'];
}
if (!empty($term->rdf_mapping['name']['predicates'])) {
$element[$delta]['#options']['attributes']['property'] = $term->rdf_mapping['name']['predicates'];
}
}
}
}
}
/**
* @} End of "addtogroup field_attach".
*/
......
......@@ -7,7 +7,6 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\entity\Entity\EntityFormDisplay;
/**
* @defgroup field_attach Field Attach API
......@@ -47,12 +46,6 @@
* exposes a single bundle (all entities of this type have the same collection
* of fields). This is the case for the 'user' entity type.
*
* Most Field Attach API functions define a corresponding hook function that
* allows any module to act on Field Attach operations for any entity after the
* operation is complete, and access or modify all the field, form, or display
* data for that entity and operation. For example, field_attach_view() invokes
* hook_field_attach_view_alter().
*
* @link field_language Field language API @endlink provides information about
* the structure of field objects.
*
......@@ -119,96 +112,6 @@ function field_invoke_method($method, $target_function, EntityInterface $entity,
return $return;
}
/**
* Invokes a method across fields on multiple entities.
*
* @param string $method
* The name of the method to invoke.
* @param callable $target_function
* A function that receives a FieldDefinitionInterface object and a bundle
* name and returns the object on which the method should be invoked.
* @param \Drupal\Core\Entity\EntityInterface[] $entities
* An array of entities, keyed by entity ID.
* @param mixed $a
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param mixed $b
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param $options
* (optional) An associative array of additional options, with the following
* keys:
* - field_name: The name of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's
* bundle.
*
* @return array
* An array of returned values keyed by entity ID.
*
* @see field_invoke_method()
*/
function field_invoke_method_multiple($method, $target_function, array $entities, &$a = NULL, &$b = NULL, array $options = array()) {
$grouped_items = array();
$grouped_targets = array();
$return = array();
// Go through the entities and collect the instances on which the method
// should be called.
foreach ($entities as $entity) {
$entity_type = $entity->getEntityTypeId();
$bundle = $entity->bundle();
$id = $entity->id();
// Determine the list of fields to iterate on.
$field_definitions = _field_invoke_get_field_definitions($entity_type, $bundle, $options);
foreach ($field_definitions as $field_definition) {
$field_name = $field_definition->getName();
$group_key = "$bundle:$field_name";
// Let the closure determine the target object on which the method should
// be called.
if (empty($grouped_targets[$group_key])) {
$target = call_user_func($target_function, $field_definition, $bundle);
if (method_exists($target, $method)) {
$grouped_targets[$group_key] = $target;
}
else {
$grouped_targets[$group_key] = FALSE;
}
}
// If there is a target, group the field items.
if ($grouped_targets[$group_key]) {
$items = $entity->get($field_name);
$items->filterEmptyItems();
$grouped_items[$group_key][$id] = $items;
}
}
// Initialize the return value for each entity.
$return[$id] = array();
}
// For each field, invoke the method and collect results.
foreach ($grouped_items as $key => $entities_items) {
$results = $grouped_targets[$key]->$method($entities_items, $a, $b);
if (isset($results)) {
// Collect results by entity.
// For hooks with array results, we merge results together.
// For hooks with scalar results, we collect results in an array.
foreach ($results as $id => $result) {
if (is_array($result)) {
$return[$id] = array_merge($return[$id], $result);
}
else {
$return[$id][] = $result;
}
}
}
}
return $return;
}
/**
* Retrieves a list of field definitions to operate on.
*
......
......@@ -387,111 +387,3 @@ function field_attach_extract_form_values(EntityInterface $entity, $form, &$form
$function($entity, $form, $form_state);
}
}
/**
* Prepares field data prior to display.
*
* This function lets field types and formatters load additional data needed for
* display that is not automatically loaded during entity loading. It accepts an
* array of entities to allow query optimization when displaying lists of
* entities.
*
* field_attach_prepare_view() and field_attach_view() are two halves of the
* same operation. It is safe to call field_attach_prepare_view() multiple times
* on the same entity before calling field_attach_view() on it, but calling any
* Field API operation on an entity between passing that entity to these two
* functions may yield incorrect results.
*
* @param $entity_type
* The type of entities in $entities; e.g. 'node' or 'user'.
* @param array $entities
* An array of entities, keyed by entity ID.
* @param array $displays
* An array of entity display objects, keyed by bundle name.
* @param $langcode
* (Optional) The language the field values are to be shown in. If no language
* is provided the current language is used.
*
* @deprecated as of Drupal 8.0. Use the entity system instead.
*/
function field_attach_prepare_view($entity_type, array $entities, array $displays, $langcode = NULL) {
// To ensure hooks are only run once per entity, only process items without
// the _field_view_prepared flag.
// @todo: resolve this more generally for both entity and field level hooks.
$prepare = array();
foreach ($entities as $id => $entity) {
if (empty($entity->_field_view_prepared)) {
// Add this entity to the items to be prepared.
$prepare[$id] = $entity;
// Mark this item as prepared.
$entity->_field_view_prepared = TRUE;
}
}
// Then let the formatters do their own specific massaging. For each
// instance, call the prepareView() method on the formatter object handed by
// the entity display.
$target_function = function (FieldDefinitionInterface $field_definition, $bundle) use ($displays) {
if (isset($displays[$bundle])) {
return $displays[$bundle]->getRenderer($field_definition->getName());
}
};
$null = NULL;
field_invoke_method_multiple('prepareView', $target_function, $prepare, $null, $null);
}
/**
* Returns a renderable array for the fields on an entity.
*
* Each field is displayed according to the display options specified in the
* $display parameter for the given view mode.
*
* field_attach_prepare_view() and field_attach_view() are two halves of the
* same operation. It is safe to call field_attach_prepare_view() multiple times
* on the same entity before calling field_attach_view() on it, but calling any
* Field API operation on an entity between passing that entity to these two
* functions may yield incorrect results.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity with fields to render.
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
* The entity display object.
* @param $langcode
* The language the field values are to be shown in. If no language is
* provided the current language is used.
* @param array $options
* An associative array of additional options. See field_invoke_method() for
* details.
*
* @return array
* A renderable array for the field values.
*
* @deprecated as of Drupal 8.0. Use the entity system instead.
*/
function field_attach_view(EntityInterface $entity, EntityViewDisplayInterface $display, $langcode = NULL, array $options = array()) {
// For each field, call the view() method on the formatter object handed
// by the entity display.
$target_function = function (FieldDefinitionInterface $field_definition) use ($display) {
return $display->getRenderer($field_definition->getName());
};
$null = NULL;
$output = field_invoke_method('view', $target_function, $entity, $null, $null, $options);
// Let other modules alter the renderable array.
$view_mode = $display->originalMode;
$context = array(
'entity' => $entity,
'view_mode' => $view_mode,
'display_options' => $view_mode,
'langcode' => $langcode,
);
drupal_alter('field_attach_view', $output, $context);
// Reset the _field_view_prepared flag set in field_attach_prepare_view(),
// in case the same entity is displayed with different settings later in
// the request.
unset($entity->_field_view_prepared);
return $output;
}
......@@ -5,7 +5,6 @@
*/
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Template\Attribute;
use Drupal\entity\Entity\EntityViewDisplay;
......@@ -325,7 +324,7 @@ function _field_filter_xss_display_allowed_tags() {
/**
* Returns a renderable array for a single field value.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* @param \Drupal\Core\Entity\ContentEntityInterface $entity
* The entity containing the field to display. Must at least contain the ID
* key and the field data to display.
* @param $field_name
......@@ -342,10 +341,10 @@ function _field_filter_xss_display_allowed_tags() {
* @return
* A renderable array for the field value.
*/
function field_view_value(EntityInterface $entity, $field_name, $item, $display = array(), $langcode = NULL) {
function field_view_value(ContentEntityInterface $entity, $field_name, $item, $display = array(), $langcode = NULL) {
$output = array();
if ($field = field_info_field($entity->getEntityTypeId(), $field_name)) {
if ($entity->hasField($field_name)) {
// Clone the entity since we are going to modify field values.
$clone = clone $entity;
......@@ -376,8 +375,8 @@ function field_view_value(EntityInterface $entity, $field_name, $item, $display
* isolated field.
* - Do not use inside node (or any other entity) templates; use
* render($content[FIELD_NAME]) instead.
* - Do not use to display all fields in an entity; use
* field_attach_prepare_view() and field_attach_view() instead.
* - Do not use to display all fields in an entity; use EntityDisplay::build()
* instead.
* - The field_view_value() function can be used to output a single formatted
* field value, without label or wrapping field markup.
*
......@@ -424,49 +423,31 @@ function field_view_field(ContentEntityInterface $entity, $field_name, $display_
if (!$entity->hasField($field_name)) {
return $output;
}
$field_definition = $entity->get($field_name)->getFieldDefinition();
// Get the formatter object.
// Get the display object.
if (is_string($display_options)) {
$view_mode = $display_options;
$formatter = EntityViewDisplay::collectRenderDisplay($entity, $view_mode)->getRenderer($field_name);
$display = EntityViewDisplay::collectRenderDisplay($entity, $view_mode);
foreach ($entity as $name => $items) {
if ($name != $field_name) {
$display->removeComponent($name);
}
}
}
else {
$view_mode = '_custom';
// hook_field_attach_display_alter() needs to receive the 'prepared'
// $display_options, so we cannot let preparation happen internally.
$formatter_manager = Drupal::service('plugin.manager.field.formatter');
$display_options = $formatter_manager->prepareConfiguration($field_definition->getType(), $display_options);
$formatter = $formatter_manager->getInstance(array(
'field_definition' => $field_definition,
'view_mode' => $view_mode,
'prepare' => FALSE,
'configuration' => $display_options,
$display = entity_create('entity_view_display', array(
'targetEntityType' => $entity->getEntityTypeId(),
'bundle' => $entity->bundle(),
'mode' => $view_mode,
'status' => TRUE,
));
$display->setComponent($field_name, $display_options);
}
if ($formatter) {
// Apply language fallback.
$entity = \Drupal::entityManager()->getTranslationFromContext($entity, $langcode);
$items = $entity->get($field_name);
// Run the formatter.
$formatter->prepareView(array($entity->id() => $items));
$result = $formatter->view($items);
// Invoke hook_field_attach_view_alter() to let other modules alter the
// renderable array, as in a full field_attach_view() execution.
$context = array(
'entity' => $entity,
'view_mode' => $view_mode,
'display_options' => $display_options,
'langcode' => $entity->language()->id,
);
drupal_alter('field_attach_view', $result, $context);
if (isset($result[$field_name])) {
$output = $result[$field_name];
}
$build = $display->build($entity);
if (isset($build[$field_name])) {
$output = $build[$field_name];
}
return $output;
......
......@@ -142,8 +142,8 @@ function testFieldViewField() {
$this->content = drupal_render($output);
$setting = $display['settings']['test_formatter_setting_multiple'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertText('field_test_field_attach_view_alter', 'Alter fired, display passed.');
$this->assertText('field language is ' . Language::LANGCODE_NOT_SPECIFIED, 'Language is placed onto the context.');
$this->assertText('field_test_entity_display_build_alter', 'Alter fired, display passed.');
$this->assertText('entity language is ' . Language::LANGCODE_NOT_SPECIFIED, 'Language is placed onto the context.');
$array = array();
foreach ($this->values as $delta => $value) {
$array[] = $delta . ':' . $value['value'];
......@@ -163,7 +163,7 @@ function testFieldViewField() {
$this->content = $view;
$setting = $display['settings']['test_formatter_setting_additional'];
$this->assertNoText($this->label, 'Label was not displayed.');
$this->assertNoText('field_test_field_attach_view_alter', 'Alter not fired.');
$this->assertNoText('field_test_entity_display_build_alter', 'Alter not fired.');
foreach ($this->values as $delta => $value) {
$this->assertText($setting . '|' . $value['value'] . '|' . ($value['value'] + 1), format_string('Value @delta was displayed with expected setting.', array('@delta' => $delta)));
}
......
......@@ -42,9 +42,9 @@ public function setUp() {
}
/**
* Test field_attach_view() and field_attach_prepare_view().
* Test rendering fields with EntityDisplay build().
*/
function testFieldAttachView() {