You are here

public function GeneralEntityReferenceFormatter::viewElements in Formatter Suite 8

Builds a renderable array for a field value.

Parameters

\Drupal\Core\Field\FieldItemListInterface $items: The field values to be rendered.

string $langcode: The language that should be used to render the field.

Return value

array A renderable array for $items, as an array of child elements keyed by consecutive numeric indexes starting from 0.

Overrides FormatterInterface::viewElements

File

src/Plugin/Field/FieldFormatter/GeneralEntityReferenceFormatter.php, line 948

Class

GeneralEntityReferenceFormatter
Formats an entity reference as one or more links.

Namespace

Drupal\formatter_suite\Plugin\Field\FieldFormatter

Code

public function viewElements(FieldItemListInterface $items, $langcode) {
  if ($items
    ->isEmpty() === TRUE) {
    return [];
  }
  $this
    ->sanitizeSettings();

  //
  // Get entities.
  // -------------
  // From the item list, get the entities to process. If there aren't
  // any, return nothing.
  $entities = $this
    ->getEntitiesToView($items, $langcode);
  if (empty($entities) === TRUE) {
    return [];
  }

  // Prepare custom classes.
  // -----------------------
  // If the admin entered custom classes, add them.
  $classes = $this
    ->getSetting('classes');
  if (empty($classes) === TRUE) {
    $classes = [];
  }
  else {

    // Security: Class names are entered by an administrator.
    // They may not include anything but CSS-compatible words, and
    // certainly no HTML.
    //
    // Here, the class text is stripped of HTML tags as a start.
    // A regex tosses unacceptable characters in a CSS class name.
    $classes = strip_tags($classes);
    $classes = mb_ereg_replace('[^_a-zA-Z0-9- ]', '', $classes);
    if ($classes === FALSE) {
      $classes = [];
    }
    $classes = explode(' ', $classes);
  }

  //
  // Prepare token prefix.
  // ---------------------
  // Tokens have a prefix (e.g. "node") that distinguishes the tokens of
  // one entity type or generic purpose from another. Normally, the prefix
  // matches the entity type, but this is not enforced. Since the token
  // API has no way to map entity types to token prefixes, assume the
  // prefix is the entity type except for known Drupal core special cases.
  $entityType = reset($entities)
    ->getEntityType()
    ->id();
  $fixPrefixes = self::getWellKnownTokenPrefixExceptions();
  if (isset($fixPrefixes[$entityType]) === TRUE) {
    $prefix = $fixPrefixes[$entityType];
  }
  else {
    $prefix = $entityType;
  }

  // Verify that the prefix exists. If it doesn't, then token replacement
  // cannot work for the standard reference styles.
  if ($this->tokenService === NULL) {
    $prefix = '';
  }
  else {
    $tokenInfo = $this->tokenService
      ->getInfo();
    if (isset($tokenInfo['tokens'][$prefix]) === FALSE) {

      // Prefix is unknown. Don't use a prefix.
      $prefix = '';
    }
  }

  //
  // Prepare token pattern.
  // ----------------------
  // Get the pattern and pre-process it. If the admin entered a custom
  // pattern, clean it of dangerous HTML. Otherwise get a well-known
  // pattern.
  $entityReferenceStyle = $this
    ->getSetting('entityReferenceStyle');
  if ($entityReferenceStyle === 'custom') {

    // Custom token pattern.
    $pattern = $this
      ->getSetting('titleCustomText');
    if (empty($pattern) === TRUE) {

      // If the custom pattern is empty, revert to the title.
      $entityReferenceStyle = 'title';
    }
    else {

      // Security: A custom pattern is entered by an administrator.
      // It may legitimately include HTML entities and minor HTML, but
      // it should not include dangerous HTML.
      $pattern = Xss::filterAdmin($this
        ->getSetting('titleCustomText'));
    }
  }
  elseif ($prefix === '') {

    // A standard style is requested, but a token prefix for the entity
    // type could not be found. This prevents us from using token replacement
    // for the standard styles. Below, several of these are implemented
    // with explicit code. But anything else needs to be reduced.
    switch ($entityReferenceStyle) {
      case 'id':
      case 'title':
      case 'title-id':

        // These have custom code below.
        break;
      default:

        // All other style choices do not. Reduce them to 'title'.
        $entityReferenceStyle = 'title';
        break;
    }

    // Because these styles are hard-coded below, we don't need to
    // get a token pattern.
    $pattern = '';
  }
  else {

    // Get the token pattern for a standard style.
    $entityReferencePatterns = self::getEntityReferenceTokenPatterns();
    $pattern = $entityReferencePatterns[$entityReferenceStyle];

    // Replace the placeholder "__PREFIX" with the entity type's prefix
    // determined above.
    $pattern = str_replace('__PREFIX', $prefix, $pattern);

    // Also map from generic tokens to the specific tokens for
    // well-known entity types.
    $fixPatterns = self::getWellKnownTokenReplacements();
    $pattern = str_replace(array_keys($fixPatterns), array_values($fixPatterns), $pattern);
  }

  //
  // Replace tokens.
  // ---------------
  // Loop through the entities and do token replacement for each one.
  $elements = [];
  foreach ($entities as $delta => $entity) {
    if ($entity === NULL) {
      continue;
    }
    $url = $entity
      ->toUrl();
    if (empty($url) === TRUE) {
      $url = NULL;
    }
    else {
      $urlOptions = $url
        ->getOptions();
    }

    // We'd like to use generic token patterns for all styles
    // in the formatter's menu, but this doesn't work across all entity
    // types because there are no standards for tokens. For example,
    // the entity ID for a node is "nid", for a file is "fid", for
    // a comment is "cid", and for a user is "uid". Other entity types
    // will generically support an "id" token. We get similar issues
    // for the name and author of an entity.
    //
    // While we do support generic pattern fixes below, we do so only
    // for custom patterns where the user entering the tokens may not
    // know to use the right tokens for the right entity type.
    switch ($entityReferenceStyle) {
      case 'id':
        $title = (string) $entity
          ->id();
        break;
      case 'title':
        $title = $entity
          ->label();
        break;
      case 'title-id':
        $title = $entity
          ->label() . ' (' . (string) $entity
          ->id() . ')';
        break;
      default:

        // Replace tokens in the pattern.
        $title = $this->tokenService
          ->replace($pattern, [
          $entityType => $entity,
        ], [
          'langcode' => $langcode,
          'clear' => TRUE,
        ]);
        break;
    }

    // Because replaced text may include HTML, we cannot pass it directly as
    // the '#title' on a link, which will escape the HTML. Instead we
    // use FormattableMarkup.
    $title = new FormattableMarkup($title, []);

    // If the link is disabled, show the title text within a <span>.
    // Otherwise, build a URL and create a link.
    if ($this
      ->getSetting('showLink') === FALSE || $url === NULL) {
      $elements[$delta] = [
        '#type' => 'html_tag',
        '#tag' => 'span',
        '#value' => $title,
        '#attributes' => [
          'class' => $classes,
        ],
        '#cache' => [
          'tags' => $entity
            ->getCacheTags(),
        ],
      ];
    }
    else {
      $rel = '';
      $target = '';
      $download = FALSE;
      switch ($this
        ->getSetting('openLinkIn')) {
        case '_self':
          $target = '_self';
          break;
        case '_blank':
          $target = '_blank';
          break;
        case 'download':
          $download = TRUE;
          break;
      }
      $topic = $this
        ->getSetting('linkTopic');
      if ($topic !== 'any') {
        $rel .= $topic;
      }
      if (empty($rel) === FALSE) {
        $urlOptions['attributes']['rel'] = $rel;
      }
      if (empty($target) === FALSE) {
        $urlOptions['attributes']['target'] = $target;
      }
      if ($download === TRUE) {
        $urlOptions['attributes']['download'] = '';
      }
      $url
        ->setOptions($urlOptions);
      $elements[$delta] = [
        '#type' => 'link',
        '#title' => $title,
        '#options' => $url
          ->getOptions(),
        '#url' => $url,
        '#attributes' => [
          'class' => $classes,
        ],
        '#cache' => [
          'tags' => $entity
            ->getCacheTags(),
        ],
      ];
      if (empty($items[$delta]->_attributes) === FALSE) {

        // There are item attributes. Add them to the link's options.
        $elements[$delta]['#options'] += [
          'attributes' => $items[$delta]->_attributes,
        ];

        // And remove them from the item since they are now included
        // on the link.
        unset($items[$delta]->_attributes);
      }
    }
  }
  if (empty($elements) === TRUE) {
    return [];
  }

  //
  // Add multi-value field processing.
  // ---------------------------------
  // If the field has multiple values, redirect to a theme and pass
  // the list style and separator.
  $isMultiple = $this->fieldDefinition
    ->getFieldStorageDefinition()
    ->isMultiple();
  if ($isMultiple === TRUE) {

    // Replace the 'field' theme with ours, which supports lists.
    $elements['#theme'] = 'formatter_suite_field_list';

    // Set the list style.
    $elements['#list_style'] = $this
      ->getSetting('listStyle');

    // Set the list separator.
    //
    // Security: The list separator is entered by an administrator.
    // It may legitimately include HTML entities and minor HTML, but
    // it should not include dangerous HTML.
    //
    // Because it may include HTML, we cannot pass it as-is and let a TWIG
    // template use {{ }}, which will process the text and corrupt any
    // entered HTML or HTML entities.
    //
    // We therefore use an Xss admin filter to remove any egreggious HTML
    // (such as scripts and styles), and then FormattableMarkup() to mark the
    // resulting text as safe.
    $listSeparator = Xss::filterAdmin($this
      ->getSetting('listSeparator'));
    $elements['#list_separator'] = new FormattableMarkup($listSeparator, []);
  }
  return $elements;
}