Commit b149df70 authored by webchick's avatar webchick

Issue #1785748 by yched, swentel, Stalski: Field formatters as plugins.

parent ca64740b
......@@ -806,8 +806,8 @@ function hook_field_widget_WIDGET_TYPE_form_alter(&$element, &$form_state, $cont
* The instance's widget properties.
* @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'.
* - entity_type: The entity type, e.g., 'node' or 'user'.
* - bundle: The bundle, e.g., 'page' or 'article'.
* - field: The field that the widget belongs to.
* - instance: The instance of the field.
*
......@@ -836,78 +836,28 @@ function hook_field_widget_properties_alter(array &$widget_properties, array $co
* which the field is attached is displayed. Fields of a given
* @link field_types field type @endlink may be displayed using more than one
* formatter. In this case, the Field UI module allows the site builder to
* choose which formatter to use. Field formatters are defined by implementing
* hook_field_formatter_info().
* choose which formatter to use.
*
* Formatters are Plugins managed by the
* Drupal\field\Plugin\Type\Formatter\FormatterPluginManager class. A formatter
* is implemented by providing a class that implements
* Drupal\field\Plugin\Type\Formatter\FormatterInterface (in most cases, by
* subclassing Drupal\field\Plugin\Type\Formatter\FormatterBase), and provides
* the proper annotation block.
*
* @see field
* @see field_types
* @see field_widget
*/
/**
* Expose Field API formatter types.
*
* 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.
*
* @return
* An array describing the formatter types implemented by the module. The keys
* are formatter type names. To avoid name clashes, formatter type names
* should be prefixed with the name of the module that exposes them. The
* values are arrays describing the formatter type, with the following
* key/value pairs:
* - label: The human-readable name of the formatter type.
* - description: A short description of the formatter type.
* - field types: An array of field types the formatter supports.
* - settings: An array whose keys are the names of the settings available to
* the formatter type, and whose values are the default values for those
* settings.
*
* @see hook_field_formatter_info_alter()
* @see hook_field_formatter_view()
* @see hook_field_formatter_prepare_view()
*/
function hook_field_formatter_info() {
return array(
'text_default' => array(
'label' => t('Default'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
'text_plain' => array(
'label' => t('Plain text'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
// The text_trimmed formatter displays the trimmed version of the
// full element of the field. It is intended to be used with text
// and text_long fields. It also works with text_with_summary
// fields though the text_summary_or_trimmed formatter makes more
// sense for that field type.
'text_trimmed' => array(
'label' => t('Trimmed'),
'field types' => array('text', 'text_long', 'text_with_summary'),
),
// The 'summary or trimmed' field formatter for text_with_summary
// fields displays returns the summary element of the field or, if
// the summary is empty, the trimmed version of the full element
// of the field.
'text_summary_or_trimmed' => array(
'label' => t('Summary or trimmed'),
'field types' => array('text_with_summary'),
),
);
}
/**
* Perform alterations on Field API formatter types.
*
* @param $info
* An array of information on formatter types exposed by
* hook_field_formatter_info() implementations.
* @param array $info
* An array of informations on existing formatter types, as collected by the
* annotation discovery mechanism.
*/
function hook_field_formatter_info_alter(&$info) {
function hook_field_formatter_info_alter(array &$info) {
// Add a setting to a formatter type.
$info['text_default']['settings'] += array(
'mymodule_additional_setting' => 'default value',
......@@ -917,149 +867,6 @@ function hook_field_formatter_info_alter(&$info) {
$info['text_default']['field types'][] = 'my_field_type';
}
/**
* Allow formatters to load information for field values being displayed.
*
* This should be used when a formatter needs to load additional information
* from the database in order to render a field, for example a reference field
* that displays properties of the referenced entities such as name or type.
*
* This hook is called after the field type's own hook_field_prepare_view().
*
* Unlike most other field hooks, this hook operates on multiple entities. The
* $entities, $instances and $items parameters are arrays keyed by entity ID.
* For performance reasons, information for all available 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.
*
* @param $entity_type
* The type of $entity.
* @param $entities
* Array of entities being displayed, keyed by entity ID.
* @param $field
* The field structure for the operation.
* @param $instances
* Array of instance structures for $field for each entity, keyed by entity
* ID.
* @param $langcode
* The language the field values are to be shown in. If no language is
* provided the current language is used.
* @param $items
* Array of field values for the entities, keyed by entity ID.
* @param $displays
* Array of display settings to use for each entity, keyed by entity ID.
*/
function hook_field_formatter_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $displays) {
$tids = array();
// Collect every possible term attached to any of the fieldable entities.
foreach ($entities as $id => $entity) {
foreach ($items[$id] as $delta => $item) {
// Force the array key to prevent duplicates.
$tids[$item['tid']] = $item['tid'];
}
}
if ($tids) {
$terms = taxonomy_term_load_multiple($tids);
// Iterate through the fieldable entities again to attach the loaded term
// data.
foreach ($entities as $id => $entity) {
$rekey = FALSE;
foreach ($items[$id] as $delta => $item) {
// Check whether the taxonomy term field instance value could be loaded.
if (isset($terms[$item['tid']])) {
// Replace the instance value with the term data.
$items[$id][$delta]['taxonomy_term'] = $terms[$item['tid']];
}
// Otherwise, unset the instance value, since the term does not exist.
else {
unset($items[$id][$delta]);
$rekey = TRUE;
}
}
if ($rekey) {
// Rekey the items array.
$items[$id] = array_values($items[$id]);
}
}
}
}
/**
* Build a renderable array for a field value.
*
* @param $entity_type
* The type of $entity.
* @param $entity
* The entity being displayed.
* @param $field
* The field structure.
* @param $instance
* The field instance.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of values for this field.
* @param $display
* The display settings to use, as found in the 'display' entry of instance
* definitions. The array notably contains the following keys and values:
* - type: The name of the formatter to use.
* - settings: The array of formatter settings.
*
* @return
* A renderable array for $items, as an array of child elements keyed by
* numeric indexes starting from 0.
*/
function hook_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
$element = array();
$settings = $display['settings'];
switch ($display['type']) {
case 'sample_field_formatter_simple':
// Common case: each value is displayed individually in a sub-element
// keyed by delta. The field.tpl.php template specifies the markup
// wrapping each value.
foreach ($items as $delta => $item) {
$element[$delta] = array('#markup' => $settings['some_setting'] . $item['value']);
}
break;
case 'sample_field_formatter_themeable':
// More elaborate formatters can defer to a theme function for easier
// customization.
foreach ($items as $delta => $item) {
$element[$delta] = array(
'#theme' => 'mymodule_theme_sample_field_formatter_themeable',
'#data' => $item['value'],
'#some_setting' => $settings['some_setting'],
);
}
break;
case 'sample_field_formatter_combined':
// Some formatters might need to display all values within a single piece
// of markup.
$rows = array();
foreach ($items as $delta => $item) {
$rows[] = array($delta, $item['value']);
}
$element[0] = array(
'#theme' => 'table',
'#header' => array(t('Delta'), t('Value')),
'#rows' => $rows,
);
break;
}
return $element;
}
/**
* @} End of "defgroup field_formatter".
*/
......@@ -2145,27 +1952,27 @@ function hook_field_info_max_weight($entity_type, $bundle, $context) {
* hook involves reading from the database, it is highly recommended to
* statically cache the information.
*
* @param $display
* @param array $display_properties
* The display settings that will be used to display the field values, as
* found in the 'display' key of $instance definitions.
* @param $context
* @param array $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - entity_type: The entity type, e.g., 'node' or 'user'.
* - bundle: The bundle, e.g., 'page' or 'article'.
* - field: The field being rendered.
* - instance: The instance being rendered.
* - entity: The entity being rendered.
* - view_mode: The view mode, e.g. 'full', 'teaser'...
*
* @see hook_field_display_ENTITY_TYPE_alter()
*/
function hook_field_display_alter(&$display, $context) {
function hook_field_display_alter(array &$display_properties, array $context) {
// Leave field labels out of the search index.
// Note: The check against $context['entity_type'] == 'node' could be avoided
// by using hook_field_display_node_alter() instead of
// hook_field_display_alter(), resulting in less function calls when
// rendering non-node entities.
if ($context['entity_type'] == 'node' && $context['view_mode'] == 'search_index') {
$display['label'] = 'hidden';
$display_properties['label'] = 'hidden';
}
}
......@@ -2180,23 +1987,23 @@ function hook_field_display_alter(&$display, $context) {
* hook involves reading from the database, it is highly recommended to
* statically cache the information.
*
* @param $display
* @param array $display_properties
* The display settings that will be used to display the field values, as
* found in the 'display' key of $instance definitions.
* @param $context
* @param array $context
* An associative array containing:
* - entity_type: The entity type; e.g., 'node' or 'user'.
* - entity_type: The entity type, e.g., 'node' or 'user'.
* - bundle: The bundle, e.g., 'page' or 'article'.
* - field: The field being rendered.
* - instance: The instance being rendered.
* - entity: The entity being rendered.
* - view_mode: The view mode, e.g. 'full', 'teaser'...
*
* @see hook_field_display_alter()
*/
function hook_field_display_ENTITY_TYPE_alter(&$display, $context) {
function hook_field_display_ENTITY_TYPE_alter(array &$display_properties, array $context) {
// Leave field labels out of the search index.
if ($context['view_mode'] == 'search_index') {
$display['label'] = 'hidden';
$display_properties['label'] = 'hidden';
}
}
......@@ -2238,8 +2045,8 @@ function hook_field_extra_fields_display_alter(&$displays, $context) {
* The instance's widget properties.
* @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'.
* - entity_type: The entity type, e.g., 'node' or 'user'.
* - bundle: The bundle, e.g., 'page' or 'article'.
* - field: The field that the widget belongs to.
* - instance: The instance of the field.
*
......
......@@ -107,24 +107,25 @@
*/
/**
* Invoke a method on all the fields of a given entity.
* Invokes a method on all the fields of a given entity.
*
* @todo Remove _field_invoke() and friends when field types and formatters are
* turned into plugins.
*
* @param string $method
* The name of the method to invoke.
* @param Closure $target
* A closure that receives an $instance object and returns the object on
* @param callable $target_function
* A function that receives an $instance object and returns the object on
* which the method should be invoked.
* @param Drupal\Core\Entity\EntityInterface $entity
* The fully formed $entity_type entity.
* @param mixed $a
* A parameter for the invoked method. Defaults to NULL.
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param mixed $b
* A parameter for the invoked method. Defaults to NULL.
* (optional) A parameter for the invoked method. Defaults to NULL.
* @param array $options
* An associative array of additional options, with the following keys:
* (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. NOTE: This option is not compatible with the 'deleted' option;
......@@ -138,8 +139,11 @@
* - langcode: A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*
* @return array
* An array of returned values.
*/
function field_invoke_method($method, \Closure $target_closure, EntityInterface $entity, &$a = NULL, &$b = NULL, array $options = array()) {
function field_invoke_method($method, $target_function, EntityInterface $entity, &$a = NULL, &$b = NULL, array $options = array()) {
// Merge default options.
$default_options = array(
'deleted' => FALSE,
......@@ -155,9 +159,9 @@ function field_invoke_method($method, \Closure $target_closure, EntityInterface
$return = array();
foreach ($instances as $instance) {
// Let the closure determine the target object on which the method should be
// Let the function determine the target object on which the method should be
// called.
$target = $target_closure($instance);
$target = call_user_func($target_function, $instance);
if (method_exists($target, $method)) {
$field = field_info_field_by_id($instance['field_id']);
......@@ -195,6 +199,136 @@ function field_invoke_method($method, \Closure $target_closure, EntityInterface
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 an $instance object and returns the object on
* which the method should be invoked.
* @param array $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. NOTE: This option is not compatible with the 'deleted' option;
* the 'field_id' option should be used instead.
* - field_id: The ID of the field whose operation should be invoked. By
* default, the operation is invoked on all the fields in the entity's'
* bundles.
* - deleted: If TRUE, the function will operate on deleted fields as well
* as non-deleted fields. If unset or FALSE, only non-deleted fields are
* operated on.
* - langcode: A language code or an array of language codes keyed by field
* name. It will be used to narrow down to a single value the available
* languages to act on.
*
* @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()) {
// Merge default options.
$default_options = array(
'deleted' => FALSE,
'langcode' => NULL,
);
$options += $default_options;
$instances = array();
$grouped_entities = 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) {
$id = $entity->id();
$entity_type = $entity->entityType();
// Determine the list of instances to iterate on.
$entity_instances = _field_invoke_get_instances($entity_type, $entity->bundle(), $options);
foreach ($entity_instances as $instance) {
$instance_id = $instance['id'];
$field_name = $instance['field_name'];
// Let the closure determine the target object on which the method should
// be called.
if (empty($grouped_targets[$instance_id])) {
$grouped_targets[$instance_id] = call_user_func($target_function, $instance);
}
if (method_exists($grouped_targets[$instance_id], $method)) {
// Add the instance to the list of instances to invoke the hook on.
if (!isset($instances[$instance_id])) {
$instances[$instance_id] = $instance;
}
// Unless a language code suggestion is provided we iterate on all the
// available language codes.
$field = field_info_field_by_id($instance['field_id']);
$available_langcodes = field_available_languages($entity_type, $field);
$langcode = !empty($options['langcode'][$id]) ? $options['langcode'][$id] : $options['langcode'];
$langcodes = _field_language_suggestion($available_langcodes, $langcode, $field_name);
foreach ($langcodes as $langcode) {
// Group the entities and items corresponding to the current field.
$grouped_entities[$instance_id][$langcode][$id] = $entities[$id];
$grouped_items[$instance_id][$langcode][$id] = isset($entity->{$field_name}[$langcode]) ? $entity->{$field_name}[$langcode] : array();
}
}
}
// Initialize the return value for each entity.
$return[$id] = array();
}
// For each instance, invoke the method and collect results.
foreach ($instances as $instance_id => $instance) {
$field_name = $instance['field_name'];
// Iterate over all the field translations.
foreach ($grouped_items[$instance_id] as $langcode => &$items) {
$entities = $grouped_entities[$instance_id][$langcode];
$results = $grouped_targets[$instance_id]->$method($entities, $langcode, $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;
}
}
}
}
// Populate field values back in the entities, but avoid replacing missing
// fields with an empty array (those are not equivalent on update).
foreach ($grouped_entities[$instance_id] as $langcode => $entities) {
foreach ($entities as $id => $entity) {
if ($grouped_items[$instance_id][$langcode][$id] !== array() || isset($entity->{$field_name}[$langcode])) {
$entity->{$field_name}[$langcode] = $grouped_items[$instance_id][$langcode][$id];
}
}
}
}
return $return;
}
/**
* Invoke a field hook.
*
......@@ -563,12 +697,12 @@ function _field_invoke_get_instances($entity_type, $bundle, $options) {
}
/**
* Defines a 'target closure' for field_invoke_method().
* Defines a 'target function' for field_invoke_method().
*
* Used to invoke methods on an instance's widget.
*
* @return Closure
* A 'target closure' for field_invoke_method().
* @return callable $target_function
* A 'target function' for field_invoke_method().
*/
function _field_invoke_widget_target() {
return function ($instance) {
......@@ -576,6 +710,26 @@ function _field_invoke_widget_target() {
};
}
/**
* Defines a 'target function' for field_invoke_method().
*
* Used to invoke methods on an instance's formatter.
*
* @param mixed $display
* Can be either:
* - The name of a view mode.
* - An array of display properties, as found in
* $instance['display'][$view_mode].
*
* @return callable $target_function
* A 'target function' for field_invoke_method().
*/
function _field_invoke_formatter_target($display) {
return function ($instance) use ($display) {
return $instance->getFormatter($display);
};
}
/**
* Adds form elements for all fields for an entity to a form structure.
*
......@@ -1223,9 +1377,7 @@ function field_attach_prepare_view($entity_type, $entities, $view_mode, $langcod
// First let the field types do their preparation.
_field_invoke_multiple('prepare_view', $entity_type, $prepare, $null, $null, $options);
// Then let the formatters do their own specific massaging.
// field_default_prepare_view() takes care of dispatching to the correct
// formatters according to the display settings for the view mode.
_field_invoke_multiple_default('prepare_view', $entity_type, $prepare, $view_mode, $null, $options);
field_invoke_method_multiple('prepareView', _field_invoke_formatter_target($view_mode), $prepare, $view_mode, $null, $options);
}
/**
......@@ -1281,7 +1433,7 @@ function field_attach_view($entity_type, EntityInterface $entity, $view_mode, $l
// Invoke field_default_view().
$null = NULL;
$output = _field_invoke_default('view', $entity_type, $entity, $view_mode, $null, $options);
$output = field_invoke_method('view', _field_invoke_formatter_target($view_mode), $entity, $view_mode, $null, $options);
// Add custom weight handling.
$output['#pre_render'][] = '_field_extra_fields_pre_render';
......
......@@ -86,127 +86,6 @@ function field_default_insert($entity_type, $entity, $field, $instance, $langcod
}
}
/**
* Invokes hook_field_formatter_prepare_view() on the relevant formatters.
*
* @param $entity_type
* The type of $entity; e.g. 'node' or 'user'.
* @param $entities
* An array of entities being displayed, keyed by entity ID.
* @param $field
* The field structure for the operation.
* @param $instances
* Array of instance structures for $field for each entity, keyed by entity
* ID.
* @param $langcode
* The language associated with $items.
* @param $items
* Array of field values already loaded for the entities, keyed by entity id.
* @param $display
* Can be either:
* - The name of a view mode
* - An array of display settings to use for display, as found in the
* 'display' entry of $instance definitions.
*/
function field_default_prepare_view($entity_type, $entities, $field, $instances, $langcode, &$items, $display) {
// Group entities, instances and items by formatter module.
$modules = array();
foreach ($instances as $id => $instance) {
if (is_string($display)) {
$view_mode = $display;
$instance_display = field_get_display($instance, $view_mode, $entities[$id]);
}
else {
$instance_display = $display;
}
if ($instance_display['type'] !== 'hidden') {
$module = $instance_display['module'];
$modules[$module] = $module;
$grouped_entities[$module][$id] = $entities[$id];
$grouped_instances[$module][$id] = $instance;
$grouped_displays[$module][$id] = $instance_display;
// hook_field_formatter_prepare_view() alters $items by reference.
$grouped_items[$module][$id] = &$items[$id];
}
}
foreach ($modules as $module) {
// Invoke hook_field_formatter_prepare_view().