You are here

trait CshsOptionsFromHelper in Client-side Hierarchical Select 8.2

Same name and namespace in other branches
  1. 8.3 src/CshsOptionsFromHelper.php \Drupal\cshs\CshsOptionsFromHelper
  2. 8 src/CshsOptionsFromHelper.php \Drupal\cshs\CshsOptionsFromHelper

Defines a class for getting options for a cshs form element from vocabulary.

Hierarchy

2 files declare their use of CshsOptionsFromHelper
CshsTaxonomyIndex.php in src/Plugin/views/filter/CshsTaxonomyIndex.php
CshsWidget.php in src/Plugin/Field/FieldWidget/CshsWidget.php

File

src/CshsOptionsFromHelper.php, line 39

Namespace

Drupal\cshs
View source
trait CshsOptionsFromHelper {
  use TaxonomyStorages;

  /**
   * Defines the default settings for this plugin.
   *
   * @return array
   *   A list of default settings, keyed by the setting name.
   */
  public static function defaultSettings() : array {
    return [
      'parent' => 0,
      'level_labels' => '',
      'force_deepest' => FALSE,
      'save_lineage' => FALSE,
      'hierarchy_depth' => 0,
      'required_depth' => 0,
      'none_label' => CshsElement::NONE_LABEL,
    ];
  }

  /**
   * Returns the array of settings, including defaults for missing settings.
   *
   * @return array
   *   The array of settings.
   */
  public abstract function getSettings() : array;

  /**
   * Returns the value of a setting, or its default value if absent.
   *
   * @param string $key
   *   The setting name.
   *
   * @return mixed
   *   The setting value.
   */
  public abstract function getSetting($key);

  /**
   * Returns the taxonomy vocabulary to work with.
   *
   * @return \Drupal\taxonomy\VocabularyInterface|null
   *   The taxonomy vocabulary.
   */
  public abstract function getVocabulary() : ?VocabularyInterface;

  /**
   * Returns a short summary for the settings.
   *
   * @return array
   *   A short summary of the settings.
   */
  public function settingsSummary() : array {
    $settings = $this
      ->getSettings();
    $summary = [];
    $deepest = $this
      ->t('Deepest');
    $none = $this
      ->t('None');
    $yes = $this
      ->t('Yes');
    $no = $this
      ->t('No');
    $summary[] = $this
      ->t('Parent: @parent', [
      '@parent' => empty($settings['parent']) ? $none : $this
        ->getTranslationFromContext($this
        ->getTermStorage()
        ->load($settings['parent']))
        ->label(),
    ]);
    foreach (HIERARCHY_OPTIONS as $option_name => [
      $title,
    ]) {

      /** @noinspection NestedTernaryOperatorInspection */

      // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
      $summary[] = $this
        ->t("{$title}: @{$option_name}", [
        "@{$option_name}" => empty($settings['force_deepest']) ? empty($settings[$option_name]) ? $none : $settings[$option_name] : $deepest,
      ]);
    }
    $summary[] = $this
      ->t('Force deepest: @force_deepest', [
      '@force_deepest' => empty($settings['force_deepest']) ? $no : $yes,
    ]);
    $summary[] = $this
      ->t('Save lineage: @save_lineage', [
      '@save_lineage' => empty($settings['save_lineage']) ? $no : $yes,
    ]);
    $summary[] = $this
      ->t('Level labels: @level_labels', [
      '@level_labels' => empty($settings['level_labels']) ? $none : $this
        ->getTranslatedLevelLabels(),
    ]);
    $summary[] = $this
      ->t('The "no selection" label: @none_label', [
      '@none_label' => $this
        ->getTranslatedNoneLabel(),
    ]);
    return $summary;
  }

