entityFieldManager = $entity_field_manager; $this->fieldTypePluginManager = $field_type_plugin_manager; $this->configFactory = $config_factory; $this->routeMatch = $route_match; $route_options = $this->routeMatch->getRouteObject()->getOptions(); $array_keys = array_keys($route_options['parameters']); $this->entityType = array_shift($array_keys); $entity_type = $this->routeMatch->getParameter($this->entityType); $this->entityBundle = $entity_type->id(); $this->entityTypeBundleOf = $entity_type->getEntityType()->getBundleOf(); $this->fileSystem = $file_system; $this->fileDownloadController = $file_download_controller; } public static function create(ContainerInterface $container) { return new static( $container->get('entity_field.manager'), $container->get('plugin.manager.field.field_type'), $container->get('config.factory'), $container->get('current_route_match'), $container->get('file_system'), FileDownloadController::create($container) ); } /** * Returns a page (render array) displaying the metadata profile. * * @return array */ public function profile() { // Get core content type information. $bundle_config = $this->config(str_replace('_', '.', $this->entityType) . '.' . $this->entityBundle); if (!$bundle_config) { return ['#markup'=> 'Not a valid content type.']; } $build = $this->formatBundle($bundle_config); $metadata_profile = $this->getMetadataProfile(); $build['summary_table'] = [ '#type' => 'container', '#attributes'=> ['class' => ['scrolling-table-container']], 'data' => $this->buildSummaryTable($metadata_profile), ]; $build['fields'] = [ '#type' => 'container', '#weight' => '9', ]; foreach ($metadata_profile as $field_name => $field_profile) { $build['fields'][$field_name] = $this->buildField($field_profile); } return $build; } /** * Format basic information about a bundle. */ protected function formatBundle(Config $bundle) { if (str_starts_with($bundle->getName(), 'node.type')) { $label = $bundle->get('name'); $machine_name = $bundle->get('type'); $description = $bundle->get('description'); } elseif (str_starts_with($bundle->getName(), 'taxonomy.vocabulary')) { $label = $bundle->get('name'); $machine_name = $bundle->get('vid'); $description = $bundle->get('description'); } elseif (str_starts_with($bundle->getName(), 'media.type')) { $label = $bundle->get('label'); $machine_name = $bundle->get('id'); $description = $bundle->get('description'); } $build = [ '#type' => 'container', '#attached' => ['library' => ['metadata_profile/metadata_profile']], 'title' => [ '#plain_text' => $label, '#prefix' => '

', '#suffix' => '

', ], 'machine_name' => [ '#plain_text' => $machine_name, '#prefix' => ' (', '#suffix' => ')', ], 'description' => [ '#plain_text' => $description, '#prefix' => '

', '#suffix' => '

', ], ]; return $build; } public function getMetadataProfile() { // $metadata_profile is an array of arrays. Its keys are the "short" machine names of the fields // such as field_abstract. The values are arrays with the following keys: // * 'label' - text. the bundle's human-readable label. // * 'machine_name' - text. The short machine name such as 'field_abstract'. // * 'id' - text. The long machine name such as 'node.islandora_object.field_abstract'. (same as this array's key) // * 'edit_url' - Url object. The link to the edit page for that field (or to the base field override page). // * 'description' - the help text for the field. // * 'type' - text. The human readable type of the field. (e.g. "Entity Reference") // * 'type_machine_name' - text. The machine name of the type (e.g. 'entity_reference') // * 'required' - 'Required'|'Not required'. Whether the field is required. // * 'cardinality' - 'Not repeatable'|'Repeatable'|'Repeatable, limit N' // * 'auto_create' - 'Create new'|'Create new #bundle'|'Do not create new'|NULL Whether a relationship field allows new entity creation on the fly. // * 'target_bundles' - array. List of bundle names that are the target of this field. // * 'search_api' - associative array. Has the following keys: // * 'in_search_api' - True|False. True if some field matches. // * 'has_facet' - True|False. True if some field has a facet. // * 'fields' - array. The 'fields' array is keyed by the machine names of the search api fields, // which are arrays with the following fields: // * 'search_api_field_name' - the machine name of the search api field. // * 'search_api_field_label' - the human readable label of the search api field. // * 'property_path' - the method for getting the values. // * 'type' = the search api field type. // * 'index_name' - the name of the Search API index (in case there are more than one) // * 'fields_included' - for aggregate and EDTF processor fields, which fields are in that field. // * 'facets' - associative array. has the following keys // * 'has_facet' - True|False. True if there is a facet, even if not showing up (can use with url alias) // * 'facets' - array. the 'facets' array is keyed by the machine name of the facet. each facet is an // array with keys: // * 'facet_name' // * 'facet_machine_name' // * 'facet_source' // * 'url_alias' // * 'block_visible' -- not yet implemented. $metadata_profile = []; $field_definitions = $this->entityFieldManager->getFieldDefinitions($this->entityTypeBundleOf, $this->entityBundle); foreach ($field_definitions as $field_name => $field_definition) { $metadata_profile[$field_name] = [ 'label' => $field_definition->getLabel(), 'machine_name' => $field_name, 'id' => method_exists($field_definition, 'id') ? $field_definition->id() : $field_definition->getUniqueIdentifier(), 'edit_url' => $this->getFieldEditUrl($field_definition), 'details_link' => $this->getFieldDetailsFragmentLink($field_name, $field_definition), 'description' => $field_definition->getDescription(), 'type' => $this->formatType($field_definition), 'type_machine_name' => $field_definition->getType(), 'required' => $this->formatRequired($field_definition), 'repeatable' => $this->formatCardinality($field_definition), 'auto_create' => $this->formatCreateNew($field_definition), 'target_bundles' => $this->formatTargetBundles($field_definition), 'base_field' => $field_definition->getFieldStorageDefinition() instanceof BaseFieldDefinition, ]; $metadata_profile[$field_name]['search_api'] = $this->getSearchApi($field_definition); } $metadata_profile = $this->addWeights($metadata_profile); return $metadata_profile; } private function getSearchApi($field_definition) { if (!$this->moduleHandler()->moduleExists('search_api')) { return FALSE; } $search_api = [ 'in_search_api' => FALSE, 'fields' => [], ]; $storage = $field_definition->getFieldStorageDefinition(); if ($storage instanceof BaseFieldDefinition) { $field_id = $this->entityTypeBundleOf . '.' . $storage->getName(); } else { $field_id = 'field.storage.' . $storage->get('id'); } $search_api_indexes = $this->configFactory->listAll('search_api.index'); foreach ($search_api_indexes as $index) { $index_config = $this->config($index); // Loop over fields. foreach ($index_config->get('field_settings') as $search_api_field_name => $search_api_field_setting) { $indexed_field_name = $index_config->getName() . '.' .$search_api_field_name; // Get Aggregated Fields. if ($search_api_field_setting['property_path'] == 'aggregated_field') { if (in_array('entity:node/' . $field_definition->getName(), $search_api_field_setting['configuration']['fields'])) { $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); } } // Get EDTF fields. else if ($field_definition->getType() == 'edtf') { // Get EDTF year. if ($search_api_field_setting['property_path'] == 'edtf_year') { $edtf_year_fields = $index_config->get('processor_settings')['edtf_year_only']['fields'] ?: []; if (in_array(str_replace('.', '|', $field_definition->id()), $edtf_year_fields)) { $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); } } // Get EDTF Date else if ($search_api_field_setting['property_path'] == 'edtf_dates') { $edtf_date_fields = $index_config->get('processor_settings')['edtf_date_processor']['fields'] ?: []; if (in_array(str_replace('.', '|', $field_definition->id()), $edtf_date_fields)) { $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); } } } // Check dependencies for a dependency on this field. if (isset($search_api_field_setting['dependencies']) and isset($search_api_field_setting['dependencies']['config'])) { $field_dependencies = $search_api_field_setting['dependencies']['config']; if (in_array($field_id, $field_dependencies)) { $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); } } // Check if the property path equals this field. if (isset($search_api_field_setting['datasource_id']) and $search_api_field_setting['property_path'] == $field_definition->getName()) { $search_api['fields'][$indexed_field_name] = $this->getSearchApiField($search_api_field_name, $search_api_field_setting, $index_config); } } if (count($search_api['fields']) > 0) { $search_api['in_search_api'] = TRUE; } if (in_array(TRUE, array_column($search_api['fields'], 'has_facet'), true)) { $search_api['has_facet'] = TRUE; } else { $search_api['has_facet'] = FALSE; } } return $search_api; } protected function buildSummaryTable($metadata_profile) { $rows = $this->getRows($metadata_profile, TRUE); return [ '#type' => 'table', '#header' => $this->getHeaders(), '#rows' => $rows, ]; } protected function getRows($metadata_profile, $display=NULL) { $rows = []; foreach ($metadata_profile as $field_name => $field_profile) { if (str_starts_with( $field_name, 'field_') or in_array($field_name, ['title', 'name'])) { $rows[] = [ $display ? $field_profile['details_link'] : $field_profile['label'], $field_profile['machine_name'], $field_profile['description'], $field_profile['type'], $field_profile['required'], $field_profile['repeatable'], $field_profile['auto_create'], $display ? $this->formatListForTable($field_profile['target_bundles']) : $field_profile['target_bundles'], $field_profile['search_api']['in_search_api'] ? 'Indexed' : '', $field_profile['search_api']['has_facet'] ? 'Facet' : '', $field_profile['weight'], ]; } } return $rows; } protected function getHeaders() { return [ $this->t('Field'), $this->t('Machine name'), $this->t('Description'), $this->t('Type'), $this->t('Required'), $this->t('Repeatable'), $this->t('Create new'), $this->t('Target bundles'), $this->t('In Search API'), $this->t('Has Facet'), $this->t('Weight'), // TODO: add more columns ]; } protected function buildField(array $field_profile) { $render_array = []; $field_name = $field_profile['machine_name']; if (str_starts_with($field_name, 'field_') or in_array($field_name, ['title', 'name'])) { $render_array = [ '#type' => 'container', '#attributes' => ['class' => ['field-info']], '#weight' => $field_profile['weight'], // TODO: add weight here from form display defaults. 'title' => [ '#plain_text' => $field_profile['label'], '#prefix' => '

', '#suffix' => '

', ], 'machine_name' => [ '#plain_text' => $field_name, '#prefix' => '(', '#suffix' => ') ', ], 'edit_link' => $field_profile['edit_url'] ? $this->formatFieldEditLink($field_profile['edit_url']) : [], 'table' => [ '#type' => 'table', '#header' => ['Field Configuration',''], '#rows' => $this->getFieldTableRows($field_profile), '#attributes' => ['class' => ['field-info-table']], '#prefix' => '

' . $this->t('Field Information') . '

', ], 'description' => [ '#type' => 'markup', '#markup' => $field_profile['description'] ?: $this->t('[No description]'), '#prefix' => '

Description:

', ], 'search_api' => $this->formatSearchApi($field_profile), 'facets' => $this->formatFacets($field_profile), ]; } return $render_array; } protected function getFieldEditUrl(FieldDefinitionInterface $field_definition) { $redirect_url = Url::fromRoute('', ['fragment' => $field_definition->getName()]); if ($field_definition->getFieldStorageDefinition() instanceof BaseFieldDefinition) { if ($this->moduleHandler()->moduleExists('base_field_override_ui')) { if (method_exists($field_definition, 'id')) { $edit_url = Url::fromRoute('entity.base_field_override.' . $this->entityTypeBundleOf . '_base_field_override_edit_form', [ $this->entityType => $field_definition->getTargetBundle(), 'base_field_override' => $field_definition->id(), 'destination' => $redirect_url->toString() ]); } else { return NULL; } } else { return NULL; } } else { $edit_url = Url::fromRoute('entity.field_config.' . $this->entityTypeBundleOf . '_field_edit_form', [ $this->entityType => $field_definition->getTargetBundle(), 'field_config' => $field_definition->id(), 'destination' => $redirect_url->toString(), ]); } return $edit_url; } private function getFieldDetailsFragmentLink(string $field_name, FieldDefinitionInterface $field_definition) { $url = Url::fromRoute('', [], ['fragment' => $field_name]); return Link::fromTextAndUrl($field_definition->getLabel(), $url); } protected function formatFieldEditLink(Url $edit_url) { return [ '#type' => 'link', '#url' => $edit_url, '#title' => $this->t('Edit'), '#attributes' => ['class' => ['edit-link']], ]; } protected function getFieldTableRows(array $field_profile) { $rows = [ [$this->t('Type:'), $field_profile['type']], [$this->t('Required:'), $field_profile['required']], [$this->t('Repeatable:'), $field_profile['repeatable']], ]; if ($field_profile['auto_create']) { $rows[] = [$this->t('Create new:'), $field_profile['auto_create']]; } if ($field_profile['target_bundles']) { $rows[] = [$this->t('Target bundles:'), $this->formatListForTable($field_profile['target_bundles'])]; } return $rows; } protected function formatType(FieldDefinitionInterface $field_definition) { $type = $field_definition->getType(); $type_label = $this->fieldTypePluginManager->getDefinition($type)['label']; if (in_array($type, ['entity_reference', 'typed_relation'])) { $type_label = $type_label . ' > ' . $field_definition->getSetting('target_type'); } return $type_label; } protected function formatRequired(FieldDefinitionInterface $field_definition) { if ($field_definition->isRequired()) { return $this->t('Required'); } else { return $this->t('Not required'); } } /** * This does not work - most fields dont have a form weight specified in getDisplayOptions. */ protected function getWeight(string $field_name, array $form) { if (isset($form[$field_name])) { $local_weight = $form[$field_name]['#weight']; if ($parent = $form['#group_children'][$field_name]) { $parent_weight = $this->getWeight($parent, $form); return $this->combine_weights($parent_weight, $local_weight); } else { return $local_weight; } } else { return 9999; } } protected function formatCardinality(FieldDefinitionInterface $field_definition) { $cardinality = $field_definition->getFieldStorageDefinition()->getCardinality(); if ($cardinality == 1) { return $this->t('Not repeatable'); } elseif ($cardinality < 0) { return $this->t('Repeatable'); } elseif ($cardinality > 1) { return $this->t('Repeatable, limit :limit', [':limit' => $cardinality]); } return NULL; } protected function formatCreateNew(FieldDefinitionInterface $field_definition) { if (in_array($field_definition->getType(), ['entity_reference', 'typed_relation'])) { $create_new_bundle = $field_definition->getSetting('handler_settings')['auto_create_bundle'] ? $create_new_bundle = $field_definition->getSetting('handler_settings')['auto_create_bundle'] : ''; $create_new = $field_definition->getSetting('handler_settings')['auto_create'] || ''; if ($create_new) { return $create_new_bundle ? 'Create new (' . $create_new_bundle . ')' : 'Create new'; } else { return $this->t('Do not create new'); } } else { return NULL; } } protected function formatTargetBundles(FieldDefinitionInterface $field_definition) { $handler = $field_definition->getSetting('handler'); $setting = $field_definition->getSetting('handler_settings'); if (!$setting) { return []; } $target_bundles = $setting['target_bundles']; foreach ($target_bundles as $index => $bundle) { if ($handler == 'default:taxonomy_term') { $bundle_config = $this->entityTypeManager()->getStorage('taxonomy_vocabulary')->load($bundle); } if ($handler == 'default:node') { $bundle_config = $this->entityTypeManager()->getStorage('node_type')->load($bundle); } // TODO: ADD MEDIA, PARAGRAPHS ETC if ($bundle_config) { $target_bundles[$index] = $bundle_config->get('name') . ' (' . $bundle . ')'; } } return $target_bundles; return [ '#theme' => 'item_list', '#title' => $this->t('Target bundles:'), '#items' => $target_bundles, '#type' => 'ul' ]; } protected function formatListForTable(array $list) { return ['data' => [ '#theme' => 'item_list', '#items' => array_values($list), '#type' => 'ul', ]]; } protected function formatSearchApi($field_profile) { if (!$field_profile['search_api']['in_search_api']) { return []; } $build = [ '#type' => 'table', '#header' => [ 'col1' => $this->t('Search API field name'), 'col2' => $this->t('Search API machine name'), 'col3' => $this->t('Search API property path'), 'col4' => $this->t('Search API field type'), 'col5' => $this->t('Aggregate field includes'), 'col0' => $this->t('Search API index'), 'col6' => $this->t('Has facet'), ], '#rows' => [], '#prefix' => '

' . $this->t('Search API fields') . '

', ]; foreach ($field_profile['search_api']['fields'] as $search_api_field) { $build['#rows'][] = $this->formatSearchApiField($search_api_field); } if (count($build['#rows']) == 0) { return []; } else { return $build; } } protected function getSearchApiField($field_setting_name, $field_setting, $index_config) { $search_api_field_array = [ 'search_api_field_name' => $field_setting_name, 'search_api_field_label' => $field_setting['label'], 'property_path' => $field_setting['property_path'], 'type' => $field_setting['type'], 'index_name' => $index_config->get('name'), ]; // Get fields included for aggregated fields. if ($field_setting['property_path'] == 'aggregated_field') { $search_api_field_array['fields_included'] = $field_setting['configuration']['fields']; } else if ($field_setting['property_path'] == 'edtf_year') { $search_api_field_array['fields_included'] = $index_config->get('processor_settings')['edtf_year_only']['fields']; } else if ($field_setting['property_path'] == 'edtf') { $search_api_field_array['fields_included'] = $index_config->get('processor_settings')['edtf_date_processor']['fields']; } else { $search_api_field_array['fields_included'] = []; } // Get facet info. $search_api_field_array['has_facet'] = FALSE; if ($this->moduleHandler()->moduleExists('facets')) { $all_facets = \Drupal::entityTypeManager()->getStorage('facets_facet')->loadMultiple(); foreach ($all_facets as $facet_id => $facet) { // If facet uses this solr field... then add it to the array. if ($facet->get('field_identifier') == $field_setting_name) { $search_api_field_array['has_facet'] = True; $search_api_field_array['facets'][] = [ 'field_identifier' => $facet->get('field_identifier'), 'facet_name' => $facet->getName(), 'facet_machine_name' => $facet_id, 'facet_source' => $facet->getFacetSourceId(), 'url_alias' => $facet->getUrlAlias(), 'block_visible' => $this->getBlockVisible($facet), ]; } } } return $search_api_field_array; } protected function getBlockVisible($facet) { return ''; } protected function formatSearchApiField($search_api_field_profile) { return [ 'data' => [ $search_api_field_profile['search_api_field_label'], $search_api_field_profile['search_api_field_name'], $search_api_field_profile['property_path'], $search_api_field_profile['type'], $this->formatListForTable($search_api_field_profile['fields_included']), $search_api_field_profile['index_name'], $search_api_field_profile['has_facet'] ? 'True' : '', ] ]; } public function sanitizeRowsForCSV($rows) { foreach ($rows as $row_key => $row) { foreach ($row as $element_key => $element) { if ($element instanceof Link) { $rows[$row_key][$element_key] = $element->getText(); } else if (is_array($element)) { $rows[$row_key][$element_key] = implode('|', $element); } } } return $rows; } public function download() { $contentType = $this->entityBundle; $entityKey = $this->entityTypeBundleOf; $headers = $this->getHeaders(); $metadata_profile = $this->getMetadataProfile(); $rows = $this->getRows($metadata_profile); $rows = $this->sanitizeRowsForCSV($rows); $filename_slug = "{$entityKey}__{$contentType}.csv"; $filename = $this->fileSystem->getTempDirectory() . '/' . $filename_slug; // Write file to temporary filesystem. if (file_exists($filename)) { $this->fileSystem->delete($filename); } $fh = fopen($filename, 'w'); fputcsv($fh, $headers); foreach ($rows as $row) { fputcsv($fh, $row); } fclose($fh); $request = new Request(['file' => $filename_slug]); return $this->fileDownloadController->download($request, 'temporary'); } private function formatFacets(array $field_profile) { if (!$field_profile['search_api']['has_facet']) { return []; } $build = [ '#type' => 'table', '#header' => [ $this->t('Search API field'), $this->t('Facet name'), $this->t('Facet machine name'), $this->t('Facet source'), $this->t('URL fragment alias'), $this->t('Block is placed'), ], '#rows' => [], '#prefix' => '

' . $this->t('Search Facets') . '

', ]; foreach ($field_profile['search_api']['fields'] as $field) { if ($field['has_facet']) { foreach ($field['facets'] as $facet) { $build['#rows'][] = $this->formatFacet($facet); } } } if (count($build['#rows']) == 0) { return []; } else { return $build; } } private function formatFacet($facet) { return [ 'data' => [ $facet['field_identifier'], $facet['facet_name'], $facet['facet_machine_name'], $facet['facet_source'], $facet['url_alias'], $facet['block_visible'], ] ]; } private function addWeights(array $metadata_profile) { if ($this->entityTypeBundleOf == 'node') { $node = \Drupal\node\Entity\Node::create(['type' => $this->entityBundle]); $form = \Drupal::service('entity.form_builder')->getForm($node); foreach($metadata_profile as $field_name => $field_profile) { $metadata_profile[$field_name]['weight'] = $this->getWeight($field_name, $form); } $weights = array_column($metadata_profile, 'weight'); array_multisort($weights, SORT_ASC, $metadata_profile); } return $metadata_profile; } private function combine_weights($parent_weight, $local_weight) { $parent_int = (string)floor($parent_weight); $parent_fraction = (string)($parent_weight - $parent_int); $parent_fraction = str_replace('0.', '', $parent_fraction); $local_int = (string)floor($local_weight); $local_fraction = (string)($local_weight - $local_int); $combined_weight = $parent_int; $combined_weight .= '.'; if ($parent_fraction) { # pad so its length is a multiple of 2 $length = mb_strlen($parent_fraction); if (fmod($length, 2) != 0){ $parent_fraction .= '0'; } $combined_weight .= $parent_fraction; } $combined_weight .= str_pad($local_int, 2, '0', STR_PAD_LEFT); if ($local_fraction) { $a=2; } return (float)$combined_weight; } }