3 namespace Drupal\views\Plugin\views\field;
5 use Drupal\Component\Plugin\DependentPluginInterface;
6 use Drupal\Component\Utility\Xss;
7 use Drupal\Core\Cache\Cache;
8 use Drupal\Core\Cache\CacheableDependencyInterface;
9 use Drupal\Core\Entity\EntityInterface;
10 use Drupal\Core\Entity\EntityManagerInterface;
11 use Drupal\Core\Field\FieldStorageDefinitionInterface;
12 use Drupal\Core\Field\FieldTypePluginManagerInterface;
13 use Drupal\Core\Field\FormatterPluginManager;
14 use Drupal\Core\Form\FormHelper;
15 use Drupal\Core\Form\FormStateInterface;
16 use Drupal\Core\Language\LanguageManagerInterface;
17 use Drupal\Core\Plugin\PluginDependencyTrait;
18 use Drupal\Core\Render\BubbleableMetadata;
19 use Drupal\Core\Render\Element;
20 use Drupal\Core\Render\RendererInterface;
21 use Drupal\Core\Session\AccountInterface;
22 use Drupal\Core\TypedData\TypedDataInterface;
23 use Drupal\views\FieldAPIHandlerTrait;
24 use Drupal\views\Entity\Render\EntityFieldRenderer;
25 use Drupal\views\Plugin\views\display\DisplayPluginBase;
26 use Drupal\views\Plugin\DependentWithRemovalPluginInterface;
27 use Drupal\views\ResultRow;
28 use Drupal\views\ViewExecutable;
29 use Symfony\Component\DependencyInjection\ContainerInterface;
32 * A field that displays entity field data.
34 * @ingroup views_field_handlers
36 * @ViewsField("field")
38 class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface, DependentWithRemovalPluginInterface {
40 use FieldAPIHandlerTrait;
41 use PluginDependencyTrait;
44 * An array to store field renderable arrays for use by renderItems().
51 * Does the field supports multiple field values.
58 * Does the rendered fields get limited.
65 * A shortcut for $view->base_table.
72 * An array of formatter options.
76 protected $formatterOptions;
81 * @var \Drupal\Core\Entity\EntityManagerInterface
83 protected $entityManager;
86 * The field formatter plugin manager.
88 * @var \Drupal\Core\Field\FormatterPluginManager
90 protected $formatterPluginManager;
93 * The language manager.
95 * @var \Drupal\Core\Language\LanguageManagerInterface
97 protected $languageManager;
102 * @var \Drupal\Core\Render\RendererInterface
107 * The field type plugin manager.
109 * @var \Drupal\Core\Field\FieldTypePluginManagerInterface
111 protected $fieldTypePluginManager;
114 * Static cache for ::getEntityFieldRenderer().
116 * @var \Drupal\views\Entity\Render\EntityFieldRenderer
118 protected $entityFieldRenderer;
121 * Constructs a \Drupal\field\Plugin\views\field\Field object.
123 * @param array $configuration
124 * A configuration array containing information about the plugin instance.
125 * @param string $plugin_id
126 * The plugin_id for the plugin instance.
127 * @param mixed $plugin_definition
128 * The plugin implementation definition.
129 * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
130 * The field formatter plugin manager.
131 * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager
132 * The field formatter plugin manager.
133 * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager
134 * The field plugin type manager.
135 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
136 * The language manager.
137 * @param \Drupal\Core\Render\RendererInterface $renderer
140 public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager, FormatterPluginManager $formatter_plugin_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, LanguageManagerInterface $language_manager, RendererInterface $renderer) {
141 parent::__construct($configuration, $plugin_id, $plugin_definition);
143 $this->entityManager = $entity_manager;
144 $this->formatterPluginManager = $formatter_plugin_manager;
145 $this->fieldTypePluginManager = $field_type_plugin_manager;
146 $this->languageManager = $language_manager;
147 $this->renderer = $renderer;
149 // @todo Unify 'entity field'/'field_name' instead of converting back and
150 // forth. https://www.drupal.org/node/2410779
151 if (isset($this->definition['entity field'])) {
152 $this->definition['field_name'] = $this->definition['entity field'];
159 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
164 $container->get('entity.manager'),
165 $container->get('plugin.manager.field.formatter'),
166 $container->get('plugin.manager.field.field_type'),
167 $container->get('language_manager'),
168 $container->get('renderer')
175 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
176 parent::init($view, $display, $options);
178 $this->multiple = FALSE;
179 $this->limit_values = FALSE;
181 $field_definition = $this->getFieldDefinition();
182 $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality();
183 if ($field_definition->getFieldStorageDefinition()->isMultiple()) {
184 $this->multiple = TRUE;
186 // If "Display all values in the same row" is FALSE, then we always limit
187 // in order to show a single unique value per row.
188 if (!$this->options['group_rows']) {
189 $this->limit_values = TRUE;
192 // If "First and last only" is chosen, limit the values
193 if (!empty($this->options['delta_first_last'])) {
194 $this->limit_values = TRUE;
197 // Otherwise, we only limit values if the user hasn't selected "all", 0, or
198 // the value matching field cardinality.
199 if ((($this->options['delta_limit'] > 0) && ($this->options['delta_limit'] != $cardinality)) || intval($this->options['delta_offset'])) {
200 $this->limit_values = TRUE;
208 protected function getEntityManager() {
209 return $this->entityManager;
215 public function access(AccountInterface $account) {
216 $access_control_handler = $this->entityManager->getAccessControlHandler($this->getEntityType());
217 return $access_control_handler->fieldAccess('view', $this->getFieldDefinition(), $account);
221 * Called to add the field to a query.
223 * By default, all needed data is taken from entities loaded by the query
224 * plugin. Columns are added only if they are used in groupings.
226 public function query($use_groupby = FALSE) {
227 $fields = $this->additional_fields;
228 // No need to add the entity type.
229 $entity_type_key = array_search('entity_type', $fields);
230 if ($entity_type_key !== FALSE) {
231 unset($fields[$entity_type_key]);
235 // Add the fields that we're actually grouping on.
237 if ($this->options['group_column'] != 'entity_id') {
238 $options = [$this->options['group_column'] => $this->options['group_column']];
240 $options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : [];
242 // Go through the list and determine the actual column name from field api.
244 $table_mapping = $this->getTableMapping();
245 $field_definition = $this->getFieldStorageDefinition();
247 foreach ($options as $column) {
248 $fields[$column] = $table_mapping->getFieldColumnName($field_definition, $column);
251 $this->group_fields = $fields;
254 // Add additional fields (and the table join itself) if needed.
255 if ($this->add_field_table($use_groupby)) {
256 $this->ensureMyTable();
257 $this->addAdditionalFields($fields);
260 // Let the entity field renderer alter the query if needed.
261 $this->getEntityFieldRenderer()->query($this->query, $this->relationship);
265 * Determine if the field table should be added to the query.
267 public function add_field_table($use_groupby) {
268 // Grouping is enabled.
272 // This a multiple value field, but "group multiple values" is not checked.
273 if ($this->multiple && !$this->options['group_rows']) {
282 public function clickSortable() {
283 // A field is not click sortable if it's a multiple field with
284 // "group multiple values" checked, since a click sort in that case would
285 // add a join to the field table, which would produce unwanted duplicates.
286 if ($this->multiple && $this->options['group_rows']) {
290 // If field definition is set, use that.
291 if (isset($this->definition['click sortable'])) {
292 return (bool) $this->definition['click sortable'];
300 * Called to determine what to tell the clicksorter.
302 public function clickSort($order) {
303 // No column selected, can't continue.
304 if (empty($this->options['click_sort_column'])) {
308 $this->ensureMyTable();
309 $field_storage_definition = $this->getFieldStorageDefinition();
310 $column = $this->getTableMapping()->getFieldColumnName($field_storage_definition, $this->options['click_sort_column']);
311 if (!isset($this->aliases[$column])) {
312 // Column is not in query; add a sort on it (without adding the column).
313 $this->aliases[$column] = $this->tableAlias . '.' . $column;
315 $this->query->addOrderBy(NULL, NULL, $order, $this->aliases[$column]);
319 * Gets the field storage definition.
321 * @return \Drupal\Core\Field\FieldStorageDefinitionInterface
322 * The field storage definition used by this handler.
324 protected function getFieldStorageDefinition() {
325 $entity_type_id = $this->definition['entity_type'];
326 $field_storage_definitions = $this->entityManager->getFieldStorageDefinitions($entity_type_id);
328 // @todo Unify 'entity field'/'field_name' instead of converting back and
329 // forth. https://www.drupal.org/node/2410779
330 if (isset($this->definition['field_name']) && isset($field_storage_definitions[$this->definition['field_name']])) {
331 return $field_storage_definitions[$this->definition['field_name']];
334 if (isset($this->definition['entity field']) && isset($field_storage_definitions[$this->definition['entity field']])) {
335 return $field_storage_definitions[$this->definition['entity field']];
338 // The list of field storage definitions above does not include computed
339 // base fields, so we need to explicitly fetch a list of all base fields in
340 // order to support them.
341 // @see \Drupal\Core\Entity\EntityFieldManager::getFieldStorageDefinitions()
342 $base_fields = $this->entityManager->getBaseFieldDefinitions($entity_type_id);
343 if (isset($this->definition['field_name']) && isset($base_fields[$this->definition['field_name']])) {
344 return $base_fields[$this->definition['field_name']]->getFieldStorageDefinition();
351 protected function defineOptions() {
352 $options = parent::defineOptions();
354 $field_storage_definition = $this->getFieldStorageDefinition();
355 $field_type = $this->fieldTypePluginManager->getDefinition($field_storage_definition->getType());
356 $column_names = array_keys($field_storage_definition->getColumns());
357 $default_column = '';
358 // Try to determine a sensible default.
359 if (count($column_names) == 1) {
360 $default_column = $column_names[0];
362 elseif (in_array('value', $column_names)) {
363 $default_column = 'value';
366 // If the field has a "value" column, we probably need that one.
367 $options['click_sort_column'] = [
368 'default' => $default_column,
371 if (isset($this->definition['default_formatter'])) {
372 $options['type'] = ['default' => $this->definition['default_formatter']];
374 elseif (isset($field_type['default_formatter'])) {
375 $options['type'] = ['default' => $field_type['default_formatter']];
378 $options['type'] = ['default' => ''];
381 $options['settings'] = [
382 'default' => isset($this->definition['default_formatter_settings']) ? $this->definition['default_formatter_settings'] : [],
384 $options['group_column'] = [
385 'default' => $default_column,
387 $options['group_columns'] = [
391 // Options used for multiple value fields.
392 $options['group_rows'] = [
395 // If we know the exact number of allowed values, then that can be
396 // the default. Otherwise, default to 'all'.
397 $options['delta_limit'] = [
398 'default' => ($field_storage_definition->getCardinality() > 1) ? $field_storage_definition->getCardinality() : 0,
400 $options['delta_offset'] = [
403 $options['delta_reversed'] = [
406 $options['delta_first_last'] = [
410 $options['multi_type'] = [
411 'default' => 'separator'
413 $options['separator'] = [
417 $options['field_api_classes'] = [
427 public function buildOptionsForm(&$form, FormStateInterface $form_state) {
428 parent::buildOptionsForm($form, $form_state);
430 $field = $this->getFieldDefinition();
431 $formatters = $this->formatterPluginManager->getOptions($field->getType());
432 $column_names = array_keys($field->getColumns());
434 // If this is a multiple value field, add its options.
435 if ($this->multiple) {
436 $this->multiple_options_form($form, $form_state);
439 // No need to ask the user anything if the field has only one column.
440 if (count($field->getColumns()) == 1) {
441 $form['click_sort_column'] = [
443 '#value' => isset($column_names[0]) ? $column_names[0] : '',
447 $form['click_sort_column'] = [
449 '#title' => $this->t('Column used for click sorting'),
450 '#options' => array_combine($column_names, $column_names),
451 '#default_value' => $this->options['click_sort_column'],
452 '#description' => $this->t('Used by Style: Table to determine the actual column to click sort the field on. The default is usually fine.'),
458 '#title' => $this->t('Formatter'),
459 '#options' => $formatters,
460 '#default_value' => $this->options['type'],
462 'url' => views_ui_build_form_url($form_state),
464 '#submit' => [[$this, 'submitTemporaryForm']],
465 '#executes_submit_callback' => TRUE,
468 $form['field_api_classes'] = [
469 '#title' => $this->t('Use field template'),
470 '#type' => 'checkbox',
471 '#default_value' => $this->options['field_api_classes'],
472 '#description' => $this->t('If checked, field api classes will be added by field templates. This is not recommended unless your CSS depends upon these classes. If not checked, template will not be used.'),
473 '#fieldset' => 'style_settings',
477 if ($this->multiple) {
478 $form['field_api_classes']['#description'] .= ' ' . $this->t('Checking this option will cause the group Display Type and Separator values to be ignored.');
481 // Get the settings form.
482 $settings_form = ['#value' => []];
483 $format = isset($form_state->getUserInput()['options']['type']) ? $form_state->getUserInput()['options']['type'] : $this->options['type'];
484 if ($formatter = $this->getFormatterInstance($format)) {
485 $settings_form = $formatter->settingsForm($form, $form_state);
486 // Convert field UI selector states to work in the Views field form.
487 FormHelper::rewriteStatesSelector($settings_form, "fields[{$field->getName()}][settings_edit_form]", 'options');
489 $form['settings'] = $settings_form;
495 public function submitFormCalculateOptions(array $options, array $form_state_options) {
496 // When we change the formatter type we don't want to keep any of the
497 // previous configured formatter settings, as there might be schema
499 unset($options['settings']);
500 $options = $form_state_options + $options;
501 if (!isset($options['settings'])) {
502 $options['settings'] = [];
508 * Provide options for multiple value fields.
510 public function multiple_options_form(&$form, FormStateInterface $form_state) {
511 $field = $this->getFieldDefinition();
513 $form['multiple_field_settings'] = [
514 '#type' => 'details',
515 '#title' => $this->t('Multiple field settings'),
519 $form['group_rows'] = [
520 '#title' => $this->t('Display all values in the same row'),
521 '#type' => 'checkbox',
522 '#default_value' => $this->options['group_rows'],
523 '#description' => $this->t('If checked, multiple values for this field will be shown in the same row. If not checked, each value in this field will create a new row. If using group by, please make sure to group by "Entity ID" for this setting to have any effect.'),
524 '#fieldset' => 'multiple_field_settings',
527 // Make the string translatable by keeping it as a whole rather than
528 // translating prefix and suffix separately.
529 list($prefix, $suffix) = explode('@count', $this->t('Display @count value(s)'));
531 if ($field->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
538 $range = range(1, $field->getCardinality());
539 $options = array_combine($range, $range);
542 $form['multi_type'] = [
544 '#title' => $this->t('Display type'),
546 'ul' => $this->t('Unordered list'),
547 'ol' => $this->t('Ordered list'),
548 'separator' => $this->t('Simple separator'),
552 ':input[name="options[group_rows]"]' => ['checked' => TRUE],
555 '#default_value' => $this->options['multi_type'],
556 '#fieldset' => 'multiple_field_settings',
559 $form['separator'] = [
560 '#type' => 'textfield',
561 '#title' => $this->t('Separator'),
562 '#default_value' => $this->options['separator'],
565 ':input[name="options[group_rows]"]' => ['checked' => TRUE],
566 ':input[name="options[multi_type]"]' => ['value' => 'separator'],
569 '#fieldset' => 'multiple_field_settings',
572 $form['delta_limit'] = [
575 '#field_prefix' => $prefix,
576 '#field_suffix' => $suffix,
577 '#options' => $options,
578 '#default_value' => $this->options['delta_limit'],
579 '#prefix' => '<div class="container-inline">',
582 ':input[name="options[group_rows]"]' => ['checked' => TRUE],
585 '#fieldset' => 'multiple_field_settings',
588 list($prefix, $suffix) = explode('@count', $this->t('starting from @count'));
589 $form['delta_offset'] = [
590 '#type' => 'textfield',
592 '#field_prefix' => $prefix,
593 '#field_suffix' => $suffix,
594 '#default_value' => $this->options['delta_offset'],
597 ':input[name="options[group_rows]"]' => ['checked' => TRUE],
600 '#description' => $this->t('(first item is 0)'),
601 '#fieldset' => 'multiple_field_settings',
603 $form['delta_reversed'] = [
604 '#title' => $this->t('Reversed'),
605 '#type' => 'checkbox',
606 '#default_value' => $this->options['delta_reversed'],
607 '#suffix' => $suffix,
610 ':input[name="options[group_rows]"]' => ['checked' => TRUE],
613 '#description' => $this->t('(start from last values)'),
614 '#fieldset' => 'multiple_field_settings',
616 $form['delta_first_last'] = [
617 '#title' => $this->t('First and last only'),
618 '#type' => 'checkbox',
619 '#default_value' => $this->options['delta_first_last'],
620 '#suffix' => '</div>',
623 ':input[name="options[group_rows]"]' => ['checked' => TRUE],
626 '#fieldset' => 'multiple_field_settings',
631 * Extend the groupby form with group columns.
633 public function buildGroupByForm(&$form, FormStateInterface $form_state) {
634 parent::buildGroupByForm($form, $form_state);
635 // With "field API" fields, the column target of the grouping function
636 // and any additional grouping columns must be specified.
638 $field_columns = array_keys($this->getFieldDefinition()->getColumns());
640 'entity_id' => $this->t('Entity ID'),
641 ] + array_map('ucfirst', array_combine($field_columns, $field_columns));
643 $form['group_column'] = [
645 '#title' => $this->t('Group column'),
646 '#default_value' => $this->options['group_column'],
647 '#description' => $this->t('Select the column of this field to apply the grouping function selected above.'),
648 '#options' => $group_columns,
652 'bundle' => 'Bundle',
653 'language' => 'Language',
654 'entity_type' => 'Entity_type',
656 // Add on defined fields, noting that they're prefixed with the field name.
657 $form['group_columns'] = [
658 '#type' => 'checkboxes',
659 '#title' => $this->t('Group columns (additional)'),
660 '#default_value' => $this->options['group_columns'],
661 '#description' => $this->t('Select any additional columns of this field to include in the query and to group on.'),
662 '#options' => $options + $group_columns,
666 public function submitGroupByForm(&$form, FormStateInterface $form_state) {
667 parent::submitGroupByForm($form, $form_state);
668 $item = &$form_state->get('handler')->options;
670 // Add settings for "field API" fields.
671 $item['group_column'] = $form_state->getValue(['options', 'group_column']);
672 $item['group_columns'] = array_filter($form_state->getValue(['options', 'group_columns']));
676 * Render all items in this field together.
678 * When using advanced render, each possible item in the list is rendered
679 * individually. Then the items are all pasted together.
681 public function renderItems($items) {
682 if (!empty($items)) {
683 $items = $this->prepareItemsByDelta($items);
684 if ($this->options['multi_type'] == 'separator' || !$this->options['group_rows']) {
685 $separator = $this->options['multi_type'] == 'separator' ? Xss::filterAdmin($this->options['separator']) : '';
687 '#type' => 'inline_template',
688 '#template' => '{{ items | safe_join(separator) }}',
689 '#context' => ['separator' => $separator, 'items' => $items],
694 '#theme' => 'item_list',
697 '#list_type' => $this->options['multi_type'],
700 return $this->renderer->render($build);
705 * Adapts the $items according to the delta configuration.
707 * This selects displayed deltas, reorders items, and takes offsets into
710 * @param array $all_values
711 * The items for individual rendering.
714 * The manipulated items.
716 protected function prepareItemsByDelta(array $all_values) {
717 if ($this->options['delta_reversed']) {
718 $all_values = array_reverse($all_values);
721 // We are supposed to show only certain deltas.
722 if ($this->limit_values) {
723 $row = $this->view->result[$this->view->row_index];
725 // Offset is calculated differently when row grouping for a field is not
726 // enabled. Since there are multiple rows, delta needs to be taken into
727 // account, so that different values are shown per row.
728 if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($row->{$this->aliases['delta']})) {
730 $offset = $row->{$this->aliases['delta']};
732 // Single fields don't have a delta available so choose 0.
733 elseif (!$this->options['group_rows'] && !$this->multiple) {
738 $delta_limit = $this->options['delta_limit'];
739 $offset = intval($this->options['delta_offset']);
741 // We should only get here in this case if there is an offset, and in
742 // that case we are limiting to all values after the offset.
743 if ($delta_limit === 0) {
744 $delta_limit = count($all_values) - $offset;
748 // Determine if only the first and last values should be shown.
749 $delta_first_last = $this->options['delta_first_last'];
752 for ($i = 0; $i < $delta_limit; $i++) {
753 $new_delta = $offset + $i;
755 if (isset($all_values[$new_delta])) {
756 // If first-last option was selected, only use the first and last
758 if (!$delta_first_last
759 // Use the first value.
760 || $new_delta == $offset
761 // Use the last value.
762 || $new_delta == ($delta_limit + $offset - 1)) {
763 $new_values[] = $all_values[$new_delta];
767 $all_values = $new_values;
776 public function preRender(&$values) {
777 parent::preRender($values);
778 $this->getEntityFieldRenderer()->preRender($values);
782 * Returns the entity field renderer.
784 * @return \Drupal\views\Entity\Render\EntityFieldRenderer
785 * The entity field renderer.
787 protected function getEntityFieldRenderer() {
788 if (!isset($this->entityFieldRenderer)) {
789 // This can be invoked during field handler initialization in which case
790 // view fields are not set yet.
791 if (!empty($this->view->field)) {
792 foreach ($this->view->field as $field) {
793 // An entity field renderer can handle only a single relationship.
794 if ($field->relationship == $this->relationship && isset($field->entityFieldRenderer)) {
795 $this->entityFieldRenderer = $field->entityFieldRenderer;
800 if (!isset($this->entityFieldRenderer)) {
801 $entity_type = $this->entityManager->getDefinition($this->getEntityType());
802 $this->entityFieldRenderer = new EntityFieldRenderer($this->view, $this->relationship, $this->languageManager, $entity_type, $this->entityManager);
805 return $this->entityFieldRenderer;
809 * Gets an array of items for the field.
811 * @param \Drupal\views\ResultRow $values
812 * The result row object containing the values.
815 * An array of items for the field.
817 public function getItems(ResultRow $values) {
818 if (!$this->displayHandler->useGroupBy()) {
819 $build_list = $this->getEntityFieldRenderer()->render($values, $this);
822 // For grouped results we need to retrieve a massaged entity having
823 // grouped field values to ensure that "grouped by" values, especially
824 // those with multiple cardinality work properly. See
825 // \Drupal\Tests\views\Kernel\QueryGroupByTest::testGroupByFieldWithCardinality.
827 'type' => $this->options['type'],
828 'settings' => $this->options['settings'],
831 // Optional relationships may not provide an entity at all. So we can't
832 // use createEntityForGroupBy() for those rows.
833 if ($entity = $this->getEntity($values)) {
834 $entity = $this->createEntityForGroupBy($entity, $values);
835 // Some bundles might not have a specific field, in which case the faked
836 // entity doesn't have it either.
837 $build_list = isset($entity->{$this->definition['field_name']}) ? $entity->{$this->definition['field_name']}->view($display) : NULL;
848 if ($this->options['field_api_classes']) {
849 return [['rendered' => $this->renderer->render($build_list)]];
852 // Render using the formatted data itself.
854 // Each item is extracted and rendered separately, the top-level formatter
855 // render array itself is never rendered, so we extract its bubbleable
856 // metadata and add it to each child individually.
857 $bubbleable = BubbleableMetadata::createFromRenderArray($build_list);
858 foreach (Element::children($build_list) as $delta) {
859 BubbleableMetadata::createFromRenderArray($build_list[$delta])
861 ->applyTo($build_list[$delta]);
863 'rendered' => $build_list[$delta],
864 // Add the raw field items (for use in tokens).
865 'raw' => $build_list['#items'][$delta],
872 * Creates a fake entity with grouped field values.
874 * @param \Drupal\Core\Entity\EntityInterface $entity
875 * The entity to be processed.
876 * @param \Drupal\views\ResultRow $row
877 * The result row object containing the values.
879 * @return bool|\Drupal\Core\Entity\FieldableEntityInterface
880 * Returns a new entity object containing the grouped field values.
882 protected function createEntityForGroupBy(EntityInterface $entity, ResultRow $row) {
883 // Retrieve the correct translation object.
884 $processed_entity = clone $this->getEntityFieldRenderer()->getEntityTranslation($entity, $row);
886 // Copy our group fields into the cloned entity. It is possible this will
887 // cause some weirdness, but there is only so much we can hope to do.
888 if (!empty($this->group_fields) && isset($entity->{$this->definition['field_name']})) {
889 // first, test to see if we have a base value.
891 // Note: We would copy original values here, but it can cause problems.
892 // For example, text fields store cached filtered values as 'safe_value'
893 // which does not appear anywhere in the field definition so we cannot
894 // affect it. Other side effects could happen similarly.
896 foreach ($this->group_fields as $field_name => $column) {
897 if (property_exists($row, $this->aliases[$column])) {
898 $base_value[$field_name] = $row->{$this->aliases[$column]};
899 if (isset($base_value[$field_name])) {
905 // If any of our aggregated fields have data, fake it:
907 // Now, overwrite the original value with our aggregated value.
908 // This overwrites it so there is always just one entry.
909 $processed_entity->{$this->definition['field_name']} = [$base_value];
912 $processed_entity->{$this->definition['field_name']} = [];
916 return $processed_entity;
919 public function render_item($count, $item) {
920 return render($item['rendered']);
923 protected function documentSelfTokens(&$tokens) {
924 $field = $this->getFieldDefinition();
925 foreach ($field->getColumns() as $id => $column) {
926 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = $this->t('Raw @column', ['@column' => $id]);
930 protected function addSelfTokens(&$tokens, $item) {
931 $field = $this->getFieldDefinition();
932 foreach ($field->getColumns() as $id => $column) {
933 // Use \Drupal\Component\Utility\Xss::filterAdmin() because it's user data
934 // and we can't be sure it is safe. We know nothing about the data,
935 // though, so we can't really do much else.
936 if (isset($item['raw'])) {
939 if (is_array($raw)) {
940 if (isset($raw[$id]) && is_scalar($raw[$id])) {
941 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($raw[$id]);
944 // Make sure that empty values are replaced as well.
945 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = '';
949 if (is_object($raw)) {
950 $property = $raw->get($id);
951 // Check if TypedDataInterface is implemented so we know how to render
952 // the item as a string.
953 if (!empty($property) && $property instanceof TypedDataInterface) {
954 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($property->getString());
957 // Make sure that empty values are replaced as well.
958 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = '';
966 * Returns the field formatter instance.
968 * @return \Drupal\Core\Field\FormatterInterface|null
969 * The field formatter instance.
971 protected function getFormatterInstance($format = NULL) {
972 if (!isset($format)) {
973 $format = $this->options['type'];
975 $settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($format);
978 'field_definition' => $this->getFieldDefinition(),
981 'settings' => $settings,
985 'view_mode' => '_custom',
988 return $this->formatterPluginManager->getInstance($options);
994 public function calculateDependencies() {
995 $this->dependencies = parent::calculateDependencies();
997 // Add the module providing the configured field storage as a dependency.
998 if (($field_storage_definition = $this->getFieldStorageDefinition()) && $field_storage_definition instanceof EntityInterface) {
999 $this->dependencies['config'][] = $field_storage_definition->getConfigDependencyName();
1001 if (!empty($this->options['type'])) {
1002 // Add the module providing the formatter.
1003 $this->dependencies['module'][] = $this->formatterPluginManager->getDefinition($this->options['type'])['provider'];
1005 // Add the formatter's dependencies.
1006 if (($formatter = $this->getFormatterInstance()) && $formatter instanceof DependentPluginInterface) {
1007 $this->calculatePluginDependencies($formatter);
1011 return $this->dependencies;
1017 public function getCacheMaxAge() {
1018 return Cache::PERMANENT;
1024 public function getCacheContexts() {
1025 return $this->getEntityFieldRenderer()->getCacheContexts();
1031 public function getCacheTags() {
1032 $field_definition = $this->getFieldDefinition();
1033 $field_storage_definition = $this->getFieldStorageDefinition();
1034 return Cache::mergeTags(
1035 $field_definition instanceof CacheableDependencyInterface ? $field_definition->getCacheTags() : [],
1036 $field_storage_definition instanceof CacheableDependencyInterface ? $field_storage_definition->getCacheTags() : []
1041 * Gets the table mapping for the entity type of the field.
1043 * @return \Drupal\Core\Entity\Sql\DefaultTableMapping
1044 * The table mapping.
1046 protected function getTableMapping() {
1047 return $this->entityManager->getStorage($this->definition['entity_type'])->getTableMapping();
1053 public function getValue(ResultRow $values, $field = NULL) {
1054 $entity = $this->getEntity($values);
1055 // Retrieve the translated object.
1056 $translated_entity = $this->getEntityFieldRenderer()->getEntityTranslation($entity, $values);
1058 // Some bundles might not have a specific field, in which case the entity
1059 // (potentially a fake one) doesn't have it either.
1060 /** @var \Drupal\Core\Field\FieldItemListInterface $field_item_list */
1061 $field_item_list = isset($translated_entity->{$this->definition['field_name']}) ? $translated_entity->{$this->definition['field_name']} : NULL;
1063 if (!isset($field_item_list)) {
1064 // There isn't anything we can do without a valid field.
1068 $field_item_definition = $field_item_list->getFieldDefinition();
1071 foreach ($field_item_list as $field_item) {
1072 /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
1074 $values[] = $field_item->$field;
1076 // Find the value using the main property of the field. If no main
1077 // property is provided fall back to 'value'.
1078 elseif ($main_property_name = $field_item->mainPropertyName()) {
1079 $values[] = $field_item->{$main_property_name};
1082 $values[] = $field_item->value;
1085 if ($field_item_definition->getFieldStorageDefinition()->getCardinality() == 1) {
1086 return reset($values);
1096 public function onDependencyRemoval(array $dependencies) {
1097 // See if this handler is responsible for any of the dependencies being
1098 // removed. If this is the case, indicate that this handler needs to be
1099 // removed from the View.
1101 // Get all the current dependencies for this handler.
1102 $current_dependencies = $this->calculateDependencies();
1103 foreach ($current_dependencies as $group => $dependency_list) {
1104 // Check if any of the handler dependencies match the dependencies being
1106 foreach ($dependency_list as $config_key) {
1107 if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) {
1108 // This handlers dependency matches a dependency being removed,
1109 // indicate that this handler needs to be removed.