  /**
   * Returns a form to configure settings.
   *
   * @param array $form
   *   The form where the settings form is being included in.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The form definition for the settings.
   */
  public function settingsForm(array $form, FormStateInterface $form_state) : array {
    $vocabulary = $this
      ->getVocabulary();
    \assert($vocabulary !== NULL);
    $options = [];

    // Build options for parent select field.
    foreach ($this
      ->getOptions($vocabulary
      ->id()) as $key => $value) {
      $options[$key] = $value['name'];
    }
    $element['parent'] = [
      '#type' => 'select',
      '#title' => $this
        ->t('Parent'),
      '#options' => $options,
      '#description' => $this
        ->t('Select a parent term to use only a subtree of a vocabulary for this field.'),
      '#default_value' => $this
        ->getSetting('parent'),
    ];
    foreach (HIERARCHY_OPTIONS as $option_name => [
      $title,
      $description,
    ]) {
      $description[] = '<i>Ignored when the deepest selection is enforced.</i>';
      $element[$option_name] = [
        '#min' => 0,
        '#type' => 'number',
        '#title' => $title,
        // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
        '#description' => $this
          ->t(\implode('<br />', $description)),
        '#default_value' => $this
          ->getSetting($option_name),
        '#states' => [
          'disabled' => [
            ':input[name*="force_deepest"]' => [
              'checked' => TRUE,
            ],
          ],
        ],
      ];
    }
    $element['force_deepest'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->t('Force selection of deepest level'),
      '#description' => $this
        ->t('If checked the user will be forced to select terms from the deepest level.'),
      '#default_value' => $this
        ->getSetting('force_deepest'),
    ];

    // This method can be called during Views filter configuration where
    // the "$this->fieldDefinition" is not available. Moreover, we don't
    // need to provide the "save_lineage" there.
    if ($this instanceof WidgetBase) {
      $errors = [];
      $field_storage = $this->fieldDefinition
        ->getFieldStorageDefinition();
      \assert($field_storage instanceof FieldStorageConfigInterface);
      $entity_storage = $this
        ->getStorage($field_storage
        ->getTargetEntityTypeId());
      \assert($entity_storage instanceof FieldableEntityStorageInterface);
      $is_unlimited = $field_storage
        ->getCardinality() === FieldStorageConfigInterface::CARDINALITY_UNLIMITED;
      $description = [
        'Save all parents of the selected term. Please note that you will not have a',
        'familiar field when multiple items can be added via the "Add more" button.',
        'In fact, the field will look like a "single" and the selected terms will',
        'be stored each as a separate field value.',
        '',
        '<b>Keep in mind</b> that this setting cannot be changed once the field storage',
        'gets at least one item. The restriction is imposed across all bundles of',
        'the "' . $entity_storage
          ->getEntityType()
          ->getLabel() . '" entity type.',
      ];
      if ($entity_storage
        ->countFieldData($field_storage, TRUE)) {
        $errors[] = 'There is data for this field in the database. This setting can no longer be changed.';
      }
      if (!$is_unlimited) {
        $errors[] = 'The option can be enabled only for fields with unlimited cardinality.';
      }
      if (!empty($errors)) {
        \array_unshift($description, \sprintf('<b class="color-error">%s</b>', \implode('<br />', $errors)));
      }
      $element['save_lineage'] = [
        '#type' => 'checkbox',
        '#title' => $this
          ->t('Save lineage'),
        '#disabled' => !empty($errors),
        // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
        '#description' => $this
          ->t(\implode('<br />', $description)),
        '#default_value' => $is_unlimited && $this
          ->getSetting('save_lineage'),
      ];
    }
    $element['level_labels'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('Labels per hierarchy-level'),
      '#description' => $this
        ->t('Enter labels for each hierarchy-level separated by comma.'),
      '#default_value' => $this
        ->getTranslatedLevelLabels(),
    ];
    $element['none_label'] = [
      '#type' => 'textfield',
      '#title' => $this
        ->t('The "no selection" label'),
      '#description' => $this
        ->t('The label for an empty option.'),
      '#default_value' => $this
        ->getTranslatedNoneLabel(),
    ];
    $element['#element_validate'][] = [
      $this,
      'validateSettingsForm',
    ];
    $form_state
      ->set('vocabulary', $vocabulary);
    return $element;
  }

