external_entities-8.x-2.x-dev/modules/xntt_views/src/Plugin/views/field/ExternalEntityField.php

modules/xntt_views/src/Plugin/views/field/ExternalEntityField.php
<?php

namespace Drupal\xntt_views\Plugin\views\field;

use Drupal\Component\Utility\Xss;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\views\Plugin\views\field\EntityField;
use Drupal\views\ResultRow;
use Drupal\xntt_views\Entity\Render\ExternalEntityFieldRenderer;

/**
 * A field that displays external entity field data.
 *
 * @ingroup views_field_handlers
 *
 * @ViewsField("xnttfield")
 */
class ExternalEntityField extends EntityField {

  /**
   * {@inheritdoc}
   */
  public function access(AccountInterface $account) {
    $access_control_handler = $this->entityTypeManager->getAccessControlHandler($this->getEntityType());
    return $access_control_handler->fieldAccess('view', $this->getFieldDefinition(), $account);
  }

  /**
   * Called to add the field to a query.
   *
   * By default, all needed data is taken from entities loaded by the query
   * plugin. Columns are added only if they are used in groupings.
   */
  public function query($use_groupby = FALSE) {
    $fields = $this->additional_fields;

    // No need to add the entity type.
    $entity_type_key = array_search('entity_type', $fields);
    if ($entity_type_key !== FALSE) {
      unset($fields[$entity_type_key]);
    }

    if ($use_groupby) {
      // Add the fields that we're actually grouping on.
      $options = [];
      if ($this->options['group_column'] != 'entity_id') {
        $options = [$this->options['group_column'] => $this->options['group_column']];
      }
      $options += is_array($this->options['group_columns']) ? $this->options['group_columns'] : [];

      // Go through the list and determine the actual column name from field
      // api.
      $fields = [];
      $table_mapping = $this->getTableMapping();
      $field_definition = $this->getFieldStorageDefinition();

      foreach ($options as $column) {
        $fields[$column] = $table_mapping->getFieldColumnName($field_definition, $column);
      }

      $this->group_fields = $fields;
    }

    // Add additional fields (and the table join itself) if needed.
    if ($this->add_field_table($use_groupby)) {
      $this->ensureMyTable();
      $this->addAdditionalFields($fields);
    }

    // Let the entity field renderer alter the query if needed.
    $this->getEntityFieldRenderer()->query($this->query, $this->relationship);
  }

  /**
   * {@inheritdoc}
   */
  // phpcs:disable
  public function add_field_table($use_groupby) {
  // phpcs:enable
    // Grouping is enabled.
    if ($use_groupby) {
      return TRUE;
    }
    // This a multiple value field, but "group multiple values" is not checked.
    if ($this->multiple && !$this->options['group_rows']) {
      return TRUE;
    }
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function clickSortable() {
    // A field is not click sortable if it's a multiple field with
    // "group multiple values" checked, since a click sort in that case would
    // add a join to the field table, which would produce unwanted duplicates.
    if ($this->multiple && $this->options['group_rows']) {
      return FALSE;
    }

    // If field definition is set, use that.
    if (isset($this->definition['click sortable'])) {
      return (bool) $this->definition['click sortable'];
    }

    // Default to true.
    return TRUE;
  }

  /**
   * Called to determine what to tell the clicksorter.
   */
  public function clickSort($order) {
    // No column selected, can't continue.
    if (empty($this->options['click_sort_column'])) {
      return;
    }

    $this->ensureMyTable();
    $field_storage_definition = $this->getFieldStorageDefinition();
    $column = $this->getTableMapping()->getFieldColumnName($field_storage_definition, $this->options['click_sort_column']);
    if (!isset($this->aliases[$column])) {
      // Column is not in query; add a sort on it.
      $this->aliases[$column] = $this->tableAlias . '.' . $column;
      // If the query uses DISTINCT we need to add the column too.
      if (!empty($this->view->getQuery()->options['distinct'])) {
        $this->query->addField($this->tableAlias, $column);
      }
    }
    $this->query->addOrderBy(NULL, NULL, $order, $this->aliases[$column]);
  }

  /**
   * Gets the field storage definition.
   *
   * @return \Drupal\Core\Field\FieldStorageDefinitionInterface
   *   The field storage definition used by this handler.
   */
  protected function getFieldStorageDefinition() {
    $entity_type_id = $this->definition['entity_type'];
    $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type_id);

    // @todo Unify 'entity field'/'field_name' instead of converting back and
    //   forth. https://www.drupal.org/node/2410779
    if (isset($this->definition['field_name']) && isset($field_storage_definitions[$this->definition['field_name']])) {
      return $field_storage_definitions[$this->definition['field_name']];
    }

    if (isset($this->definition['entity field']) && isset($field_storage_definitions[$this->definition['entity field']])) {
      return $field_storage_definitions[$this->definition['entity field']];
    }

    // The list of field storage definitions above does not include computed
    // base fields, so we need to explicitly fetch a list of all base fields in
    // order to support them.
    // @see \Drupal\Core\Entity\EntityFieldManager::getFieldStorageDefinitions()
    $base_fields = $this->entityFieldManager->getBaseFieldDefinitions($entity_type_id);
    if (isset($this->definition['field_name']) && isset($base_fields[$this->definition['field_name']])) {
      return $base_fields[$this->definition['field_name']]->getFieldStorageDefinition();
    }
    throw new \Exception("No field storage definition found for entity type {$this->definition['entity_type']} and field {$this->definition['field_name']} on view {$this->view->storage->id()}");
  }

