5 * Provide views data that isn't tied to any other module.
8 use Drupal\Component\Utility\NestedArray;
9 use Drupal\Core\Entity\EntityStorageInterface;
10 use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
11 use Drupal\Core\Render\Markup;
12 use Drupal\field\FieldConfigInterface;
13 use Drupal\field\FieldStorageConfigInterface;
14 use Drupal\system\ActionConfigEntityInterface;
17 * Implements hook_views_data().
19 function views_views_data() {
20 $data['views']['table']['group'] = t('Global');
21 $data['views']['table']['join'] = [
22 // #global is a special flag which allows a table to appear all the time.
26 $data['views']['random'] = [
27 'title' => t('Random'),
28 'help' => t('Randomize the display order.'),
34 $data['views']['null'] = [
36 'help' => t('Allow a contextual filter value to be ignored. The query will not be altered by this contextual filter value. Can be used when contextual filter values come from the URL, and a part of the URL needs to be ignored.'),
42 $data['views']['nothing'] = [
43 'title' => t('Custom text'),
44 'help' => t('Provide custom text or link.'),
50 $data['views']['counter'] = [
51 'title' => t('View result counter'),
52 'help' => t('Displays the actual position of the view result'),
58 $data['views']['area'] = [
59 'title' => t('Text area'),
60 'help' => t('Provide markup text for the area.'),
66 $data['views']['area_text_custom'] = [
67 'title' => t('Unfiltered text'),
68 'help' => t('Add unrestricted, custom text or markup. This is similar to the custom text field.'),
70 'id' => 'text_custom',
74 $data['views']['title'] = [
75 'title' => t('Title override'),
76 'help' => t('Override the default view title for this view. This is useful to display an alternative title when a view is empty.'),
79 'sub_type' => 'empty',
83 $data['views']['view'] = [
84 'title' => t('View area'),
85 'help' => t('Insert a view inside an area.'),
91 $data['views']['result'] = [
92 'title' => t('Result summary'),
93 'help' => t('Shows result summary, for example the items per page.'),
99 $data['views']['messages'] = [
100 'title' => t('Messages'),
101 'help' => t('Displays messages in an area.'),
107 $data['views']['http_status_code'] = [
108 'title' => t('Response status code'),
109 'help' => t('Alter the HTTP response status code used by this view, mostly helpful for empty results.'),
111 'id' => 'http_status_code',
115 $data['views']['combine'] = [
116 'title' => t('Combine fields filter'),
117 'help' => t('Combine multiple fields together and search by them.'),
123 $data['views']['dropbutton'] = [
124 'title' => t('Dropbutton'),
125 'help' => t('Display fields in a dropbutton.'),
127 'id' => 'dropbutton',
131 // Registers an entity area handler per entity type.
132 foreach (\Drupal::entityManager()->getDefinitions() as $entity_type_id => $entity_type) {
133 // Excludes entity types, which cannot be rendered.
134 if ($entity_type->hasViewBuilderClass()) {
135 $label = $entity_type->getLabel();
136 $data['views']['entity_' . $entity_type_id] = [
137 'title' => t('Rendered entity - @label', ['@label' => $label]),
138 'help' => t('Displays a rendered @label entity in an area.', ['@label' => $label]),
140 'entity_type' => $entity_type_id,
147 // Registers an action bulk form per entity.
148 foreach (\Drupal::entityManager()->getDefinitions() as $entity_type => $entity_info) {
149 $actions = array_filter(\Drupal::entityManager()->getStorage('action')->loadMultiple(), function (ActionConfigEntityInterface $action) use ($entity_type) {
150 return $action->getType() == $entity_type;
152 if (empty($actions)) {
155 $data[$entity_info->getBaseTable()][$entity_type . '_bulk_form'] = [
156 'title' => t('Bulk update'),
157 'help' => t('Allows users to apply an action to one or more items.'),
164 // Registers views data for the entity itself.
165 foreach (\Drupal::entityManager()->getDefinitions() as $entity_type_id => $entity_type) {
166 if ($entity_type->hasHandlerClass('views_data')) {
167 /** @var \Drupal\views\EntityViewsDataInterface $views_data */
168 $views_data = \Drupal::entityManager()->getHandler($entity_type_id, 'views_data');
169 $data = NestedArray::mergeDeep($data, $views_data->getViewsData());
174 // Field modules can implement hook_field_views_data() to override the default
175 // behavior for adding fields.
176 $module_handler = \Drupal::moduleHandler();
178 $entity_manager = \Drupal::entityManager();
179 if ($entity_manager->hasDefinition('field_storage_config')) {
180 /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
181 foreach ($entity_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
182 if (_views_field_get_entity_type_storage($field_storage)) {
183 $result = (array) $module_handler->invoke($field_storage->getTypeProvider(), 'field_views_data', [$field_storage]);
184 if (empty($result)) {
185 $result = views_field_default_views_data($field_storage);
187 $module_handler->alter('field_views_data', $result, $field_storage);
189 if (is_array($result)) {
190 $data = NestedArray::mergeDeep($result, $data);
200 * Implements hook_views_data_alter().
202 * Field modules can implement hook_field_views_data_views_data_alter() to
203 * alter the views data on a per field basis. This is weirdly named so as
204 * not to conflict with the \Drupal::moduleHandler()->alter('field_views_data')
205 * in views_views_data().
207 function views_views_data_alter(&$data) {
208 $entity_manager = \Drupal::entityManager();
209 if (!$entity_manager->hasDefinition('field_storage_config')) {
212 /** @var \Drupal\field\FieldStorageConfigInterface $field_storage */
213 foreach ($entity_manager->getStorage('field_storage_config')->loadMultiple() as $field_storage) {
214 if (_views_field_get_entity_type_storage($field_storage)) {
215 $function = $field_storage->getTypeProvider() . '_field_views_data_views_data_alter';
216 if (function_exists($function)) {
217 $function($data, $field_storage);
224 * Determines whether the entity type the field appears in is SQL based.
226 * @param \Drupal\field\FieldStorageConfigInterface $field_storage
227 * The field storage definition.
229 * @return \Drupal\Core\Entity\Sql\SqlContentEntityStorage
230 * Returns the entity type storage if supported.
232 function _views_field_get_entity_type_storage(FieldStorageConfigInterface $field_storage) {
234 $entity_manager = \Drupal::entityManager();
235 if ($entity_manager->hasDefinition($field_storage->getTargetEntityTypeId())) {
236 $storage = $entity_manager->getStorage($field_storage->getTargetEntityTypeId());
237 $result = $storage instanceof SqlContentEntityStorage ? $storage : FALSE;
243 * Returns the label of a certain field.
245 * Therefore it looks up in all bundles to find the most used field.
247 function views_entity_field_label($entity_type, $field_name) {
250 // Count the amount of fields per label per field storage.
251 foreach (array_keys(\Drupal::entityManager()->getBundleInfo($entity_type)) as $bundle) {
252 $bundle_fields = array_filter(\Drupal::entityManager()->getFieldDefinitions($entity_type, $bundle), function ($field_definition) {
253 return $field_definition instanceof FieldConfigInterface;
255 if (isset($bundle_fields[$field_name])) {
256 $field = $bundle_fields[$field_name];
257 $label = $field->getLabel();
258 $label_counter[$label] = isset($label_counter[$label]) ? ++$label_counter[$label] : 1;
259 $all_labels[$label] = TRUE;
262 if (empty($label_counter)) {
263 return [$field_name, $all_labels];
265 // Sort the field labels by it most used label and return the most used one.
266 // If the counts are equal, sort by the label to ensure the result is
268 uksort($label_counter, function($a, $b) use ($label_counter) {
269 if ($label_counter[$a] === $label_counter[$b]) {
270 return strcmp($a, $b);
272 return $label_counter[$a] > $label_counter[$b] ? -1 : 1;
274 $label_counter = array_keys($label_counter);
275 return [$label_counter[0], $all_labels];
279 * Default views data implementation for a field.
281 * @param \Drupal\field\FieldStorageConfigInterface $field_storage
282 * The field definition.
285 * The default views data for the field.
287 function views_field_default_views_data(FieldStorageConfigInterface $field_storage) {
290 // Check the field type is available.
291 if (!\Drupal::service('plugin.manager.field.field_type')->hasDefinition($field_storage->getType())) {
294 // Check the field storage has fields.
295 if (!$field_storage->getBundles()) {
299 // Ignore custom storage too.
300 if ($field_storage->hasCustomStorage()) {
304 // Check whether the entity type storage is supported.
305 $storage = _views_field_get_entity_type_storage($field_storage);
310 $field_name = $field_storage->getName();
311 $field_columns = $field_storage->getColumns();
313 // Grab information about the entity type tables.
314 // We need to join to both the base table and the data table, if available.
315 $entity_manager = \Drupal::entityManager();
316 $entity_type_id = $field_storage->getTargetEntityTypeId();
317 $entity_type = $entity_manager->getDefinition($entity_type_id);
318 if (!$base_table = $entity_type->getBaseTable()) {
319 // We cannot do anything if for some reason there is no base table.
322 $entity_tables = [$base_table => $entity_type_id];
323 // Some entities may not have a data table.
324 $data_table = $entity_type->getDataTable();
326 $entity_tables[$data_table] = $entity_type_id;
328 $entity_revision_table = $entity_type->getRevisionTable();
329 $supports_revisions = $entity_type->hasKey('revision') && $entity_revision_table;
330 if ($supports_revisions) {
331 $entity_tables[$entity_revision_table] = $entity_type_id;
332 $entity_revision_data_table = $entity_type->getRevisionDataTable();
333 if ($entity_revision_data_table) {
334 $entity_tables[$entity_revision_data_table] = $entity_type_id;
338 // Description of the field tables.
339 // @todo Generalize this code to make it work with any table layout. See
340 // https://www.drupal.org/node/2079019.
341 $table_mapping = $storage->getTableMapping();
343 EntityStorageInterface::FIELD_LOAD_CURRENT => [
344 'table' => $table_mapping->getDedicatedDataTableName($field_storage),
345 'alias' => "{$entity_type_id}__{$field_name}",
348 if ($supports_revisions) {
349 $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION] = [
350 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
351 'alias' => "{$entity_type_id}_revision__{$field_name}",
355 // Build the relationships between the field table and the entity tables.
356 $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_CURRENT]['alias'];
358 // Tell Views how to join to the base table, via the data table.
359 $data[$table_alias]['table']['join'][$data_table] = [
360 'table' => $table_mapping->getDedicatedDataTableName($field_storage),
361 'left_field' => $entity_type->getKey('id'),
362 'field' => 'entity_id',
364 ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
365 ['left_field' => 'langcode', 'field' => 'langcode'],
370 // If there is no data table, just join directly.
371 $data[$table_alias]['table']['join'][$base_table] = [
372 'table' => $table_mapping->getDedicatedDataTableName($field_storage),
373 'left_field' => $entity_type->getKey('id'),
374 'field' => 'entity_id',
376 ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
381 if ($supports_revisions) {
382 $table_alias = $field_tables[EntityStorageInterface::FIELD_LOAD_REVISION]['alias'];
383 if ($entity_revision_data_table) {
384 // Tell Views how to join to the revision table, via the data table.
385 $data[$table_alias]['table']['join'][$entity_revision_data_table] = [
386 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
387 'left_field' => $entity_type->getKey('revision'),
388 'field' => 'revision_id',
390 ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
391 ['left_field' => 'langcode', 'field' => 'langcode'],
396 // If there is no data table, just join directly.
397 $data[$table_alias]['table']['join'][$entity_revision_table] = [
398 'table' => $table_mapping->getDedicatedRevisionTableName($field_storage),
399 'left_field' => $entity_type->getKey('revision'),
400 'field' => 'revision_id',
402 ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE],
408 $group_name = $entity_type->getLabel();
409 // Get the list of bundles the field appears in.
410 $bundles_names = $field_storage->getBundles();
411 // Build the list of additional fields to add to queries.
412 $add_fields = ['delta', 'langcode', 'bundle'];
413 foreach (array_keys($field_columns) as $column) {
414 $add_fields[] = $table_mapping->getFieldColumnName($field_storage, $column);
416 // Determine the label to use for the field. We don't have a label available
417 // at the field level, so we just go through all fields and take the one
418 // which is used the most frequently.
419 list($label, $all_labels) = views_entity_field_label($entity_type_id, $field_name);
421 // Expose data for the field as a whole.
422 foreach ($field_tables as $type => $table_info) {
423 $table = $table_info['table'];
424 $table_alias = $table_info['alias'];
426 if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) {
427 $group = $group_name;
428 $field_alias = $field_name;
431 $group = t('@group (historical data)', ['@group' => $group_name]);
432 $field_alias = $field_name . '-revision_id';
435 $data[$table_alias][$field_alias] = [
438 'title short' => $label,
439 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]),
442 // Go through and create a list of aliases for all possible combinations of
443 // entity type + name.
446 foreach ($all_labels as $label_name => $true) {
447 if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) {
448 if ($label != $label_name) {
450 'base' => $base_table,
451 'group' => $group_name,
452 'title' => $label_name,
453 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]),
455 $also_known[] = t('@group: @field', ['@group' => $group_name, '@field' => $label_name]);
458 elseif ($supports_revisions && $label != $label_name) {
461 'group' => t('@group (historical data)', ['@group' => $group_name]),
462 'title' => $label_name,
463 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $label]),
465 $also_known[] = t('@group (historical data): @field', ['@group' => $group_name, '@field' => $label_name]);
469 $data[$table_alias][$field_alias]['aliases'] = $aliases;
470 // The $also_known variable contains markup that is HTML escaped and that
471 // loses safeness when imploded. The help text is used in #description
472 // and therefore XSS admin filtered by default. Escaped HTML is not
473 // altered by XSS filtering, therefore it is safe to just concatenate the
474 // strings. Afterwards we mark the entire string as safe, so it won't be
475 // escaped, no matter where it is used.
476 // Considering the dual use of this help data (both as metadata and as
477 // help text), other patterns such as use of #markup would not be correct
479 $data[$table_alias][$field_alias]['help'] = Markup::create($data[$table_alias][$field_alias]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known));
482 $keys = array_keys($field_columns);
483 $real_field = reset($keys);
484 $data[$table_alias][$field_alias]['field'] = [
487 'field_name' => $field_name,
488 'entity_type' => $entity_type_id,
489 // Provide a real field for group by.
490 'real field' => $field_alias . '_' . $real_field,
491 'additional fields' => $add_fields,
492 // Default the element type to div, let the UI change it if necessary.
493 'element type' => 'div',
494 'is revision' => $type == EntityStorageInterface::FIELD_LOAD_REVISION,
498 // Expose data for each field property individually.
499 foreach ($field_columns as $column => $attributes) {
502 // Identify likely filters and arguments for each column based on field type.
503 switch ($attributes['type']) {
512 $argument = 'numeric';
514 if ($field_storage->getType() == 'boolean') {
520 // It does not make sense to sort by blob or text.
524 $argument = 'string';
529 if (count($field_columns) == 1 || $column == 'value') {
530 $title = t('@label (@name)', ['@label' => $label, '@name' => $field_name]);
531 $title_short = $label;
534 $title = t('@label (@name:@column)', ['@label' => $label, '@name' => $field_name, '@column' => $column]);
535 $title_short = t('@label:@column', ['@label' => $label, '@column' => $column]);
538 // Expose data for the property.
539 foreach ($field_tables as $type => $table_info) {
540 $table = $table_info['table'];
541 $table_alias = $table_info['alias'];
543 if ($type == EntityStorageInterface::FIELD_LOAD_CURRENT) {
544 $group = $group_name;
547 $group = t('@group (historical data)', ['@group' => $group_name]);
549 $column_real_name = $table_mapping->getFieldColumnName($field_storage, $column);
551 // Load all the fields from the table by default.
552 $additional_fields = $table_mapping->getAllColumns($table);
554 $data[$table_alias][$column_real_name] = [
557 'title short' => $title_short,
558 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]),
561 // Go through and create a list of aliases for all possible combinations of
562 // entity type + name.
565 foreach ($all_labels as $label_name => $true) {
566 if ($label != $label_name) {
567 if (count($field_columns) == 1 || $column == 'value') {
568 $alias_title = t('@label (@name)', ['@label' => $label_name, '@name' => $field_name]);
571 $alias_title = t('@label (@name:@column)', ['@label' => $label_name, '@name' => $field_name, '@column' => $column]);
574 'group' => $group_name,
575 'title' => $alias_title,
576 'help' => t('This is an alias of @group: @field.', ['@group' => $group_name, '@field' => $title]),
578 $also_known[] = t('@group: @field', ['@group' => $group_name, '@field' => $title]);
582 $data[$table_alias][$column_real_name]['aliases'] = $aliases;
583 // The $also_known variable contains markup that is HTML escaped and
584 // that loses safeness when imploded. The help text is used in
585 // #description and therefore XSS admin filtered by default. Escaped
586 // HTML is not altered by XSS filtering, therefore it is safe to just
587 // concatenate the strings. Afterwards we mark the entire string as
588 // safe, so it won't be escaped, no matter where it is used.
589 // Considering the dual use of this help data (both as metadata and as
590 // help text), other patterns such as use of #markup would not be
592 $data[$table_alias][$column_real_name]['help'] = Markup::create($data[$table_alias][$column_real_name]['help'] . ' ' . t('Also known as:') . ' ' . implode(', ', $also_known));
595 $data[$table_alias][$column_real_name]['argument'] = [
596 'field' => $column_real_name,
599 'additional fields' => $additional_fields,
600 'field_name' => $field_name,
601 'entity_type' => $entity_type_id,
602 'empty field name' => t('- No value -'),
604 $data[$table_alias][$column_real_name]['filter'] = [
605 'field' => $column_real_name,
608 'additional fields' => $additional_fields,
609 'field_name' => $field_name,
610 'entity_type' => $entity_type_id,
611 'allow empty' => TRUE,
613 if (!empty($allow_sort)) {
614 $data[$table_alias][$column_real_name]['sort'] = [
615 'field' => $column_real_name,
618 'additional fields' => $additional_fields,
619 'field_name' => $field_name,
620 'entity_type' => $entity_type_id,
624 // Set click sortable if there is a field definition.
625 if (isset($data[$table_alias][$field_name]['field'])) {
626 $data[$table_alias][$field_name]['field']['click sortable'] = $allow_sort;
629 // Expose additional delta column for multiple value fields.
630 if ($field_storage->isMultiple()) {
631 $title_delta = t('@label (@name:delta)', ['@label' => $label, '@name' => $field_name]);
632 $title_short_delta = t('@label:delta', ['@label' => $label]);
634 $data[$table_alias]['delta'] = [
636 'title' => $title_delta,
637 'title short' => $title_short_delta,
638 'help' => t('Delta - Appears in: @bundles.', ['@bundles' => implode(', ', $bundles_names)]),
640 $data[$table_alias]['delta']['field'] = [
643 $data[$table_alias]['delta']['argument'] = [
647 'additional fields' => $additional_fields,
648 'empty field name' => t('- No value -'),
649 'field_name' => $field_name,
650 'entity_type' => $entity_type_id,
652 $data[$table_alias]['delta']['filter'] = [
656 'additional fields' => $additional_fields,
657 'field_name' => $field_name,
658 'entity_type' => $entity_type_id,
659 'allow empty' => TRUE,
661 $data[$table_alias]['delta']['sort'] = [
665 'additional fields' => $additional_fields,
666 'field_name' => $field_name,
667 'entity_type' => $entity_type_id,
677 * Implements hook_field_views_data().
679 * The function implements the hook in behalf of 'core' because it adds a
680 * relationship and a reverse relationship to entity_reference field type, which
681 * is provided by core.
683 function core_field_views_data(FieldStorageConfigInterface $field_storage) {
684 $data = views_field_default_views_data($field_storage);
686 // The code below only deals with the Entity reference field type.
687 if ($field_storage->getType() != 'entity_reference') {
691 $entity_manager = \Drupal::entityManager();
692 $entity_type_id = $field_storage->getTargetEntityTypeId();
693 /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */
694 $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping();
696 foreach ($data as $table_name => $table_data) {
697 // Add a relationship to the target entity type.
698 $target_entity_type_id = $field_storage->getSetting('target_type');
699 $target_entity_type = $entity_manager->getDefinition($target_entity_type_id);
700 $entity_type_id = $field_storage->getTargetEntityTypeId();
701 $entity_type = $entity_manager->getDefinition($entity_type_id);
702 $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable();
703 $field_name = $field_storage->getName();
705 // Provide a relationship for the entity type with the entity reference
708 '@label' => $target_entity_type->getLabel(),
709 '@field_name' => $field_name,
711 $data[$table_name][$field_name]['relationship'] = [
712 'title' => t('@label referenced from @field_name', $args),
713 'label' => t('@field_name: @label', $args),
714 'group' => $entity_type->getLabel(),
715 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $field_storage->getBundles())]),
717 'base' => $target_base_table,
718 'entity type' => $target_entity_type_id,
719 'base field' => $target_entity_type->getKey('id'),
720 'relationship field' => $field_name . '_target_id',
723 // Provide a reverse relationship for the entity type that is referenced by
725 $args['@entity'] = $entity_type->getLabel();
726 $args['@label'] = $target_entity_type->getLowercaseLabel();
727 $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name;
728 $data[$target_base_table][$pseudo_field_name]['relationship'] = [
729 'title' => t('@entity using @field_name', $args),
730 'label' => t('@field_name', ['@field_name' => $field_name]),
731 'group' => $target_entity_type->getLabel(),
732 'help' => t('Relate each @entity with a @field_name set to the @label.', $args),
733 'id' => 'entity_reverse',
734 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(),
735 'entity_type' => $entity_type_id,
736 'base field' => $entity_type->getKey('id'),
737 'field_name' => $field_name,
738 'field table' => $table_mapping->getDedicatedDataTableName($field_storage),
739 'field field' => $field_name . '_target_id',
742 'field' => 'deleted',