  /**
   * Validates the settings form.
   *
   * @param array $element
   *   The element's form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function validateSettingsForm(array &$element, FormStateInterface $form_state) : void {
    $settings = $form_state
      ->getValue($element['#parents']);
    $options = $element['parent']['#options'];
    foreach ($options as $id => $label) {

      // This always removes at least the first item, which is what we
      // want. If a user selects nothing we remove the `- Please select -`
      // and count only the number of nesting levels. In another case,
      // we remove everything before and including the selected item and
      // count the rest.
      unset($options[$id]);

      // Leave the rest of the list after the selected option.
      if ((string) $id === $settings['parent']) {
        break;
      }
    }
    if ($settings['hierarchy_depth'] > ($max_hierarchy_depth = \count($options))) {
      $form_state
        ->setError($element['hierarchy_depth'], $this
        ->t('The hierarchy depth cannot be @actual because the selection list has @levels levels.', [
        '@actual' => $settings['hierarchy_depth'],
        '@levels' => $max_hierarchy_depth,
      ]));
    }
    elseif ($settings['required_depth'] > $settings['hierarchy_depth']) {
      $form_state
        ->setError($element['required_depth'], $this
        ->t('The required depth cannot be greater than the hierarchy depth.'));
    }
  }

  /**
   * Returns the form for a single widget.
   *
   * @return array
   *   The form elements for a single widget.
   */
  public function formElement() : array {
    $vocabulary = $this
      ->getVocabulary();
    \assert($vocabulary !== NULL);
    $settings = $this
      ->getSettings();
    if (!empty($settings['force_deepest']) || ($max_depth = $settings['hierarchy_depth']) < 1) {
      $max_depth = NULL;
    }
    return [
      '#type' => CshsElement::ID,
      '#labels' => $this
        ->getTranslatedLevelLabels(FALSE),
      '#parent' => $settings['parent'],
      '#options' => $this
        ->getOptions($vocabulary
        ->id(), $settings['parent'], CshsElement::NONE_VALUE, $max_depth),
      '#multiple' => $settings['save_lineage'],
      '#vocabulary' => $vocabulary,
      '#none_value' => CshsElement::NONE_VALUE,
      '#none_label' => $this
        ->getTranslatedNoneLabel(),
      '#default_value' => CshsElement::NONE_VALUE,
      '#force_deepest' => $settings['force_deepest'],
      '#required_depth' => $settings['required_depth'],
    ];
  }

  /**
   * Collects the options.
   *
   * @param string $vocabulary_id
   *   Name of taxonomy vocabulary.
   * @param int $parent
   *   ID of a parent term.
   * @param int|string $none_value
   *   Value for the first option.
   * @param int|null $max_depth
   *   The number of levels of the tree to return.
   *
   * @return array[]
   *   Widget options.
   */
  private function getOptions(string $vocabulary_id, int $parent = 0, $none_value = 0, int $max_depth = NULL) : array {
    \assert(\is_int($none_value) || \is_string($none_value));
    $cache =& \drupal_static(__METHOD__, []);
    $cache_id = "{$vocabulary_id}:{$parent}:{$none_value}:{$max_depth}";
    if (!isset($cache[$cache_id])) {
      $storage = $this
        ->getTermStorage();
      $cache[$cache_id] = [
        // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
        $none_value => CshsElement::option($this
          ->t(CshsElement::NONE_LABEL)),
      ];
      if ($this
        ->needsTranslatedContent()) {
        $get_name = function (object $term) use ($storage) : string {
          return $this
            ->getTranslationFromContext($storage
            ->load($term->tid))
            ->label();
        };
      }
      else {

        // Avoid loading the entity if we don't need its specific translation.
        $get_name = static function (object $term) : string {
          return $term->name;
        };
      }
      foreach ($storage
        ->loadTree($vocabulary_id, $parent, $max_depth, FALSE) as $term) {
        \assert($term instanceof \stdClass);
        \assert(\is_array($term->parents));
        \assert(\is_numeric($term->status));
        \assert(\is_numeric($term->depth));
        \assert(\is_numeric($term->tid));
        \assert(\is_string($term->name));

        // Allow only published terms.
        if ((bool) $term->status) {
          $parents = \array_values($term->parents);
          $cache[$cache_id][$term->tid] = CshsElement::option($get_name($term), (int) \reset($parents));
        }
      }
    }
    return $cache[$cache_id];
  }