  /**
   * {@inheritdoc}
   */
  public function submitFormCalculateOptions(array $options, array $form_state_options) {
    // When we change the formatter type we don't want to keep any of the
    // previous configured formatter settings, as there might be schema
    // conflict.
    unset($options['settings']);
    $options = $form_state_options + $options;
    if (!isset($options['settings'])) {
      $options['settings'] = [];
    }
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  // phpcs:disable
  public function multiple_options_form(&$form, FormStateInterface $form_state) {
  // phpcs:enable
    $field = $this->getFieldDefinition();

    $form['multiple_field_settings'] = [
      '#type' => 'details',
      '#title' => $this->t('Multiple field settings'),
      '#weight' => 5,
    ];

    $form['group_rows'] = [
      '#title' => $this->t('Display all values in the same row'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['group_rows'],
      '#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.'),
      '#fieldset' => 'multiple_field_settings',
    ];

    // Make the string translatable by keeping it as a whole rather than
    // translating prefix and suffix separately.
    [$prefix, $suffix] = explode('@count', $this->t('Display @count value(s)'));

    if ($field->getCardinality() == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) {
      $type = 'textfield';
      $options = NULL;
      $size = 5;
    }
    else {
      $type = 'select';
      $range = range(1, $field->getCardinality());
      $options = array_combine($range, $range);
      $size = 1;
    }
    $form['multi_type'] = [
      '#type' => 'radios',
      '#title' => $this->t('Display type'),
      '#options' => [
        'ul' => $this->t('Unordered list'),
        'ol' => $this->t('Ordered list'),
        'separator' => $this->t('Simple separator'),
      ],
      '#states' => [
        'visible' => [
          ':input[name="options[group_rows]"]' => ['checked' => TRUE],
        ],
      ],
      '#default_value' => $this->options['multi_type'],
      '#fieldset' => 'multiple_field_settings',
    ];

    $form['separator'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Separator'),
      '#default_value' => $this->options['separator'],
      '#states' => [
        'visible' => [
          ':input[name="options[group_rows]"]' => ['checked' => TRUE],
          ':input[name="options[multi_type]"]' => ['value' => 'separator'],
        ],
      ],
      '#fieldset' => 'multiple_field_settings',
    ];

    $form['delta_limit'] = [
      '#type' => $type,
      '#size' => $size,
      '#field_prefix' => $prefix,
      '#field_suffix' => $suffix,
      '#options' => $options,
      '#default_value' => $this->options['delta_limit'],
      '#prefix' => '<div class="container-inline">',
      '#states' => [
        'visible' => [
          ':input[name="options[group_rows]"]' => ['checked' => TRUE],
        ],
      ],
      '#fieldset' => 'multiple_field_settings',
    ];

    [$prefix, $suffix] = explode('@count', $this->t('starting from @count'));
    $form['delta_offset'] = [
      '#type' => 'textfield',
      '#size' => 5,
      '#field_prefix' => $prefix,
      '#field_suffix' => $suffix,
      '#default_value' => $this->options['delta_offset'],
      '#states' => [
        'visible' => [
          ':input[name="options[group_rows]"]' => ['checked' => TRUE],
        ],
      ],
      '#description' => $this->t('(first item is 0)'),
      '#fieldset' => 'multiple_field_settings',
    ];
    $form['delta_reversed'] = [
      '#title' => $this->t('Reversed'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['delta_reversed'],
      '#suffix' => $suffix,
      '#states' => [
        'visible' => [
          ':input[name="options[group_rows]"]' => ['checked' => TRUE],
        ],
      ],
      '#description' => $this->t('(start from last values)'),
      '#fieldset' => 'multiple_field_settings',
    ];
    $form['delta_first_last'] = [
      '#title' => $this->t('First and last only'),
      '#type' => 'checkbox',
      '#default_value' => $this->options['delta_first_last'],
      '#suffix' => '</div>',
      '#states' => [
        'visible' => [
          ':input[name="options[group_rows]"]' => ['checked' => TRUE],
        ],
      ],
      '#fieldset' => 'multiple_field_settings',
    ];
  }

  /**
   * Render all items in this field together.
   *
   * When using advanced render, each possible item in the list is rendered
   * individually. Then the items are all pasted together.
   */
  public function renderItems($items) {
    if (!empty($items)) {
      if ($this->options['multi_type'] == 'separator' || !$this->options['group_rows']) {
        $separator = $this->options['multi_type'] == 'separator' ? Xss::filterAdmin($this->options['separator']) : '';
        $build = [
          '#type' => 'inline_template',
          '#template' => '{{ items | safe_join(separator) }}',
          '#context' => ['separator' => $separator, 'items' => $items],
        ];
      }
      else {
        $build = [
          '#theme' => 'item_list',
          '#items' => $items,
          '#title' => NULL,
          '#list_type' => $this->options['multi_type'],
        ];
      }
      return $this->renderer->render($build);
    }
    // No items.
    return '';
  }

  /**
   * Adapts the $items according to the delta configuration.
   *
   * This selects displayed deltas, reorders items, and takes offsets into
   * account.
   *
   * @param array $all_values
   *   The items for individual rendering.
   *
   * @return array
   *   The manipulated items.
   */
  protected function prepareItemsByDelta(array $all_values) {
    if ($this->options['delta_reversed']) {
      $all_values = array_reverse($all_values);
    }

    // We are supposed to show only certain deltas.
    if ($this->limit_values) {
      $row = $this->view->result[$this->view->row_index];

      // Offset is calculated differently when row grouping for a field is not
      // enabled. Since there are multiple rows, delta needs to be taken into
      // account, so that different values are shown per row.
      if (!$this->options['group_rows'] && isset($this->aliases['delta']) && isset($row->{$this->aliases['delta']})) {
        $delta_limit = 1;
        $offset = $row->{$this->aliases['delta']};
      }
      // Single fields don't have a delta available so choose 0.
      elseif (!$this->options['group_rows'] && !$this->multiple) {
        $delta_limit = 1;
        $offset = 0;
      }
      else {
        $delta_limit = (int) $this->options['delta_limit'];
        $offset = intval($this->options['delta_offset']);

        // We should only get here in this case if there is an offset, and in
        // that case we are limiting to all values after the offset.
        if ($delta_limit === 0) {
          $delta_limit = count($all_values) - $offset;
        }
      }

      // Determine if only the first and last values should be shown.
      $delta_first_last = $this->options['delta_first_last'];

      $new_values = [];
      for ($i = 0; $i < $delta_limit; $i++) {
        $new_delta = $offset + $i;

        if (isset($all_values[$new_delta])) {
          // If first-last option was selected, only use the first and last
          // values.
          if (!$delta_first_last
            // Use the first value.
            || $new_delta == $offset
            // Use the last value.
            || $new_delta == ($delta_limit + $offset - 1)) {
            $new_values[] = $all_values[$new_delta];
          }
        }
      }
      $all_values = $new_values;
    }
    return $all_values;
  }

  /**
   * Returns the entity field renderer.
   *
   * @return \Drupal\views\Entity\Render\EntityFieldRenderer
   *   The entity field renderer.
   */
  protected function getEntityFieldRenderer() {
    if (!isset($this->entityFieldRenderer)) {
      // This can be invoked during field handler initialization in which case
      // view fields are not set yet.
      if (!empty($this->view->field)) {
        foreach ($this->view->field as $field) {
          // An entity field renderer can handle only a single relationship.
          if ($field->relationship == $this->relationship && isset($field->entityFieldRenderer)) {
            $this->entityFieldRenderer = $field->entityFieldRenderer;
            break;
          }
        }
      }
      if (!isset($this->entityFieldRenderer)) {
        $entity_type = $this->entityTypeManager->getDefinition($this->getEntityType());
        $this->entityFieldRenderer = new ExternalEntityFieldRenderer(
          $this->view,
          $this->relationship,
          $this->languageManager,
          $entity_type,
          $this->entityTypeManager,
          $this->entityRepository
        );
      }
    }
    return $this->entityFieldRenderer;
  }

  /**
   * Gets an array of items for the field.
   *
   * @param \Drupal\views\ResultRow $values
   *   The result row object containing the values.
   *
   * @return array
   *   An array of items for the field.
   */
  public function getItems(ResultRow $values) {
    if (!$this->displayHandler->useGroupBy()) {
      $build_list = $this->getEntityFieldRenderer()->render($values, $this);
    }
    else {
      // For grouped results we need to retrieve a massaged entity having
      // grouped field values to ensure that "grouped by" values, especially
      // those with multiple cardinality work properly. See
      // \Drupal\Tests\views\Kernel\QueryGroupByTest::testGroupByFieldWithCardinality.
      $display = [
        'type' => $this->options['type'],
        'settings' => $this->options['settings'],
        'label' => 'hidden',
      ];
      // Optional relationships may not provide an entity at all. So we can't
      // use createEntityForGroupBy() for those rows.
      if ($entity = $this->getEntity($values)) {
        $entity = $this->createEntityForGroupBy($entity, $values);
        // Some bundles might not have a specific field, in which case the faked
        // entity doesn't have it either.
        $build_list = isset($entity->{$this->definition['field_name']}) ? $entity->{$this->definition['field_name']}->view($display) : NULL;
      }
      else {
        $build_list = NULL;
      }
    }

    if (!$build_list) {
      return [];
    }

    if ($this->options['field_api_classes']) {
      return [['rendered' => $build_list]];
    }

    // Render using the formatted data itself.
    $items = [];
    // Each item is extracted and rendered separately, the top-level formatter
    // render array itself is never rendered, so we extract its bubbleable
    // metadata and add it to each child individually.
    $bubbleable = BubbleableMetadata::createFromRenderArray($build_list);
    foreach (Element::children($build_list) as $delta) {
      BubbleableMetadata::createFromRenderArray($build_list[$delta])
        ->merge($bubbleable)
        ->applyTo($build_list[$delta]);
      $items[$delta] = [
        'rendered' => $build_list[$delta],
        // Add the raw field items (for use in tokens).
        'raw' => $build_list['#items'][$delta],
      ];
    }
    return $this->prepareItemsByDelta($items);
  }

  /**
   * Creates a fake entity with grouped field values.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to be processed.
   * @param \Drupal\views\ResultRow $row
   *   The result row object containing the values.
   *
   * @return bool|\Drupal\Core\Entity\FieldableEntityInterface
   *   Returns a new entity object containing the grouped field values.
   */
  protected function createEntityForGroupBy(EntityInterface $entity, ResultRow $row) {
    // Retrieve the correct translation object.
    $processed_entity = clone $this->getEntityFieldRenderer()->getEntityTranslationByRelationship($entity, $row);

    // Copy our group fields into the cloned entity. It is possible this will
    // cause some weirdness, but there is only so much we can hope to do.
    if (!empty($this->group_fields) && isset($entity->{$this->definition['field_name']})) {
      // first, test to see if we have a base value.
      $base_value = [];
      // Note: We would copy original values here, but it can cause problems.
      // For example, text fields store cached filtered values as 'safe_value'
      // which does not appear anywhere in the field definition so we cannot
      // affect it. Other side effects could happen similarly.
      $data = FALSE;
      foreach ($this->group_fields as $field_name => $column) {
        if (property_exists($row, $this->aliases[$column])) {
          $base_value[$field_name] = $row->{$this->aliases[$column]};
          if (isset($base_value[$field_name])) {
            $data = TRUE;
          }
        }
      }

      // If any of our aggregated fields have data, fake it:
      if ($data) {
        // Now, overwrite the original value with our aggregated value.
        // This overwrites it so there is always just one entry.
        $processed_entity->{$this->definition['field_name']} = [$base_value];
      }
      else {
        $processed_entity->{$this->definition['field_name']} = [];
      }
    }

    return $processed_entity;
  }

  /**
   * {@inheritdoc}
   */
  // phpcs:disable
  public function render_item($count, $item) {
  // phpcs:enable
    return $this->renderer->render($item['rendered']);
  }

  /**
   * {@inheritdoc}
   */
  protected function documentSelfTokens(&$tokens) {
    $field = $this->getFieldDefinition();
    foreach ($field->getColumns() as $id => $column) {
      $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = $this->t('Raw @column', ['@column' => $id]);
    }
  }

  /**
   * {@inheritdoc}
   */
  protected function addSelfTokens(&$tokens, $item) {
    $field = $this->getFieldDefinition();
    foreach ($field->getColumns() as $id => $column) {
      // Use \Drupal\Component\Utility\Xss::filterAdmin() because it's user data
      // and we can't be sure it is safe. We know nothing about the data,
      // though, so we can't really do much else.
      if (isset($item['raw'])) {
        $raw = $item['raw'];

        if (is_array($raw)) {
          if (isset($raw[$id]) && is_scalar($raw[$id])) {
            $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($raw[$id]);
          }
          else {
            // Make sure that empty values are replaced as well.
            $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = '';
          }
        }

        if (is_object($raw)) {
          $property = $raw->get($id);
          // Check if TypedDataInterface is implemented so we know how to render
          // the item as a string.
          if (!empty($property) && $property instanceof TypedDataInterface) {
            $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = Xss::filterAdmin($property->getString());
          }
          else {
            // Make sure that empty values are replaced as well.
            $tokens['{{ ' . $this->options['id'] . '__' . $id . ' }}'] = '';
          }
        }
      }
    }
  }

  /**
   * Returns the field formatter instance.
   *
   * @return \Drupal\Core\Field\FormatterInterface|null
   *   The field formatter instance.
   */
  protected function getFormatterInstance($format = NULL) {
    if (!isset($format)) {
      $format = $this->options['type'];
    }
    $settings = $this->options['settings'] + $this->formatterPluginManager->getDefaultSettings($format);

    $options = [
      'field_definition' => $this->getFieldDefinition(),
      'configuration' => [
        'type' => $format,
        'settings' => $settings,
        'label' => '',
        'weight' => 0,
      ],
      'view_mode' => '_custom',
    ];

    return $this->formatterPluginManager->getInstance($options);
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    return Cache::PERMANENT;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return $this->getEntityFieldRenderer()->getCacheContexts();
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    $field_definition = $this->getFieldDefinition();
    $field_storage_definition = $this->getFieldStorageDefinition();
    return Cache::mergeTags(
      $field_definition instanceof CacheableDependencyInterface ? $field_definition->getCacheTags() : [],
      $field_storage_definition instanceof CacheableDependencyInterface ? $field_storage_definition->getCacheTags() : []
    );
  }

  /**
   * Gets the table mapping for the entity type of the field.
   *
   * @return \Drupal\Core\Entity\Sql\DefaultTableMapping
   *   The table mapping.
   */
  protected function getTableMapping() {
    return $this->entityTypeManager->getStorage($this->definition['entity_type'])->getTableMapping();
  }

  /**
   * {@inheritdoc}
   */
  public function getValue(ResultRow $values, $field = NULL) {
    $entity = $this->getEntity($values);
    // Ensure the object is not NULL before attempting to translate it.
    if ($entity === NULL) {
      return NULL;
    }

    // Retrieve the translated object.
    $translated_entity = $this->getEntityFieldRenderer()->getEntityTranslationByRelationship($entity, $values);

    // Some bundles might not have a specific field, in which case the entity
    // (potentially a fake one) doesn't have it either.
    /** @var \Drupal\Core\Field\FieldItemListInterface $field_item_list */
    $field_item_list = $translated_entity->{$this->definition['field_name']} ?? NULL;

    if (!isset($field_item_list)) {
      // There isn't anything we can do without a valid field.
      return NULL;
    }

    $field_item_definition = $field_item_list->getFieldDefinition();

    $values = [];
    foreach ($field_item_list as $field_item) {
      /** @var \Drupal\Core\Field\FieldItemInterface $field_item */
      if ($field) {
        $values[] = $field_item->$field;
      }
      // Find the value using the main property of the field. If no main
      // property is provided fall back to 'value'.
      elseif ($main_property_name = $field_item->mainPropertyName()) {
        $values[] = $field_item->{$main_property_name};
      }
      else {
        $values[] = $field_item->value;
      }
    }

    if ($field_item_definition->getFieldStorageDefinition()->getCardinality() == 1) {
      return reset($values);
    }
    else {
      return $values;
    }
  }

  /**
   * {@inheritdoc}
   */
  public function onDependencyRemoval(array $dependencies) {
    // See if this handler is responsible for any of the dependencies being
    // removed. If this is the case, indicate that this handler needs to be
    // removed from the View.
    $remove = FALSE;
    // Get all the current dependencies for this handler.
    $current_dependencies = $this->calculateDependencies();
    foreach ($current_dependencies as $group => $dependency_list) {
      // Check if any of the handler dependencies match the dependencies being
      // removed.
      foreach ($dependency_list as $config_key) {
        if (isset($dependencies[$group]) && array_key_exists($config_key, $dependencies[$group])) {
          // This handlers dependency matches a dependency being removed,
          // indicate that this handler needs to be removed.
          $remove = TRUE;
          break 2;
        }
      }
    }
    return $remove;
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc