1<?php 2 3namespace Drupal\views\Plugin\views\field; 4 5use Drupal\Component\Plugin\DependentPluginInterface; 6use Drupal\Component\Utility\Xss; 7use Drupal\Core\Cache\Cache; 8use Drupal\Core\Cache\CacheableDependencyInterface; 9use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; 10use Drupal\Core\Entity\EntityFieldManagerInterface; 11use Drupal\Core\Entity\EntityInterface; 12use Drupal\Core\Entity\EntityRepositoryInterface; 13use Drupal\Core\Entity\EntityTypeManagerInterface; 14use Drupal\Core\Field\FieldStorageDefinitionInterface; 15use Drupal\Core\Field\FieldTypePluginManagerInterface; 16use Drupal\Core\Field\FormatterPluginManager; 17use Drupal\Core\Form\FormHelper; 18use Drupal\Core\Form\FormStateInterface; 19use Drupal\Core\Language\LanguageManagerInterface; 20use Drupal\Core\Plugin\PluginDependencyTrait; 21use Drupal\Core\Render\BubbleableMetadata; 22use Drupal\Core\Render\Element; 23use Drupal\Core\Render\RendererInterface; 24use Drupal\Core\Session\AccountInterface; 25use Drupal\Core\TypedData\TypedDataInterface; 26use Drupal\views\FieldAPIHandlerTrait; 27use Drupal\views\Entity\Render\EntityFieldRenderer; 28use Drupal\views\Plugin\views\display\DisplayPluginBase; 29use Drupal\views\Plugin\DependentWithRemovalPluginInterface; 30use Drupal\views\ResultRow; 31use Drupal\views\ViewExecutable; 32use Symfony\Component\DependencyInjection\ContainerInterface; 33 34/** 35 * A field that displays entity field data. 36 * 37 * @ingroup views_field_handlers 38 * 39 * @ViewsField("field") 40 */ 41class EntityField extends FieldPluginBase implements CacheableDependencyInterface, MultiItemsFieldHandlerInterface, DependentWithRemovalPluginInterface { 42 43 use FieldAPIHandlerTrait; 44 use PluginDependencyTrait; 45 use DeprecatedServicePropertyTrait; 46 47 /** 48 * {@inheritdoc} 49 */ 50 protected $deprecatedProperties = ['entityManager' => 'entity.manager']; 51 52 /** 53 * An array to store field renderable arrays for use by renderItems(). 54 * 55 * @var array 56 */ 57 public $items = []; 58 59 /** 60 * Does the field supports multiple field values. 61 * 62 * @var bool 63 */ 64 public $multiple; 65 66 /** 67 * Does the rendered fields get limited. 68 * 69 * @var bool 70 */ 71 public $limit_values; 72 73 /** 74 * A shortcut for $view->base_table. 75 * 76 * @var string 77 */ 78 public $base_table; 79 80 /** 81 * An array of formatter options. 82 * 83 * @var array 84 */ 85 protected $formatterOptions; 86 87 /** 88 * The entity typemanager. 89 * 90 * @var \Drupal\Core\Entity\EntityTypeManagerInterface 91 */ 92 protected $entityTypeManager; 93 94 /** 95 * The entity repository service. 96 * 97 * @var \Drupal\Core\Entity\EntityRepositoryInterface 98 */ 99 protected $entityRepository; 100 101 /** 102 * The field formatter plugin manager. 103 * 104 * @var \Drupal\Core\Field\FormatterPluginManager 105 */ 106 protected $formatterPluginManager; 107 108 /** 109 * The language manager. 110 * 111 * @var \Drupal\Core\Language\LanguageManagerInterface 112 */ 113 protected $languageManager; 114 115 /** 116 * The renderer. 117 * 118 * @var \Drupal\Core\Render\RendererInterface 119 */ 120 protected $renderer; 121 122 /** 123 * The field type plugin manager. 124 * 125 * @var \Drupal\Core\Field\FieldTypePluginManagerInterface 126 */ 127 protected $fieldTypePluginManager; 128 129 /** 130 * Static cache for ::getEntityFieldRenderer(). 131 * 132 * @var \Drupal\views\Entity\Render\EntityFieldRenderer 133 */ 134 protected $entityFieldRenderer; 135 136 /** 137 * Constructs a \Drupal\field\Plugin\views\field\Field object. 138 * 139 * @param array $configuration 140 * A configuration array containing information about the plugin instance. 141 * @param string $plugin_id 142 * The plugin_id for the plugin instance. 143 * @param mixed $plugin_definition 144 * The plugin implementation definition. 145 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager 146 * The entity type manager. 147 * @param \Drupal\Core\Field\FormatterPluginManager $formatter_plugin_manager 148 * The field formatter plugin manager. 149 * @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_plugin_manager 150 * The field plugin type manager. 151 * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager 152 * The language manager. 153 * @param \Drupal\Core\Render\RendererInterface $renderer 154 * The renderer. 155 * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository 156 * The entity repository. 157 * @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager 158 * The entity field manager. 159 */ 160 public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, FormatterPluginManager $formatter_plugin_manager, FieldTypePluginManagerInterface $field_type_plugin_manager, LanguageManagerInterface $language_manager, RendererInterface $renderer, EntityRepositoryInterface $entity_repository = NULL, EntityFieldManagerInterface $entity_field_manager = NULL) { 161 parent::__construct($configuration, $plugin_id, $plugin_definition); 162 163 $this->entityTypeManager = $entity_type_manager; 164 $this->formatterPluginManager = $formatter_plugin_manager; 165 $this->fieldTypePluginManager = $field_type_plugin_manager; 166 $this->languageManager = $language_manager; 167 $this->renderer = $renderer; 168 169 // @todo Unify 'entity field'/'field_name' instead of converting back and 170 // forth. https://www.drupal.org/node/2410779 171 if (isset($this->definition['entity field'])) { 172 $this->definition['field_name'] = $this->definition['entity field']; 173 } 174 175 if (!$entity_repository) { 176 @trigger_error('Calling EntityField::__construct() with the $entity_repository argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); 177 $entity_repository = \Drupal::service('entity.repository'); 178 } 179 $this->entityRepository = $entity_repository; 180 181 if (!$entity_field_manager) { 182 @trigger_error('Calling EntityField::__construct() with the $entity_field_manager argument is supported in drupal:8.7.0 and will be required before drupal:9.0.0. See https://www.drupal.org/node/2549139.', E_USER_DEPRECATED); 183 $entity_field_manager = \Drupal::service('entity_field.manager'); 184 } 185 $this->entityFieldManager = $entity_field_manager; 186 } 187 188 /** 189 * {@inheritdoc} 190 */ 191 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { 192 return new static( 193 $configuration, 194 $plugin_id, 195 $plugin_definition, 196 $container->get('entity_type.manager'), 197 $container->get('plugin.manager.field.formatter'), 198 $container->get('plugin.manager.field.field_type'), 199 $container->get('language_manager'), 200 $container->get('renderer'), 201 $container->get('entity.repository'), 202 $container->get('entity_field.manager') 203 ); 204 } 205 206 /** 207 * {@inheritdoc} 208 */ 209 public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) { 210 parent::init($view, $display, $options); 211 212 $this->multiple = FALSE; 213 $this->limit_values = FALSE; 214 215 $field_definition = $this->getFieldDefinition(); 216 $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality(); 217 if ($field_definition->getFieldStorageDefinition()->isMultiple()) { 218 $this->multiple = TRUE; 219 220 // If "Display all values in the same row" is FALSE, then we always limit 221 // in order to show a single unique value per row. 222 if (!$this->options['group_rows']) { 223 $this->limit_values = TRUE; 224 } 225 226 // If "First and last only" is chosen, limit the values 227 if (!empty($this->options['delta_first_last'])) { 228 $this->limit_values = TRUE; 229 } 230 231 // Otherwise, we only limit values if the user hasn't selected "all", 0, or 232 // the value matching field cardinality. 233 if ((($this->options['delta_limit'] > 0) && ($this->options['delta_limit'] != $cardinality)) || intval($this->options['delta_offset'])) { 234 $this->limit_values = TRUE; 235 } 236 } 237 } 238 239 /** 240 * {@inheritdoc} 241 */ 242 public function access(AccountInterface $account) { 243 $access_control_handler = $this->entityTypeManager->getAccessControlHandler($this->getEntityType()); 244 return $access_control_handler->fieldAccess('view', $this->getFieldDefinition(), $account); 245 } 246 247 /** 248 * Called to add the field to a query. 249 * 250 * By default, all needed data is taken from entities loaded by the query 251 * plugin. Columns are added only if they are used in groupings. 252 */ 253 public function query($use_groupby = FALSE) { 254 $fields = $this->additional_fields; 255 // No need to add the entity type. 256 $entity_type_key = array_search('entity_type', $fields); 257 if ($entity_type_key !== FALSE) { 258 unset($fields[$entity_type_key]); 259 } 260 261 if ($use_groupby) { 262 // Add the fields that we're actually grouping on. 263 $options = []; 264 if ($this->options['group_column'] != 'entity_id') { 265 $options = [$this->options['group_column'] => $this->options['group_column']]; 266 } 267 $options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : []; 268 269 // Go through the list and determine the actual column name from field api. 270 $fields = []; 271 $table_mapping = $this->getTableMapping(); 272 $field_definition = $this->getFieldStorageDefinition(); 273 274 foreach ($options as $column) { 275 $fields[$column] = $table_mapping->getFieldColumnName($field_definition, $column); 276 } 277 278 $this->group_fields = $fields; 279 } 280 281 // Add additional fields (and the table join itself) if needed. 282 if ($this->add_field_table($use_groupby)) { 283 $this->ensureMyTable(); 284 $this->addAdditionalFields($fields); 285 } 286 287 // Let the entity field renderer alter the query if needed. 288 $this->getEntityFieldRenderer()->query($this->query, $this->relationship); 289 } 290 291 /** 292 * Determine if the field table should be added to the query. 293 */ 294 public function add_field_table($use_groupby) { 295 // Grouping is enabled. 296 if ($use_groupby) { 297 return TRUE; 298 } 299 // This a multiple value field, but "group multiple values" is not checked. 300 if ($this->multiple && !$this->options['group_rows']) { 301 return TRUE; 302 } 303 return FALSE; 304 } 305 306 /** 307 * {@inheritdoc} 308 */ 309 public function clickSortable() { 310 // A field is not click sortable if it's a multiple field with 311 // "group multiple values" checked, since a click sort in that case would 312 // add a join to the field table, which would produce unwanted duplicates. 313 if ($this->multiple && $this->options['group_rows']) { 314 return FALSE; 315 } 316 317 // If field definition is set, use that. 318 if (isset($this->definition['click sortable'])) { 319 return (bool) $this->definition['click sortable']; 320 } 321 322 // Default to true. 323 return TRUE; 324 } 325 326 /** 327 * Called to determine what to tell the clicksorter. 328 */ 329 public function clickSort($order) { 330 // No column selected, can't continue. 331 if (empty($this->options['click_sort_column'])) { 332 return; 333 } 334 335 $this->ensureMyTable(); 336 $field_storage_definition = $this->getFieldStorageDefinition(); 337 $column = $this->getTableMapping()->getFieldColumnName($field_storage_definition, $this->options['click_sort_column']); 338 if (!isset($this->aliases[$column])) { 339 // Column is not in query; add a sort on it (without adding the column). 340 $this->aliases[$column] = $this->tableAlias . '.' . $column; 341 } 342 $this->query->addOrderBy(NULL, NULL, $order, $this->aliases[$column]); 343 } 344 345 /** 346 * Gets the field storage definition. 347 * 348 * @return \Drupal\Core\Field\FieldStorageDefinitionInterface 349 * The field storage definition used by this handler. 350 */ 351 protected function getFieldStorageDefinition() { 352 $entity_type_id = $this->definition['entity_type']; 353 $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id); 354 355 // @todo Unify 'entity field'/'field_name' instead of converting back and 356 // forth. https://www.drupal.org/node/2410779 357 if (isset($this->definition['field_name']) && isset($field_storage_definitions[$this->definition['field_name']])) { 358 return $field_storage_definitions[$this->definition['field_name']]; 359 } 360 361 if (isset($this->definition['entity field']) && isset($field_storage_definitions[$this->definition['entity field']])) { 362 return $field_storage_definitions[$this->definition['entity field']]; 363 } 364 365 // The list of field storage definitions above does not include computed 366 // base fields, so we need to explicitly fetch a list of all base fields in 367 // order to support them. 368 // @see \Drupal\Core\Entity\EntityFieldManager::getFieldStorageDefinitions() 369 $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id); 370 if (isset($this->definition['field_name']) && isset($base_fields[$this->definition['field_name']])) { 371 return $base_fields[$this->definition['field_name']]->getFieldStorageDefinition(); 372 } 373 } 374 375 /** 376 * {@inheritdoc} 377 */ 378 protected function defineOptions() { 379 $options = parent::defineOptions(); 380 381 $field_storage_definition = $this->getFieldStorageDefinition(); 382 $field_type = $this->fieldTypePluginManager->getDefinition($field_storage_definition->getType()); 383 $column_names = array_keys($field_storage_definition->getColumns()); 384 $default_column = ''; 385 // Try to determine a sensible default. 386 if (count($column_names) == 1) { 387 $default_column = $column_names[0]; 388 } 389 elseif (in_array('value', $column_names)) { 390 $default_column = 'value'; 391 } 392 393 // If the field has a "value" column, we probably need that one. 394 $options['click_sort_column'] = [ 395 'default' => $default_column, 396 ]; 397 398 if (isset($this->definition['default_formatter'])) { 399 $options['type'] = ['default' => $this->definition['default_formatter']]; 400 } 401 elseif (isset($field_type['default_formatter'])) { 402 $options['type'] = ['default' => $field_type['default_formatter']]; 403 } 404 else { 405 $options['type'] = ['default' => '']; 406 } 407 408 $options['settings'] = [ 409 'default' => isset($this->definition['default_formatter_settings']) ? $this->definition['default_formatter_settings'] : [], 410 ]; 411 $options['group_column'] = [ 412 'default' => $default_column, 413 ]; 414 $options['group_columns'] = [ 415 'default' => [], 416 ]; 417 418 // Options used for multiple value fields. 419 $options['group_rows'] = [ 420 'default' => TRUE, 421 ]; 422 // If we know the exact number of allowed values, then that can be 423 // the default. Otherwise, default to 'all'. 424 $options['delta_limit'] = [ 425 'default' => ($field_storage_definition->getCardinality() > 1) ? $field_storage_definition->getCardinality() : 0, 426 ]; 427 $options['delta_offset'] = [ 428 'default' => 0, 429 ]; 430 $options['delta_reversed'] = [ 431 'default' => FALSE, 432 ]; 433 $options['delta_first_last'] = [ 434 'default' => FALSE, 435 ]; 436 437 $options['multi_type'] = [ 438 'default' => 'separator', 439 ]; 440 $options['separator'] = [ 441 'default' => ', ', 442 ]; 443 444 $options['field_api_classes'] = [ 445 'default' => FALSE, 446 ]; 447 448 return $options; 449 } 450 451 /** 452 * {@inheritdoc} 453 */ 454 public function buildOptionsForm(&$form, FormStateInterface $form_state) { 455 parent::buildOptionsForm($form, $form_state); 456 457 $field = $this->getFieldDefinition(); 458 $formatters = $this->formatterPluginManager->getOptions($field->getType()); 459 $column_names = array_keys($field->getColumns()); 460 461 // If this is a multiple value field, add its options. 462 if ($this->multiple) { 463 $this->multiple_options_form($form, $form_state); 464 } 465 466 // No need to ask the user anything if the field has only one column. 467 if (count($field->getColumns()) == 1) { 468 $form['click_sort_column'] = [ 469 '#type' => 'value', 470 '#value' => isset($column_names[0]) ? $column_names[0] : '', 471 ]; 472 } 473 else { 474 $form['click_sort_column'] = [ 475 '#type' => 'select', 476 '#title' => $this->t('Column used for click sorting'), 477 '#options' => array_combine($column_names, $column_names), 478 '#default_value' => $this->options['click_sort_column'], 479 '#description' => $this->t('Used by Style: Table to determine the actual column to click sort the field on. The default is usually fine.'), 480 ]; 481 } 482 483 $form['type'] = [ 484 '#type' => 'select', 485 '#title' => $this->t('Formatter'), 486 '#options' => $formatters, 487 '#default_value' => $this->options['type'], 488 '#ajax' => [ 489 'url' => views_ui_build_form_url($form_state), 490 ], 491 '#submit' => [[$this, 'submitTemporaryForm']], 492 '#executes_submit_callback' => TRUE, 493 ]; 494 495 $form['field_api_classes'] = [ 496 '#title' => $this->t('Use field template'), 497 '#type' => 'checkbox', 498 '#default_value' => $this->options['field_api_classes'], 499 '#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.'), 500 '#fieldset' => 'style_settings', 501 '#weight' => 20, 502 ]; 503 504 if ($this->multiple) { 505 $form['field_api_classes']['#description'] .= ' ' . $this->t('Checking this option will cause the group Display Type and Separator values to be ignored.'); 506 } 507 508 // Get the settings form. 509 $settings_form = ['#value' => []]; 510 $format = isset($form_state->getUserInput()['options']['type']) ? $form_state->getUserInput()['options']['type'] : $this->options['type']; 511 if ($formatter = $this->getFormatterInstance($format)) { 512 $settings_form = $formatter->settingsForm($form, $form_state); 513 // Convert field UI selector states to work in the Views field form. 514 FormHelper::rewriteStatesSelector($settings_form, "fields[{$field->getName()}][settings_edit_form]", 'options'); 515 } 516 $form['settings'] = $settings_form; 517 } 518 519 /** 520 * {@inheritdoc} 521 */ 522 public function submitFormCalculateOptions(array $options, array $form_state_options) { 523 // When we change the formatter type we don't want to keep any of the 524 // previous configured formatter settings, as there might be schema 525 // conflict. 526 unset($options['settings']); 527 $options = $form_state_options + $options; 528 if (!isset($options['settings'])) { 529 $options['settings'] = []; 530 } 531 return $options; 532 } 533 534 /** 535 * Provide options for multiple value fields. 536 */ 537 public function multiple_options_form(&$form, FormStateInterface $form_state) { 538 $field = $this->getFieldDefinition(); 539 540 $form['multiple_field_settings'] = [ 541 '#type' => 'details', 542 '#title' => $this->t('Multiple field settings'), 543 '#weight' => 5, 544 ]; 545 546 $form['group_rows'] = [ 547 '#title' => $this->t('Display all values in the same row'), 548 '#type' => 'checkbox', 549 '#default_value' => $this->options['group_rows'], 550 '#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.'), 551 '#fieldset' => 'multiple_field_settings', 552 ]; 553 554 // Make the string translatable by keeping it as a whole rather than 555 // translating prefix and suffix separately. 556 list($prefix, $suffix) = explode('@count', $this->t('Display @count value(s)')); 557 558 if ($field->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) { 559 $type = 'textfield'; 560 $options = NULL; 561 $size = 5; 562 } 563 else { 564 $type = 'select'; 565 $range = range(1, $field->getCardinality()); 566 $options = array_combine($range, $range); 567 $size = 1; 568 } 569 $form['multi_type'] = [ 570 '#type' => 'radios', 571 '#title' => $this->t('Display type'), 572 '#options' => [ 573 'ul' => $this->t('Unordered list'), 574 'ol' => $this->t('Ordered list'), 575 'separator' => $this->t('Simple separator'), 576 ], 577 '#states' => [ 578 'visible' => [ 579 ':input[name="options[group_rows]"]' => ['checked' => TRUE], 580 ], 581 ], 582 '#default_value' => $this->options['multi_type'], 583 '#fieldset' => 'multiple_field_settings', 584 ]; 585 586 $form['separator'] = [ 587 '#type' => 'textfield', 588 '#title' => $this->t('Separator'), 589 '#default_value' => $this->options['separator'], 590 '#states' => [ 591 'visible' => [ 592 ':input[name="options[group_rows]"]' => ['checked' => TRUE], 593 ':input[name="options[multi_type]"]' => ['value' => 'separator'], 594 ], 595 ], 596 '#fieldset' => 'multiple_field_settings', 597 ]; 598 599 $form['delta_limit'] = [ 600 '#type' => $type, 601 '#size' => $size, 602 '#field_prefix' => $prefix, 603 '#field_suffix' => $suffix, 604 '#options' => $options, 605 '#default_value' => $this->options['delta_limit'], 606 '#prefix' => '<div class="container-inline">', 607 '#states' => [ 608 'visible' => [ 609 ':input[name="options[group_rows]"]' => ['checked' => TRUE], 610 ], 611 ], 612 '#fieldset' => 'multiple_field_settings', 613 ]; 614 615 list($prefix, $suffix) = explode('@count', $this->t('starting from @count')); 616 $form['delta_offset'] = [ 617 '#type' => 'textfield', 618 '#size' => 5, 619 '#field_prefix' => $prefix, 620 '#field_suffix' => $suffix, 621 '#default_value' => $this->options['delta_offset'], 622 '#states' => [ 623 'visible' => [ 624 ':input[name="options[group_rows]"]' => ['checked' => TRUE], 625 ], 626 ], 627 '#description' => $this->t('(first item is 0)'), 628 '#fieldset' => 'multiple_field_settings', 629 ]; 630 $form['delta_reversed'] = [ 631 '#title' => $this->t('Reversed'), 632 '#type' => 'checkbox', 633 '#default_value' => $this->options['delta_reversed'], 634 '#suffix' => $suffix, 635 '#states' => [ 636 'visible' => [ 637 ':input[name="options[group_rows]"]' => ['checked' => TRUE], 638 ], 639 ], 640 '#description' => $this->t('(start from last values)'), 641 '#fieldset' => 'multiple_field_settings', 642 ]; 643 $form['delta_first_last'] = [ 644 '#title' => $this->t('First and last only'), 645 '#type' => 'checkbox', 646 '#default_value' => $this->options['delta_first_last'], 647 '#suffix' => '</div>', 648 '#states' => [ 649 'visible' => [ 650 ':input[name="options[group_rows]"]' => ['checked' => TRUE], 651 ], 652 ], 653 '#fieldset' => 'multiple_field_settings', 654 ]; 655 } 656 657 /** 658 * Extend the groupby form with group columns. 659 */ 660 public function buildGroupByForm(&$form, FormStateInterface $form_state) { 661 parent::buildGroupByForm($form, $form_state); 662 // With "field API" fields, the column target of the grouping function 663 // and any additional grouping columns must be specified. 664 665 $field_columns = array_keys($this->getFieldDefinition()->getColumns()); 666 $group_columns = [ 667 'entity_id' => $this->t('Entity ID'), 668 ] + array_map('ucfirst', array_combine($field_columns, $field_columns)); 669 670 $form['group_column'] = [ 671 '#type' => 'select', 672 '#title' => $this->t('Group column'), 673 '#default_value' => $this->options['group_column'], 674 '#description' => $this->t('Select the column of this field to apply the grouping function selected above.'), 675 '#options' => $group_columns, 676 ]; 677 678 $options = [ 679 'bundle' => 'Bundle', 680 'language' => 'Language', 681 'entity_type' => 'Entity_type', 682 ]; 683 // Add on defined fields, noting that they're prefixed with the field name. 684 $form['group_columns'] = [ 685 '#type' => 'checkboxes', 686 '#title' => $this->t('Group columns (additional)'), 687 '#default_value' => $this->options['group_columns'], 688 '#description' => $this->t('Select any additional columns of this field to include in the query and to group on.'), 689 '#options' => $options + $group_columns, 690 ]; 691 } 692 693 public function submitGroupByForm(&$form, FormStateInterface $form_state) { 694 parent::submitGroupByForm($form, $form_state); 695 $item = &$form_state->get('handler')->options; 696 697 // Add settings for "field API" fields. 698 $item['group_column'] = $form_state->getValue(['options', 'group_column']); 699 $item['group_columns'] = array_filter($form_state->getValue(['options', 'group_columns'])); 700 } 701 702 /** 703 * Render all items in this field together. 704 * 705 * When using advanced render, each possible item in the list is rendered 706 * individually. Then the items are all pasted together. 707 */ 708 public function renderItems($items) { 709 if (!empty($items)) { 710 $items = $this->prepareItemsByDelta($items); 711 if ($this->options['multi_type'] == 'separator' || !$this->options['group_rows']) { 712 $separator = $this->options['multi_type'] == 'separator' ? Xss::filterAdmin($this->options['separator']) : ''; 713 $build = [ 714 '#type' => 'inline_template', 715 '#template' => '{{ items | safe_join(separator) }}', 716 '#context' => ['separator' => $separator, 'items' => $items], 717 ]; 718 } 719 else { 720 $build = [ 721 '#theme' => 'item_list', 722 '#items' => $items, 723 '#title' => NULL, 724 '#list_type' => $this->options['multi_type'], 725 ]; 726 } 727 return $this->renderer->render($build); 728 } 729 } 730 731 /** 732 * Adapts the $items according to the delta configuration. 733 * 734 * This selects displayed deltas, reorders items, and takes offsets into 735 * account. 736 * 737 * @param array $all_values 738 * The items for individual rendering. 739 * 740 * @return array 741 * The manipulated items. 742 */ 743 protected function prepareItemsByDelta(array $all_values) { 744 if ($this->options['delta_reversed']) { 745 $all_values = array_reverse($all_values); 746 } 747 748 // We are supposed to show only certain deltas. 749 if ($this->limit_values) { 750 $row = $this->view->result[$this->view->row_index]; 751 752 // Offset is calculated differently when row grouping for a field is not 753 // enabled. Since there are multiple rows, delta needs to be taken into 754 // account, so that different values are shown per row. 755 if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($row->{$this->aliases['delta']})) { 756 $delta_limit = 1; 757 $offset = $row->{$this->aliases['delta']}; 758 } 759 // Single fields don't have a delta available so choose 0. 760 elseif (!$this->options['group_rows'] && !$this->multiple) { 761 $delta_limit = 1; 762 $offset = 0; 763 } 764 else { 765 $delta_limit = $this->options['delta_limit']; 766 $offset = intval($this->options['delta_offset']); 767 768 // We should only get here in this case if there is an offset, and in 769 // that case we are limiting to all values after the offset. 770 if ($delta_limit === 0) { 771 $delta_limit = count($all_values) - $offset; 772 } 773 } 774 775 // Determine if only the first and last values should be shown. 776 $delta_first_last = $this->options['delta_first_last']; 777 778 $new_values = []; 779 for ($i = 0; $i < $delta_limit; $i++) { 780 $new_delta = $offset + $i; 781 782 if (isset($all_values[$new_delta])) { 783 // If first-last option was selected, only use the first and last 784 // values. 785 if (!$delta_first_last 786 // Use the first value. 787 || $new_delta == $offset 788 // Use the last value. 789 || $new_delta == ($delta_limit + $offset - 1)) { 790 $new_values[] = $all_values[$new_delta]; 791 } 792 } 793 } 794 $all_values = $new_values; 795 } 796 797 return $all_values; 798 } 799 800 /** 801 * {@inheritdoc} 802 */ 803 public function preRender(&$values) { 804 parent::preRender($values); 805 $this->getEntityFieldRenderer()->preRender($values); 806 } 807 808 /** 809 * Returns the entity field renderer. 810 * 811 * @return \Drupal\views\Entity\Render\EntityFieldRenderer 812 * The entity field renderer. 813 */ 814 protected function getEntityFieldRenderer() { 815 if (!isset($this->entityFieldRenderer)) { 816 // This can be invoked during field handler initialization in which case 817 // view fields are not set yet. 818 if (!empty($this->view->field)) { 819 foreach ($this->view->field as $field) { 820 // An entity field renderer can handle only a single relationship. 821 if ($field->relationship == $this->relationship && isset($field->entityFieldRenderer)) { 822 $this->entityFieldRenderer = $field->entityFieldRenderer; 823 break; 824 } 825 } 826 } 827 if (!isset($this->entityFieldRenderer)) { 828 $entity_type = $this->entityTypeManager->getDefinition($this->getEntityType()); 829 $this->entityFieldRenderer = new EntityFieldRenderer($this->view, $this->relationship, $this->languageManager, $entity_type, $this->entityTypeManager, $this->entityRepository); 830 } 831 } 832 return $this->entityFieldRenderer; 833 } 834 835 /** 836 * Gets an array of items for the field. 837 * 838 * @param \Drupal\views\ResultRow $values 839 * The result row object containing the values. 840 * 841 * @return array 842 * An array of items for the field. 843 */ 844 public function getItems(ResultRow $values) { 845 if (!$this->displayHandler->useGroupBy()) { 846 $build_list = $this->getEntityFieldRenderer()->render($values, $this); 847 } 848 else { 849 // For grouped results we need to retrieve a massaged entity having 850 // grouped field values to ensure that "grouped by" values, especially 851 // those with multiple cardinality work properly. See 852 // \Drupal\Tests\views\Kernel\QueryGroupByTest::testGroupByFieldWithCardinality. 853 $display = [ 854 'type' => $this->options['type'], 855 'settings' => $this->options['settings'], 856 'label' => 'hidden', 857 ]; 858 // Optional relationships may not provide an entity at all. So we can't 859 // use createEntityForGroupBy() for those rows. 860 if ($entity = $this->getEntity($values)) { 861 $entity = $this->createEntityForGroupBy($entity, $values); 862 // Some bundles might not have a specific field, in which case the faked 863 // entity doesn't have it either. 864 $build_list = isset($entity->{$this->definition['field_name']}) ? $entity->{$this->definition['field_name']}->view($display) : NULL; 865 } 866 else { 867 $build_list = NULL; 868 } 869 } 870 871 if (!$build_list) { 872 return []; 873 } 874 875 if ($this->options['field_api_classes']) { 876 return [['rendered' => $this->renderer->render($build_list)]]; 877 } 878 879 // Render using the formatted data itself. 880 $items = []; 881 // Each item is extracted and rendered separately, the top-level formatter 882 // render array itself is never rendered, so we extract its bubbleable 883 // metadata and add it to each child individually. 884 $bubbleable = BubbleableMetadata::createFromRenderArray($build_list); 885 foreach (Element::children($build_list) as $delta) { 886 BubbleableMetadata::createFromRenderArray($build_list[$delta]) 887 ->merge($bubbleable) 888 ->applyTo($build_list[$delta]); 889 $items[$delta] = [ 890 'rendered' => $build_list[$delta], 891 // Add the raw field items (for use in tokens). 892 'raw' => $build_list['#items'][$delta], 893 ]; 894 } 895 return $items; 896 } 897 898 /** 899 * Creates a fake entity with grouped field values. 900 * 901 * @param \Drupal\Core\Entity\EntityInterface $entity 902 * The entity to be processed. 903 * @param \Drupal\views\ResultRow $row 904 * The result row object containing the values. 905 * 906 * @return bool|\Drupal\Core\Entity\FieldableEntityInterface 907 * Returns a new entity object containing the grouped field values. 908 */ 909 protected function createEntityForGroupBy(EntityInterface $entity, ResultRow $row) { 910 // Retrieve the correct translation object. 911 $processed_entity = clone $this->getEntityFieldRenderer()->getEntityTranslation($entity, $row); 912 913 // Copy our group fields into the cloned entity. It is possible this will 914 // cause some weirdness, but there is only so much we can hope to do. 915 if (!empty($this->group_fields) && isset($entity->{$this->definition['field_name']})) { 916 // first, test to see if we have a base value. 917 $base_value = []; 918 // Note: We would copy original values here, but it can cause problems. 919 // For example, text fields store cached filtered values as 'safe_value' 920 // which does not appear anywhere in the field definition so we cannot 921 // affect it. Other side effects could happen similarly. 922 $data = FALSE; 923 foreach ($this->group_fields as $field_name => $column) { 924 if (property_exists($row, $this->aliases[$column])) { 925 $base_value[$field_name] = $row->{$this->aliases[$column]}; 926 if (isset($base_value[$field_name])) { 927 $data = TRUE; 928 } 929 } 930 } 931 932 // If any of our aggregated fields have data, fake it: 933 if ($data) { 934 // Now, overwrite the original value with our aggregated value. 935 // This overwrites it so there is always just one entry. 936 $processed_entity->{$this->definition['field_name']} = [$base_value]; 937 } 938 else { 939 $processed_entity->{$this->definition['field_name']} = []; 940 } 941 } 942 943 return $processed_entity; 944 } 945 946 public function render_item($count, $item) { 947 return render($item['rendered']); 948 } 949 950 protected function documentSelfTokens(&$tokens) { 951 $field = $this->getFieldDefinition(); 952 foreach ($field->getColumns() as $id => $column) { 953 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = $this->t('Raw @column', ['@column' => $id]); 954 } 955 } 956 957 protected function addSelfTokens(&$tokens, $item) { 958 $field = $this->getFieldDefinition(); 959 foreach ($field->getColumns() as $id => $column) { 960 // Use \Drupal\Component\Utility\Xss::filterAdmin() because it's user data 961 // and we can't be sure it is safe. We know nothing about the data, 962 // though, so we can't really do much else. 963 if (isset($item['raw'])) { 964 $raw = $item['raw']; 965 966 if (is_array($raw)) { 967 if (isset($raw[$id]) && is_scalar($raw[$id])) { 968 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($raw[$id]); 969 } 970 else { 971 // Make sure that empty values are replaced as well. 972 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = ''; 973 } 974 } 975 976 if (is_object($raw)) { 977 $property = $raw->get($id); 978 // Check if TypedDataInterface is implemented so we know how to render 979 // the item as a string. 980 if (!empty($property) && $property instanceof TypedDataInterface) { 981 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($property->getString()); 982 } 983 else { 984 // Make sure that empty values are replaced as well. 985 $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = ''; 986 } 987 } 988 } 989 } 990 } 991 992 /** 993 * Returns the field formatter instance. 994 * 995 * @return \Drupal\Core\Field\FormatterInterface|null 996 * The field formatter instance. 997 */ 998 protected function getFormatterInstance($format = NULL) { 999 if (!isset($format)) { 1000 $format = $this->options['type']; 1001 } 1002 $settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($format); 1003 1004 $options = [ 1005 'field_definition' => $this->getFieldDefinition(), 1006 'configuration' => [ 1007 'type' => $format, 1008 'settings' => $settings, 1009 'label' => '', 1010 'weight' => 0, 1011 ], 1012 'view_mode' => '_custom', 1013 ]; 1014 1015 return $this->formatterPluginManager->getInstance($options); 1016 } 1017 1018 /** 1019 * {@inheritdoc} 1020 */ 1021 public function calculateDependencies() { 1022 $this->dependencies = parent::calculateDependencies(); 1023 1024 // Add the module providing the configured field storage as a dependency. 1025 if (($field_storage_definition = $this->getFieldStorageDefinition()) && $field_storage_definition instanceof EntityInterface) { 1026 $this->dependencies['config'][] = $field_storage_definition->getConfigDependencyName(); 1027 } 1028 if (!empty($this->options['type'])) { 1029 // Add the module providing the formatter. 1030 $this->dependencies['module'][] = $this->formatterPluginManager->getDefinition($this->options['type'])['provider']; 1031 1032 // Add the formatter's dependencies. 1033 if (($formatter = $this->getFormatterInstance()) && $formatter instanceof DependentPluginInterface) { 1034 $this->calculatePluginDependencies($formatter); 1035 } 1036 } 1037 1038 return $this->dependencies; 1039 } 1040 1041 /** 1042 * {@inheritdoc} 1043 */ 1044 public function getCacheMaxAge() { 1045 return Cache::PERMANENT; 1046 } 1047 1048 /** 1049 * {@inheritdoc} 1050 */ 1051 public function getCacheContexts() { 1052 return $this->getEntityFieldRenderer()->getCacheContexts(); 1053 } 1054 1055 /** 1056 * {@inheritdoc} 1057 */ 1058 public function getCacheTags() { 1059 $field_definition = $this->getFieldDefinition(); 1060 $field_storage_definition = $this->getFieldStorageDefinition(); 1061 return Cache::mergeTags( 1062 $field_definition instanceof CacheableDependencyInterface ? $field_definition->getCacheTags() : [], 1063 $field_storage_definition instanceof CacheableDependencyInterface ? $field_storage_definition->getCacheTags() : [] 1064 ); 1065 } 1066 1067 /** 1068 * Gets the table mapping for the entity type of the field. 1069 * 1070 * @return \Drupal\Core\Entity\Sql\DefaultTableMapping 1071 * The table mapping. 1072 */ 1073 protected function getTableMapping() { 1074 return $this->entityTypeManager->getStorage($this->definition['entity_type'])->getTableMapping(); 1075 } 1076 1077 /** 1078 * {@inheritdoc} 1079 */ 1080 public function getValue(ResultRow $values, $field = NULL) { 1081 $entity = $this->getEntity($values); 1082 1083 // Ensure the object is not NULL before attempting to translate it. 1084 if ($entity === NULL) { 1085 return NULL; 1086 } 1087 1088 // Retrieve the translated object. 1089 $translated_entity = $this->getEntityFieldRenderer()->getEntityTranslation($entity, $values); 1090 1091 // Some bundles might not have a specific field, in which case the entity 1092 // (potentially a fake one) doesn't have it either. 1093 /** @var \Drupal\Core\Field\FieldItemListInterface $field_item_list */ 1094 $field_item_list = isset($translated_entity->{$this->definition['field_name']}) ? $translated_entity->{$this->definition['field_name']} : NULL; 1095 1096 if (!isset($field_item_list)) { 1097 // There isn't anything we can do without a valid field. 1098 return NULL; 1099 } 1100 1101 $field_item_definition = $field_item_list->getFieldDefinition(); 1102 1103 $values = []; 1104 foreach ($field_item_list as $field_item) { 1105 /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ 1106 if ($field) { 1107 $values[] = $field_item->$field; 1108 } 1109 // Find the value using the main property of the field. If no main 1110 // property is provided fall back to 'value'. 1111 elseif ($main_property_name = $field_item->mainPropertyName()) { 1112 $values[] = $field_item->{$main_property_name}; 1113 } 1114 else { 1115 $values[] = $field_item->value; 1116 } 1117 } 1118 if ($field_item_definition->getFieldStorageDefinition()->getCardinality() == 1) { 1119 return reset($values); 1120 } 1121 else { 1122 return $values; 1123 } 1124 } 1125 1126 /** 1127 * {@inheritdoc} 1128 */ 1129 public function onDependencyRemoval(array $dependencies) { 1130 // See if this handler is responsible for any of the dependencies being 1131 // removed. If this is the case, indicate that this handler needs to be 1132 // removed from the View. 1133 $remove = FALSE; 1134 // Get all the current dependencies for this handler. 1135 $current_dependencies = $this->calculateDependencies(); 1136 foreach ($current_dependencies as $group => $dependency_list) { 1137 // Check if any of the handler dependencies match the dependencies being 1138 // removed. 1139 foreach ($dependency_list as $config_key) { 1140 if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) { 1141 // This handlers dependency matches a dependency being removed, 1142 // indicate that this handler needs to be removed. 1143 $remove = TRUE; 1144 break 2; 1145 } 1146 } 1147 } 1148 return $remove; 1149 } 1150 1151} 1152