  /**
   * Returns translated labels with escaped markup.
   *
   * @param bool $return_as_string
   *   Whether returning value have to be a string.
   *
   * @return string|string[]
   *   Translated labels, splitted by comma, or an array of them.
   */
  private function getTranslatedLevelLabels(bool $return_as_string = TRUE) {
    $labels = $this
      ->getSetting('level_labels');
    if (empty($labels)) {
      return $return_as_string ? '' : [];
    }
    $labels = \array_map([
      $this,
      'getTranslatedValue',
    ], Tags::explode($labels));
    return $return_as_string ? \implode(', ', $labels) : $labels;
  }

  /**
   * Returns the translated label for the "no selection" option.
   *
   * @return string
   *   The label.
   */
  private function getTranslatedNoneLabel() : string {
    return $this
      ->getTranslatedValue($this
      ->getSetting('none_label') ?: CshsElement::NONE_LABEL);
  }

  /**
   * Returns the translated label.
   *
   * @param string $value
   *   The value for translate.
   *
   * @return string
   *   The translated value.
   */
  private function getTranslatedValue(string $value) : string {
    $value = \trim($value);

    // phpcs:ignore Drupal.Semantics.FunctionT.NotLiteralString
    return $value === '' ? $value : (string) $this
      ->t(Html::escape($value));
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CshsOptionsFromHelper::defaultSettings public static function Defines the default settings for this plugin.
CshsOptionsFromHelper::formElement public function Returns the form for a single widget.
CshsOptionsFromHelper::getOptions private function Collects the options.
CshsOptionsFromHelper::getSetting abstract public function Returns the value of a setting, or its default value if absent. 2
CshsOptionsFromHelper::getSettings abstract public function Returns the array of settings, including defaults for missing settings. 2
CshsOptionsFromHelper::getTranslatedLevelLabels private function Returns translated labels with escaped markup.
CshsOptionsFromHelper::getTranslatedNoneLabel private function Returns the translated label for the "no selection" option.
CshsOptionsFromHelper::getTranslatedValue private function Returns the translated label.
CshsOptionsFromHelper::getVocabulary abstract public function Returns the taxonomy vocabulary to work with. 2
CshsOptionsFromHelper::settingsForm public function Returns a form to configure settings.
CshsOptionsFromHelper::settingsSummary public function Returns a short summary for the settings.
CshsOptionsFromHelper::validateSettingsForm public function Validates the settings form.
TaxonomyStorages::$entityRepository protected property An instance of the "entity.repository" service.
TaxonomyStorages::$entityTypeManager protected property An instance of the "entity_type.manager" service.
TaxonomyStorages::$needsTranslatedContent private property The state of whether content translation should be used.
TaxonomyStorages::getStorage private function Returns the entity storage.
TaxonomyStorages::getTermStorage protected function Returns storage of the "taxonomy_term" entities. 1
TaxonomyStorages::getTranslationFromContext protected function Returns the entity translation to be used in the given context.
TaxonomyStorages::getVocabularyStorage protected function Returns storage of the "taxonomy_vocabulary" entities. 1
TaxonomyStorages::needsTranslatedContent protected function Returns the state of whether content translation needed.