You are here

unique_field_ajax.module in Unique field ajax 2.x

Same filename and directory in other branches
  1. 8 unique_field_ajax.module

Unique value for cck fields check module.

File

unique_field_ajax.module
View source
<?php

/**
 * @file
 * Unique value for cck fields check module.
 */
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\ContentEntityType;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Field\Entity\BaseFieldOverride;
use Drupal\Core\Entity\EntityBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\Core\Cache\CacheableMetadata;

/**
 * Implements hook_help().
 */
function unique_field_ajax_help($route_name, RouteMatchInterface $route_match) {
  switch ($route_name) {

    // Main module help for the unique_field_ajax module.
    case 'help.page.unique_field_ajax':
      $output = '<h3>' . t('About') . '</h3>';
      $output .= '<p>' . t('The Unique Field module allows administrators to require that content supplied for specified fields is unique.') . '</p>';
      return $output;
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function unique_field_ajax_form_field_config_edit_form_alter(&$form, FormStateInterface $form_state, $form_id) {
  $field = $form_state
    ->getFormObject()
    ->getEntity();
  $unique_field_ajax_types = [
    'string',
    'list_string',
    'text',
    'email',
    'entity_reference',
    'path',
    'uri',
    'link',
    'integer',
    'decimal',
  ];
  if (!$field
    ->getFieldStorageDefinition()
    ->isMultiple()) {
    if (in_array($field
      ->getType(), $unique_field_ajax_types)) {
      $form['third_party_settings']['unique_field_ajax'] = [
        '#type' => 'details',
        '#group' => 'additional_settings',
        '#title' => t('Unique title settings'),
        '#open' => TRUE,
        '#tree' => TRUE,
      ];
      $fields = _unique_field_ajax_form_fields($field, 'third_party_settings[unique_field_ajax]');
      if (!empty($fields)) {
        foreach ($fields as $name => $details) {
          $form['third_party_settings']['unique_field_ajax'][$name] = $details;
        }
      }
    }
  }
}

/**
 * Alter forms to add unique title checkbox.
 *
 * @param array $form
 *   Form array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state.
 * @param string $form_id
 *   Form id.
 */
function unique_field_ajax_form_alter(array &$form, FormStateInterface $form_state, string $form_id) {
  if (_unique_field_ajax_is_config_form($form_id)) {
    $entity = $form_state
      ->getFormObject()
      ->getEntity();
    $form['unique_field_ajax'] = [
      '#type' => 'details',
      '#group' => 'additional_settings',
      '#title' => t('Unique title settings'),
      '#open' => TRUE,
      '#tree' => TRUE,
    ];
    $fields = _unique_field_ajax_form_fields($entity, 'unique_field_ajax');
    if (!empty($fields)) {
      foreach ($fields as $name => $details) {
        $form['unique_field_ajax'][$name] = $details;
      }
    }
    $form['#entity_builders'][] = '_unique_field_ajax_entity_form_builder';
  }
}

/**
 * Save the third party settings.
 *
 * @param string $entity_type
 *   Entity type.
 * @param \Drupal\Core\Entity\EntityInterface $entity
 *   Entity object.
 * @param array $form
 *   Form array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   Form state.
 */
function _unique_field_ajax_entity_form_builder(string $entity_type, EntityInterface $entity, array &$form, FormStateInterface $form_state) {
  $fields = [
    'unique',
    'per_lang',
    'use_ajax',
    'message',
  ];
  foreach ($fields as $field) {
    $value = $form_state
      ->getValue([
      'unique_field_ajax',
      $field,
    ]);
    if ($value) {
      $entity
        ->setThirdPartySetting('unique_field_ajax', $field, $value);
    }
    else {
      $entity
        ->unsetThirdPartySetting('unique_field_ajax', $field);
    }
  }
}

/**
 * Attaching data to unique fields.
 *
 * @param array $element
 *   Element array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   FormState.
 * @param array $context
 *   Context array.
 */
function unique_field_ajax_field_widget_form_alter(array &$element, FormStateInterface $form_state, array $context) {
  $field_definition = $context['items']
    ->getFieldDefinition();
  $name = $field_definition
    ->getFieldStorageDefinition()
    ->getName();
  $label = $field_definition
    ->getLabel();

  // Alter field widgets to enable unique settings.
  if ($field_definition instanceof FieldConfig) {
    $is_unique_per_lang = NULL;
    if (\Drupal::moduleHandler()
      ->moduleExists('language') && \Drupal::languageManager()
      ->getCurrentLanguage()
      ->getId()) {
      $is_unique_per_lang = $field_definition
        ->getThirdPartySetting('unique_field_ajax', 'per_lang');
    }
    if ($field_definition
      ->getThirdPartySetting('unique_field_ajax', 'unique')) {
      $main_property_name = $field_definition
        ->getFieldStorageDefinition()
        ->getMainPropertyName();
      $element[$main_property_name]['#unique_field_ajax_settings'] = [
        'per_lang' => $is_unique_per_lang,
        'field_definition' => $field_definition,
        'field_name' => $name,
        'field_label' => $label,
        'message' => $field_definition
          ->getThirdPartySetting('unique_field_ajax', 'message'),
      ];
      $element[$main_property_name]['#element_validate'][] = 'unique_field_ajax_validate_unique';
      if ($field_definition
        ->getThirdPartySetting('unique_field_ajax', 'use_ajax')) {
        $element['#process'] = [
          '_unique_field_ajax_process',
        ];
      }
    }
  }

  // Alter title to enable unique settings.
  if ($field_definition instanceof BaseFieldDefinition || $field_definition instanceof BaseFieldOverride) {
    $entity = NULL;
    if (isset($context['form']) && isset($context['form']['#type']) && $context['form']['#type'] == 'inline_entity_form') {
      $entity = $context['form']['#default_value'];
    }
    else {
      $form_object = $form_state
        ->getFormObject();
      if (method_exists($form_object, 'getEntity')) {
        $entity = $form_object
          ->getEntity();
      }
    }
    if (!$entity) {
      return;
    }
    $entity_type = $entity
      ->getEntityType();
    if (!$entity_type instanceof ContentEntityType || !$entity_type
      ->hasKey('label')) {
      return;
    }
    $entity_label = $entity_type
      ->getKey('label');
    if ($entity_label !== $name) {
      return;
    }
    $bundle_def_id = $entity_type
      ->getBundleEntityType();
    if (!$bundle_def_id) {
      return;
    }
    $bundle_def = \Drupal::service('entity_type.manager')
      ->getStorage($bundle_def_id)
      ->load($entity
      ->bundle());
    if (!$bundle_def
      ->getThirdPartySetting('unique_field_ajax', 'unique')) {
      return;
    }
    $is_unique_per_lang = NULL;
    if (\Drupal::moduleHandler()
      ->moduleExists('language') && \Drupal::languageManager()
      ->getCurrentLanguage()
      ->getId()) {
      $is_unique_per_lang = $bundle_def
        ->getThirdPartySetting('unique_field_ajax', 'per_lang');
    }
    $use_ajax = $bundle_def
      ->getThirdPartySetting('unique_field_ajax', 'use_ajax');
    $element['value']['#unique_field_ajax_settings'] = [
      'per_lang' => $is_unique_per_lang,
      'field_definition' => $field_definition,
      'field_name' => $name,
      'field_label' => $entity_label,
      'message' => $bundle_def
        ->getThirdPartySetting('unique_field_ajax', 'message'),
    ];
    $element['value']['#element_validate'][] = 'unique_field_ajax_validate_unique';
    if ($use_ajax) {
      $element['#process'] = [
        '_unique_field_ajax_process',
      ];
    }
  }
}

/**
 * Callback for process.
 *
 * @param array $form
 *   Form array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   FormState.
 *
 * @return mixed
 *   Return nested array.
 */
function _unique_field_ajax(array &$form, FormStateInterface $form_state) {
  $element = $form_state
    ->getTriggeringElement();
  return NestedArray::getValue($form, $element['#array_parents']);
}

/**
 * Attach ajax to unique field.
 *
 * @param array $element
 *   Element array.
 * @param \Drupal\Core\Form\FormStateInterface $form_state
 *   FormState.
 * @param array $form
 *   Form array.
 *
 * @return array
 *   Element array.
 *
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 * @throws \Drupal\Core\Entity\EntityMalformedException
 */
function _unique_field_ajax_process(array $element, FormStateInterface &$form_state, array &$form) : array {
  foreach (Element::children($element) as $property) {
    if (isset($element[$property]) && isset($element[$property]['#unique_field_ajax_settings'])) {
      $field_name = $element[$property]['#unique_field_ajax_settings']['field_name'];
      $unique_field_ajax_settings = $element[$property]['#unique_field_ajax_settings'];
      if ($unique_field_ajax_settings) {
        $field_label = $unique_field_ajax_settings['field_label'];
        $wrapper = 'unique-' . $field_name;
        $form['#attached']['library'][] = 'unique_field_ajax/unique_event';
        $settings = [
          'id' => '#' . $wrapper . ' input',
        ];
        $form['#attached']['drupalSettings']['unique_field_ajax'][] = $settings;
        $element[$property]['#ajax'] = [
          'callback' => '_unique_field_ajax',
          'event' => 'finishedinput',
          'wrapper' => $wrapper,
          'progress' => [
            'type' => 'throbber',
            'message' => t('Verifying @field_label...', [
              '@field_label' => $field_label,
            ]),
          ],
        ];
        $element[$property]['#prefix'] = '<div id="' . $wrapper . '">';
        $element[$property]['#suffix'] = '</div>';
        $value = $form_state
          ->getValue($field_name);
        $value = !empty($value) ? $value[0][$property] : NULL;
        if (!isset($value)) {
          return $element;
        }
        $entity = $form_state
          ->getFormObject()
          ->getEntity();
        $entity_type = $entity
          ->getEntityTypeId();
        $lang_code = !empty($form_state
          ->getValues()['langcode'][0][$property]) ? $form_state
          ->getValues()['langcode'][0][$property] : '0';
        $valid = unique_field_ajax_is_unique($entity_type, $lang_code, $field_name, $value, $entity
          ->bundle(), $element[$property]['#unique_field_ajax_settings']['per_lang'], $entity);
        $message = unique_field_ajax_custom_message($entity, $element, $valid, $property, TRUE);
        if ($valid !== TRUE) {
          $element[$property]['#attributes']['class'][] = 'error';
          $element[$property]['#attributes']['aria-invalid'] = 'true';
          $element[$property]['#suffix'] = '<div class="error">' . $message . '</div>' . $element[$property]['#suffix'];
        }
      }
    }
  }
  return $element;
}

/**
 * Determine if this is a form for title-able entity config.
 *
 * @param string $form_id
 *   Form id.
 *
 * @return bool
 *   Return if node edit or add.
 */
function _unique_field_ajax_is_config_form($form_id) : bool {
  $entity_config_forms = [
    'node_type_edit_form',
    'node_type_add_form',
  ];
  return in_array($form_id, $entity_config_forms);
}

/**
 * Return the form field information.
 *
 * @param object $field
 *   Field details.
 * @param string $inputLookup
 *   Input lookup.
 *
 * @return array
 *   Form fields.
 */
function _unique_field_ajax_form_fields($field, string $inputLookup = '') : array {
  $fields['unique'] = [
    '#type' => 'checkbox',
    '#title' => t("Unique"),
    '#default_value' => $field
      ->getThirdPartySetting('unique_field_ajax', 'unique'),
    '#weight' => -10,
  ];
  $fields['per_lang'] = [
    '#type' => 'checkbox',
    '#title' => t("Per Language"),
    '#description' => t("Do not allow duplicated content per language"),
    '#default_value' => $field
      ->getThirdPartySetting('unique_field_ajax', 'per_lang'),
    '#weight' => -9,
    '#states' => [
      'visible' => [
        ':input[name="' . $inputLookup . '[unique]"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];
  $fields['use_ajax'] = [
    '#type' => 'checkbox',
    '#title' => t("Use Ajax"),
    '#description' => t("Use ajax for validation."),
    '#default_value' => $field
      ->getThirdPartySetting('unique_field_ajax', 'use_ajax'),
    '#weight' => -8,
    '#states' => [
      'visible' => [
        ':input[name="' . $inputLookup . '[unique]"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];
  $description = t("The message when the value is not unique. The following tokens that can be used:");
  $description .= "<br/>" . t("The placeholder <em>%link</em> can be used to display a link to the entity matching the unique value.");
  $description .= "<br/>" . t("The placeholder <em>%label</em> can be used to display the field label.");
  $fields['message'] = [
    '#type' => 'textarea',
    '#title' => t("Error message"),
    '#description' => $description,
    '#default_value' => $field
      ->getThirdPartySetting('unique_field_ajax', 'message'),
    '#weight' => -7,
    '#states' => [
      'visible' => [
        ':input[name="' . $inputLookup . '[unique]"]' => [
          'checked' => TRUE,
        ],
      ],
    ],
  ];
  return $fields;
}

/**
 * Creates the default or custom message.
 *
 * @param \Drupal\Core\Entity\EntityBase $entity
 *   Entity object.
 * @param array $element
 *   Element array.
 * @param bool|int $valid
 *   Is valid state.
 * @param string $property
 *   Property value.
 * @param bool $isAjax
 *   Is ajax.
 *
 * @return \Drupal\Component\Render\MarkupInterface
 *   Returns the custom message.
 *
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 * @throws \Drupal\Core\Entity\EntityMalformedException
 */
function unique_field_ajax_custom_message(EntityBase $entity, array $element, $valid, string $property, bool $isAjax) : MarkupInterface {
  $unique_field_ajax_settings = $isAjax ? $element[$property]['#unique_field_ajax_settings'] : $element['#unique_field_ajax_settings'];
  $field_label = $unique_field_ajax_settings['field_label'];
  $message = $unique_field_ajax_settings['message'];
  $hasNid = $valid !== TRUE && $valid !== "0";

  // Default message if nothing set.
  if (empty($message)) {
    $message = t('The field "@field_label" has to be unique.', [
      '@field_label' => $field_label,
    ]);
  }

  // If message contains the string %link look up the matching entities.
  if (strpos($message, '%link') !== FALSE) {
    $replacement = '<em>' . t('Unknown entity') . '</em>';
    if ($hasNid) {
      $entity_with_value = \Drupal::entityTypeManager()
        ->getStorage($entity
        ->getEntityTypeId())
        ->load($valid);
      if ($entity_with_value && $entity_with_value
        ->access('view')) {
        $link = $entity_with_value
          ->toLink(NULL, 'canonical', [
          'attributes' => [
            '_target' => 'blank',
          ],
        ]);
        $replacement = $link
          ->toString();
        $link
          ->getUrl()
          ->toString(TRUE)
          ->applyTo($element);
        CacheableMetadata::createFromObject($entity_with_value)
          ->applyTo($element);
      }
    }
    $message = str_replace('%link', $replacement, $message);
  }

  // Replace %label with the field label.
  if (strpos($message, '%label') !== FALSE) {
    $message = str_replace('%label', $field_label, $message);
  }
  return Markup::create($message);
}

/**
 * Element Validate callback to validate a field.
 *
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 * @throws \Drupal\Core\Entity\EntityMalformedException
 */
function unique_field_ajax_validate_unique($element, FormStateInterface $form_state, array $form) {
  $field_definition = $element['#unique_field_ajax_settings']['field_definition'];
  $property = $field_definition
    ->getFieldStorageDefinition()
    ->getMainPropertyName();
  $entity = $form_state
    ->getFormObject()
    ->getEntity();

  // If !isset lang_code set it to 0.
  $langcode = !empty($form_state
    ->getValues()['langcode'][0]['value']) && $form_state
    ->getValues()['langcode'][0]['value'] ? $form_state
    ->getValues()['langcode'][0]['value'] : '0';
  $field_name = $element['#unique_field_ajax_settings']['field_name'];
  $value = $form_state
    ->getValue($field_name);
  $entity_type = $entity
    ->getEntityTypeId();
  $per_lang = $element['#unique_field_ajax_settings']['per_lang'];

  // If field is not unique set error.
  $valid = unique_field_ajax_is_unique($entity_type, $langcode, $field_name, $value[0][$property], $entity
    ->bundle(), $per_lang, $entity);
  $message = unique_field_ajax_custom_message($entity, $element, $valid, $property, FALSE);
  if ($valid !== TRUE) {
    $form_state
      ->setErrorByName($field_name, $message);
    $form_state
      ->setRebuild();
  }
}

/**
 * Test if the field value already exist in the database.
 *
 * @param string $entity_type
 *   Entity type name.
 * @param string $lang_code
 *   Language code.
 * @param string $field_name
 *   Field name.
 * @param string $field_value
 *   Field value.
 * @param string $bundle
 *   Bundle.
 * @param bool|null $is_unique_per_lang
 *   Is unique per lang flag.
 * @param \Drupal\Core\Entity\EntityBase $entity
 *   The entity object.
 *
 * @return bool|string
 *   Boolean TRUE if the value is unique. In any other case, the entity ID of
 *   the entity that has the same value.
 *
 * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
 */
function unique_field_ajax_is_unique(string $entity_type, string $lang_code, string $field_name, string $field_value, string $bundle, ?bool $is_unique_per_lang, EntityBase $entity) {
  $entity_type_definition = Drupal::entityTypeManager()
    ->getDefinition($entity_type);
  $query = Drupal::entityQuery($entity_type)
    ->range(0, 1)
    ->condition($field_name, $field_value, '=');
  if (!$entity
    ->isNew()) {
    $query
      ->condition($entity_type_definition
      ->getKey('id'), $entity
      ->id(), '<>');
  }

  // Test if the entity has a bundle.
  if (!empty($entity_type_definition
    ->getKey('bundle'))) {
    $query
      ->condition($entity_type_definition
      ->getKey('bundle'), $bundle, '=');
  }

  // Test unique per language.
  if ($is_unique_per_lang) {
    $query
      ->condition('langcode', $lang_code);
  }
  $entities = $query
    ->execute();
  if (!empty($entities)) {
    return array_shift($entities);
  }
  return TRUE;
}

Functions

Namesort descending Description
unique_field_ajax_custom_message Creates the default or custom message.
unique_field_ajax_field_widget_form_alter Attaching data to unique fields.
unique_field_ajax_form_alter Alter forms to add unique title checkbox.
unique_field_ajax_form_field_config_edit_form_alter Implements hook_form_BASE_FORM_ID_alter().
unique_field_ajax_help Implements hook_help().
unique_field_ajax_is_unique Test if the field value already exist in the database.
unique_field_ajax_validate_unique Element Validate callback to validate a field.
_unique_field_ajax Callback for process.
_unique_field_ajax_entity_form_builder Save the third party settings.
_unique_field_ajax_form_fields Return the form field information.
_unique_field_ajax_is_config_form Determine if this is a form for title-able entity config.
_unique_field_ajax_process Attach ajax to unique field.