custom_field-1.0.x-dev/src/Plugin/Field/FieldType/CustomItem.php

src/Plugin/Field/FieldType/CustomItem.php
<?php

declare(strict_types=1);

namespace Drupal\custom_field\Plugin\Field\FieldType;

use Drupal\Component\Plugin\Exception\PluginException;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Field\Attribute\FieldType;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Form\SubformStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\custom_field\Plugin\CustomField\FieldType\DateRangeType;
use Drupal\custom_field\Plugin\CustomField\FieldType\DateTimeType;
use Drupal\custom_field\Plugin\CustomFieldTypeManagerInterface;
use Drupal\custom_field\Time;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;

/**
 * Plugin implementation of the 'custom' field type.
 */
#[FieldType(
  id: 'custom',
  label: new TranslatableMarkup('Custom field'),
  description: new TranslatableMarkup('A field of fields stored in a single table.'),
  default_widget: 'custom_stacked',
  default_formatter: 'custom_formatter',
  list_class: '\Drupal\custom_field\Plugin\Field\FieldType\CustomItemList',
)]
class CustomItem extends FieldItemBase {

  use StringTranslationTrait;

  /**
   * The custom field separator for extended properties.
   *
   * @var string
   */
  public const SEPARATOR = '__';

  /**
   * {@inheritdoc}
   */
  public static function mainPropertyName(): ?string {
    return NULL;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, mixed>
   *   A list of default settings, keyed by the setting name.
   */
  public static function defaultStorageSettings(): array {
    // Need to have at least one item by default because the table is created
    // before the user gets a chance to customize and will throw an Exception
    // if there isn't at least one column defined.
    return [
      'columns' => [
        'value' => [
          'name' => 'value',
          'type' => 'string',
        ],
      ],
    ] + parent::defaultStorageSettings();
  }

  /**
   * {@inheritdoc}
   */
  public static function schema(FieldStorageDefinitionInterface $field_definition): array {
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeManager $plugin_service */
    $plugin_service = \Drupal::service('plugin.manager.custom_field_type');
    $columns = [];
    foreach ($field_definition->getSetting('columns') as $item) {
      $plugin = $plugin_service->createInstance($item['type']);
      if (method_exists($plugin, 'schema')) {
        $field_schema = $plugin->schema($item);
        $columns += $field_schema;
      }
    }

    $schema['columns'] = $columns;

    return $schema;
  }

  /**
   * {@inheritdoc}
   */
  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array {
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeManager $plugin_service */
    $plugin_service = \Drupal::service('plugin.manager.custom_field_type');
    $properties = [];

    foreach ($field_definition->getSetting('columns') as $item) {
      try {
        $plugin = $plugin_service->createInstance($item['type']);
        if (method_exists($plugin, 'propertyDefinitions')) {
          $definitions = $plugin->propertyDefinitions($item);
          if (is_array($definitions)) {
            $properties += $definitions;
          }
        }
      }
      catch (PluginException $e) {
        continue;
      }
    }

    return $properties;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, mixed>
   *   An associative array of values.
   */
  public static function generateSampleValue(FieldDefinitionInterface $field_definition): array {
    $field_manager = \Drupal::service('plugin.manager.custom_field_type');
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeInterface[] $custom_items */
    $custom_items = $field_manager->getCustomFieldItems($field_definition->getSettings());
    $target_entity_type = $field_definition->getTargetEntityTypeId();
    $values = [];
    foreach ($custom_items as $name => $custom_item) {
      $values[(string) $name] = $custom_item->generateSampleValue($custom_item, $target_entity_type);
    }

    return $values;
  }

  /**
   * {@inheritdoc}
   */
  public function getConstraints(): array {
    $constraints = parent::getConstraints();
    $constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
    $settings = $this->getSettings();
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeManager $plugin_service */
    $plugin_service = \Drupal::service('plugin.manager.custom_field_type');
    $field_constraints = [];
    $custom_items = $plugin_service->getCustomFieldItems($settings);
    foreach ($custom_items as $name => $custom_item) {
      $item_constraints = $custom_item->getConstraints();
      if (!empty($item_constraints)) {
        $field_constraints[$name] = $custom_item->getConstraints();
      }
    }

    if (!empty($field_constraints)) {
      $constraints[] = $constraint_manager->create('ComplexData', $field_constraints);
    }

    return $constraints;
  }

  /**
   * {@inheritdoc}
   */
  public function preSave(): void {
    parent::preSave();
    $field_definition = $this->getFieldDefinition();
    $field_name = $field_definition->getName();
    $custom_items = $this->getCustomFieldManager()->getCustomFieldItems($field_definition->getSettings());
    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
    $entity = $this->getEntity();
    $is_default_translation = FALSE;
    $has_translations = FALSE;
    $original_entity = $entity;

    if (!$entity->isNew() && $entity->isTranslatable() && $field_definition->isTranslatable()) {
      $is_default_translation = $entity->isDefaultTranslation();
      $has_translations = count($entity->getTranslationLanguages()) > 1;
      $original_entity = $has_translations ? $entity->getUntranslated() : $entity;
    }

    // Get the fields from the original or current entity based on whether it
    // has translations.
    /** @var \Drupal\custom_field\Plugin\Field\FieldType\CustomFieldItemListInterface<\Drupal\custom_field\Plugin\Field\FieldType\CustomItem>[] $original_fields */
    $original_fields = $original_entity->get($field_name);
    foreach ($original_fields as $delta => $original_field) {

      $current_field = $entity->get($field_name)->get($delta);
      if (!$current_field instanceof CustomItem) {
        continue;
      }

      foreach ($custom_items as $name => $custom_item) {
        $field_type = $custom_item->getDataType();
        $is_subfield_translatable = $custom_item->getFieldSetting('translatable') ?? FALSE;

        // The synchronization logic only applies if the entity supports
        // translations, and we're not in the default language.
        if ($has_translations && !$is_default_translation && !$is_subfield_translatable) {
          // Fetch the value from the default language for this delta.
          $default_value = $original_field->{$name};
          if (!empty($default_value)) {
            $current_field->{$name} = $default_value;
            // Set extra default language properties for image.
            if ($field_type === 'image') {
              $alt = $original_field->{$name . self::SEPARATOR . 'alt'};
              $title = $original_field->{$name . self::SEPARATOR . 'title'};
              $width = $original_field->{$name . self::SEPARATOR . 'width'};
              $height = $original_field->{$name . self::SEPARATOR . 'height'};
              $current_field->{$name . self::SEPARATOR . 'alt'} = $alt;
              $current_field->{$name . self::SEPARATOR . 'title'} = $title;
              $current_field->{$name . self::SEPARATOR . 'width'} = $width;
              $current_field->{$name . self::SEPARATOR . 'height'} = $height;
            }
            if ($field_type === 'link') {
              $title = $original_field->{$name . self::SEPARATOR . 'title'};
              $options = $original_field->{$name . self::SEPARATOR . 'options'};
              $current_field->{$name . self::SEPARATOR . 'title'} = $title;
              $current_field->{$name . self::SEPARATOR . 'options'} = $options;
            }
            if (in_array($field_type, ['datetime', 'daterange'])) {
              $timezone = $original_field->{$name . self::SEPARATOR . 'timezone'};
              $current_field->{$name . self::SEPARATOR . 'timezone'} = $timezone;
            }
            if (in_array($field_type, ['daterange', 'time_range'])) {
              $end = $original_field->{$name . self::SEPARATOR . 'end'};
              $duration = $original_field->{$name . self::SEPARATOR . 'duration'};
              $current_field->{$name . self::SEPARATOR . 'end'} = $end;
              $current_field->{$name . self::SEPARATOR . 'duration'} = $duration;
            }
          }
        }

        // Set subfield value to current applicable translation value.
        $subfield_value = $current_field->{$name};

        // Existing field type handling logic, which should work for all cases:
        switch ($field_type) {
          case 'color':
            $color = is_string($subfield_value) ? trim($subfield_value) : '';
            if (str_starts_with($color, '#')) {
              $color = substr($color, 1);
            }

            // Make sure we have a valid hexadecimal color.
            $current_field->{$name} = strlen($color) === 6 ? '#' . strtoupper($color) : NULL;
            break;

          case 'map':
          case 'map_string':
            if (!is_array($subfield_value) || empty($subfield_value)) {
              $current_field->{$name} = NULL;
            }
            else {
              $current_field->{$name} = array_values($subfield_value);
            }
            break;

          case 'decimal':
            if (is_numeric($subfield_value)) {
              $scale = $custom_item->getScale();
              if (is_string($subfield_value)) {
                $subfield_value = (float) $subfield_value;
              }
              $current_field->{$name} = round($subfield_value, $scale);
            }
            break;

          case 'time':
            if ($subfield_value == '86401') {
              $current_field->{$name} = NULL;
            }
            break;

          case 'image':
            if (empty($subfield_value)) {
              foreach (['alt', 'title', 'width', 'height'] as $extra_property) {
                $current_field->{$name . self::SEPARATOR . $extra_property} = NULL;
              }
            }
            else {
              if (is_array($subfield_value) && isset($subfield_value['target_id'])) {
                $subfield_value = $subfield_value['target_id'];
              }
              if (!empty($subfield_value)) {
                $width = $current_field->get($name . self::SEPARATOR . 'width')
                  ->getValue();
                $height = $current_field->get($name . self::SEPARATOR . 'height')
                  ->getValue();
                if (empty($width) || empty($height)) {
                  /** @var \Drupal\file\FileInterface $file */
                  $file = \Drupal::entityTypeManager()
                    ->getStorage('file')
                    ->load($subfield_value);
                  if ($file) {
                    $image = \Drupal::service('image.factory')
                      ->get($file->getFileUri());
                    if ($image->isValid()) {
                      $current_field->{$name . self::SEPARATOR . 'width'} = $image->getWidth();
                      $current_field->{$name . self::SEPARATOR . 'height'} = $image->getHeight();
                    }
                  }
                }
              }
            }
            break;

          case 'link':
            if (empty($subfield_value)) {
              // Set extra properties to empty state.
              $current_field->{$name . self::SEPARATOR . 'title'} = NULL;
              $current_field->{$name . self::SEPARATOR . 'options'} = [];
            }
            break;

          case 'datetime':
            if (empty($subfield_value)) {
              // Set timezone to NULL when there is no date value.
              $current_field->{$name . self::SEPARATOR . 'timezone'} = NULL;
            }
            break;

          case 'daterange':
            if (empty($subfield_value)) {
              foreach (['end', 'timezone', 'duration'] as $extra_property) {
                $current_field->{$name . self::SEPARATOR . $extra_property} = NULL;
              }
            }
            else {
              $start_date = $current_field->get($name . self::SEPARATOR . 'start_date')
                ->getValue();
              $end_date = $current_field->get($name . self::SEPARATOR . 'end_date')
                ->getValue();
              $duration = NULL;
              if ($start_date instanceof DrupalDateTime) {
                $duration = 0;
                if ($end_date instanceof DrupalDateTime) {
                  $difference = $end_date->getTimestamp() - $start_date->getTimestamp();
                  if ($difference > 0) {
                    $duration = $difference;
                  }
                }
                $current_field->{$name . self::SEPARATOR . 'duration'} = $duration;
              }
            }
            break;

          case 'time_range':
            $start_value = $current_field->get($name)->getValue();
            $end_value = $current_field->get($name . self::SEPARATOR . 'end')->getValue();
            $duration = NULL;
            try {
              $start_time = Time::createFromTimestamp($start_value);
              $end_time = Time::createFromTimestamp($end_value);
            }
            catch (\Exception $e) {
              $start_time = NULL;
              $end_time = NULL;
            }
            if (!$start_time) {
              foreach (['end', 'duration'] as $extra_property) {
                $current_field->{$name . self::SEPARATOR . $extra_property} = NULL;
              }
            }
            else {
              if ($end_time) {
                $start_timestamp = $start_time->getTimestamp();
                $end_timestamp = $end_time->getTimestamp();
                if ($start_timestamp > $end_timestamp) {
                  $current_field->{$name . self::SEPARATOR . 'end'} = NULL;
                }
                else {
                  $difference = $end_timestamp - $start_timestamp;
                  if ($difference > 0) {
                    $duration = $difference;
                  }
                }
              }
              $current_field->{$name . self::SEPARATOR . 'duration'} = $duration;
            }
            break;

          case 'viewfield':
            if (empty($subfield_value)) {
              // Set extra properties to NULL.
              foreach (['display', 'arguments', 'items'] as $extra_property) {
                $current_field->{$name . self::SEPARATOR . $extra_property} = NULL;
              }
            }
            break;
        }
      }
    }
  }

  /**
   * {@inheritdoc}
   *
   * @param array<string, mixed> $form
   *   The form where the settings form is being included in.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the (entire) configuration form.
   * @param bool $has_data
   *   TRUE if the field already has data, FALSE if not.
   *
   * @return array<string, mixed>
   *   The form definition for the field settings.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data): array {
    assert($form_state instanceof SubformStateInterface);
    $form_state = $form_state->getCompleteFormState();
    $parents = ['field_storage', 'subform', 'settings'];
    $storage = $form_state->getStorage();
    $settings = $this->getSettings();
    $current_settings = $form_state->get('current_settings');
    $field_name = $this->getFieldDefinition()->getName();
    // Calculate a safe max column length to coincide with SQL column limit.
    $max_name_length = 64 - strlen($field_name) - 12;
    $is_cloning = FALSE;
    if (empty($current_settings)) {
      $form_state->set('current_settings', $this->getSettings());
    }

    if ($form_state->isRebuilding()) {
      $settings['items'] = $form_state->getValue([...$parents, 'items']) ?? $current_settings['columns'];
      $is_cloning = !empty($form_state->getValue([...$parents, 'clone']));
      $field_settings = $form_state->getValue(['settings', 'field_settings']) ?? $current_settings['field_settings'];
      $current_columns = $current_settings['columns'];
      $columns = $settings['items'];
      $user_input = $form_state->getUserInput();
      $input = NestedArray::getValue($user_input, [...$parents, 'items']);
      $widget_input = NestedArray::getValue($user_input, ['settings', 'field_settings']);
      $default_value_input = NestedArray::getValue($user_input, ['default_value_input', $field_name]);
      $reset_input = FALSE;
      foreach ($settings['items'] as $name => $item) {
        unset($item['remove']);
        if ($name != $item['name']) {
          $settings['items'][$item['name']] = $item;
          $columns[$item['name']] = $item;
          unset($settings['items'][$name]);
          unset($columns[$name]);
          if (isset($field_settings[$name])) {
            unset($field_settings[$name]);
          }
          // Clear out existing input.
          if (isset($input[$name])) {
            unset($input[$name]);
          }
          // Clear out existing widget input.
          if (isset($widget_input[$name])) {
            unset($widget_input[$name]);
          }
          $reset_input = TRUE;
        }
        elseif (isset($current_columns[$name])) {
          $diff_item = array_filter($item, 'is_scalar');
          $diff_column = array_filter($current_columns[$name], 'is_scalar');
          $diffs = array_diff($diff_item, $diff_column);
          if (!empty($diffs)) {
            $columns[$name] = $item;
            if (isset($field_settings[$name])) {
              unset($field_settings[$name]);
            }
          }
          if ($item['type'] !== $current_columns[$name]['type']) {
            // Clear out existing widget.
            if (isset($field_settings[$name])) {
              unset($field_settings[$name]);
            }
            // Clear out existing widget input.
            if (isset($widget_input[$name])) {
              unset($widget_input[$name]);
              $reset_input = TRUE;
            }
            // Clear out default value input.
            foreach ($default_value_input as $delta => $default_value) {
              if (isset($default_value[$name])) {
                unset($default_value_input[$delta][$name]);
                $reset_input = TRUE;
              }
            }

            if (isset($item['length'])) {
              // Unset length.
              if (!in_array($item['type'], ['string', 'telephone'])) {
                unset($item['length']);
                if (isset($input[$name]['length'])) {
                  unset($input[$name]['length']);
                }
              }
              // Reset length.
              else {
                $item['length'] = NULL;
                if (isset($input[$name]['length'])) {
                  $input[$name]['length'] = NULL;
                }
              }
              $columns[$name] = $item;
              $settings['items'][$name] = $item;
              $reset_input = TRUE;
            }

            if ($item['type'] === 'entity_reference') {
              $settings['items'][$name]['target_type'] = NULL;
              // Force the selection of target type.
              unset($columns[$name]);
            }
            elseif (in_array($item['type'], ['file', 'image'])) {
              $settings['items'][$name]['target_type'] = 'file';
            }
            elseif ($item['type'] === 'viewfield') {
              $settings['items'][$name]['target_type'] = 'view';
            }
          }
        }
      }
      $form_state->set('current_settings', [
        'columns' => $columns,
        'field_settings' => $field_settings,
      ]);
      if ($reset_input) {
        $user_input = $form_state->getUserInput();
        NestedArray::setValue($user_input, [...$parents, 'items'], $input);
        NestedArray::setValue($user_input, ['settings', 'field_settings'], $widget_input);
        NestedArray::setValue($user_input, ['default_value_input', $field_name], $default_value_input);
        $form_state->setUserInput($user_input);
      }
    }
    else {
      $settings['items'] = $settings['columns'];
    }

    $element = [
      '#tree' => TRUE,
      'columns' => [
        '#type' => 'value',
        '#value' => $settings['columns'],
        '#parents' => [...$parents, 'columns'],
      ],
      'items' => [
        '#type' => 'container',
        '#parents' => [...$parents, 'items'],
        '#title' => $this->t('Custom field items'),
        '#attributes' => [
          'style' => 'display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 1rem;',
        ],
      ],
      'actions' => [
        '#type' => 'actions',
        '#weight' => -9,
      ],
    ];

    $items_count = count($settings['items']);

    // Support copying settings from another custom field.
    if (!$has_data) {
      $sources = $this->getExistingCustomFieldStorageOptions($storage['entity_type_id']);
      if (!empty($sources)) {
        $element['clone'] = [
          '#type' => 'select',
          '#title' => $this->t('Clone settings from:'),
          '#description' => $this->t('Copy configuration from an existing field.'),
          '#options' => [
            '' => $this->t("- Don't clone settings -"),
          ] + $sources,
          '#attributes' => [
            'data-id' => 'custom-field-storage-clone',
          ],
          '#weight' => -10,
        ];
        $element['clone_message'] = [
          '#type' => 'container',
          '#states' => [
            'invisible' => [
              'select[data-id="custom-field-storage-clone"]' => ['value' => ''],
            ],
          ],
          // Initialize the display, so we don't see it flash on init page load.
          '#attributes' => [
            'style' => 'display: none;',
          ],
        ];
        $element['clone_message']['message'] = [
          '#markup' => 'The selected custom field field settings will be cloned. Any existing settings for this field will be overwritten. Field widget and formatter settings will not be cloned.',
          '#prefix' => '<div class="messages messages--warning" role="alert" style="display: block;">',
          '#suffix' => '</div>',
        ];
        // Add states to items.
        $element['items']['#states'] = [
          'visible' => [
            'select[data-id="custom-field-storage-clone"]' => ['value' => ''],
          ],
        ];
      }
    }

    foreach ($settings['items'] as $i => $item) {
      $type = $item['type'] ?? '';
      $element['items'][$i] = [
        '#type' => 'details',
        '#title' => $this->t('@label', ['@label' => $item['name']]),
        '#attributes' => [
          'style' => 'margin-top: 0; margin-bottom: 0;',
        ],
        '#open' => !$has_data,
      ];
      $element['items'][$i]['name'] = [
        '#type' => 'machine_name',
        '#description' => $this->t('A unique machine-readable name containing only letters, numbers, or single underscores.'),
        '#default_value' => $item['name'],
        '#disabled' => $has_data,
        '#machine_name' => [
          'source' => ['items', $i, 'name'],
          'exists' => [$this, 'machineNameExists'],
          'label' => $this->t('Machine-readable name'),
          'standalone' => FALSE,
          'error' => $this->t('The machine-readable name must contain only letters, numbers, or single underscores'),
        ],
        '#maxlength' => $max_name_length,
        '#size' => 20,
      ];
      if (!$has_data) {
        $element['items'][$i]['name']['#machine_name']['replace_pattern'] = '[^a-zA-Z0-9_]+|__';
      }
      $element['items'][$i]['type'] = [
        '#type' => 'select',
        '#title' => $this->t('Type'),
        '#options' => $this->getCustomFieldManager()->fieldTypeOptions(),
        '#default_value' => $type,
        '#delta' => $i,
        '#required' => TRUE,
        '#empty_option' => $this->t('- Select -'),
        '#disabled' => $has_data,
      ];
      // Length field for supported types.
      if (in_array($type, ['string', 'telephone'])) {
        $default_max = $type === 'telephone' ? 256 : 255;
        $element['items'][$i]['length'] = [
          '#type' => 'number',
          '#title' => $this->t('Length'),
          '#default_value' => !empty($item['length']) ? $item['length'] : $default_max,
          '#required' => TRUE,
          '#description' => $this->t('The maximum length of the field in characters.'),
          '#min' => 1,
          '#max' => $default_max,
          '#disabled' => $has_data,
        ];
      }
      // Size field for supported types.
      if (in_array($type, ['integer', 'float'])) {
        $element['items'][$i]['size'] = [
          '#type' => 'select',
          '#title' => $this->t('Size'),
          '#default_value' => $item['size'] ?? 'normal',
          '#disabled' => $has_data,
          '#options' => [
            'tiny' => $this->t('Tiny'),
            'small' => $this->t('Small'),
            'medium' => $this->t('Medium'),
            'big' => $this->t('Big'),
            'normal' => $this->t('Normal'),
          ],
        ];
      }
      // Unsigned field for supported types.
      if (in_array($type, ['integer', 'float', 'decimal'])) {
        $element['items'][$i]['unsigned'] = [
          '#type' => 'checkbox',
          '#title' => $this->t('Unsigned'),
          '#default_value' => $item['unsigned'] ?? FALSE,
          '#disabled' => $has_data,
        ];
      }
      // Decimal field extra settings.
      if ($type === 'decimal') {
        $element['items'][$i]['precision'] = [
          '#type' => 'number',
          '#title' => $this->t('Precision'),
          '#min' => 10,
          '#max' => 32,
          '#default_value' => $item['precision'] ?? 10,
          '#description' => $this->t('The total number of digits to store in the database, including those to the right of the decimal.'),
          '#disabled' => $has_data,
          '#required' => TRUE,
        ];
        $element['items'][$i]['scale'] = [
          '#type' => 'number',
          '#title' => $this->t('Scale'),
          '#description' => $this->t('The number of digits to the right of the decimal.'),
          '#default_value' => $item['scale'] ?? 2,
          '#disabled' => $has_data,
          '#min' => 0,
          '#max' => 10,
          '#required' => TRUE,
        ];
      }
      // Datetime field extra settings.
      if ($type === 'datetime' || $type === 'daterange') {
        $datetime_type_options = [
          DateTimeType::DATETIME_TYPE_DATETIME => $this->t('Date and time'),
          DateTimeType::DATETIME_TYPE_DATE => $this->t('Date only'),
        ];
        if ($type === 'daterange') {
          $datetime_type_options[DateRangeType::DATETIME_TYPE_ALLDAY] = $this->t('All day');
        }
        $element['items'][$i]['datetime_type'] = [
          '#type' => 'select',
          '#title' => $this->t('Date type'),
          '#description' => $this->t('Choose the type of date to create.'),
          '#default_value' => $item['datetime_type'] ?? DateTimeType::DATETIME_TYPE_DATETIME,
          '#disabled' => $has_data,
          '#options' => $datetime_type_options,
          '#required' => TRUE,
        ];
      }
      // Entity reference field extra settings.
      if ($type === 'entity_reference') {
        // Only allow the field to target entity types that have an ID key. This
        // is enforced in ::propertyDefinitions().
        $entity_type_manager = \Drupal::entityTypeManager();
        $filter = function (string $entity_type_id) use ($entity_type_manager): bool {
          return $entity_type_manager->getDefinition($entity_type_id)
            ->hasKey('id');
        };
        $options = \Drupal::service('entity_type.repository')->getEntityTypeLabels(TRUE);

        $element['items'][$i]['target_type'] = [
          '#type' => 'select',
          '#title' => $this->t('Type of item to reference'),
          '#default_value' => $item['target_type'] ?? NULL,
          '#required' => TRUE,
          '#disabled' => $has_data,
          '#size' => 1,
        ];
        foreach ($options as $group_name => $group) {
          $element['items'][$i]['target_type']['#options'][$group_name] = array_filter($group, $filter, ARRAY_FILTER_USE_KEY);
        }
      }
      // File & Image field extra settings.
      if ($type === 'file' || $type === 'image') {
        $element['#attached']['library'][] = 'file/drupal.file';
        $scheme_options = \Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE);
        $element['items'][$i]['uri_scheme'] = [
          '#type' => 'radios',
          '#title' => $this->t('Upload destination'),
          '#options' => $scheme_options,
          '#default_value' => $item['uri_scheme'] ?? \Drupal::config('system.file')->get('default_scheme'),
          '#description' => $this->t('Select where the final files should be stored. Private file storage has significantly more overhead than public files, but allows restricted access to files within this field.'),
          '#disabled' => $has_data,
        ];
        $element['items'][$i]['target_type'] = [
          '#type' => 'value',
          '#value' => 'file',
        ];
      }
      // Viewfield extra settings.
      elseif ($type === 'viewfield') {
        $element['items'][$i]['target_type'] = [
          '#type' => 'value',
          '#value' => 'view',
        ];
      }
      $element['items'][$i]['remove'] = [
        '#type' => 'submit',
        '#value' => $this->t('Remove'),
        '#submit' => [get_class($this) . '::removeSubmit'],
        '#name' => 'remove_' . $i,
        '#delta' => $i,
        '#access' => !($has_data || $items_count === 1),
        '#ajax' => [
          'callback' => [$this, 'actionCallback'],
          'wrapper' => 'field-combined',
        ],
      ];
    }

    if (!$has_data && !$is_cloning) {
      $element['actions']['add'] = [
        '#type' => 'submit',
        '#value' => $this->t('Add sub-field'),
        '#submit' => [get_class($this) . '::addSubmit'],
        '#name' => 'custom_field_add_item',
        '#ajax' => [
          'callback' => [$this, 'actionCallback'],
          'wrapper' => 'field-combined',
        ],
        '#attributes' => [
          'class' => [
            'button--primary',
          ],
        ],
      ];
    }

    $form_state->setCached(FALSE);

    return $element;
  }

  /**
   * Submit handler for the StorageConfigEditForm.
   *
   * This handler is added in custom_field.module since it has to be placed
   * directly on the submit button (which we don't have access to in our
   * ::storageSettingsForm() method above).
   *
   * @param array<string, mixed> $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public static function submitStorageConfigEditForm(array &$form, FormStateInterface $form_state): void {
    // Rekey our column settings and overwrite the values in form_state so that
    // we have clean settings saved to the db.
    $columns = [];
    $parents = ['field_storage', 'subform', 'settings'];
    $item_parents = ['field_storage', 'subform', 'settings', 'items'];
    /** @var \Drupal\Core\Field\FieldConfigInterface $field_config */
    $field_config = $form_state->get('field_config');

    if ($field_name = $form_state->getValue([...$parents, 'clone'])) {
      [$entity_type, $bundle_name, $field_name] = explode('.', $field_name);
      $source_storage = FieldStorageConfig::loadByName($entity_type, $field_name);
      $source_field_config = FieldConfig::loadByName($entity_type, $bundle_name, $field_name);
      // Grab the columns from the field storage config.
      $columns = $source_storage->getSetting('columns');
      $field_settings = $source_field_config->getSettings();
      $field_config->setSettings($field_settings);
      $form_state->setValue(['settings'], $field_settings);
    }
    else {
      $items = $form_state->getValue($item_parents) ?? [];
      foreach ($items as $item) {
        $column_name = (string) $item['name'];
        $columns[$column_name] = $item;
        unset($columns[$column_name]['remove']);
      }
    }

    $form_state->setValue([...$parents, 'columns'], $columns);
    $form_state->setValue([...$parents, 'items'], NULL);

    // Reset the field storage config property - it will be recalculated when
    // accessed via the property definitions getter.
    // @see Drupal\field\Entity\FieldStorageConfig::getPropertyDefinitions()
    // If we don't do this, an exception is thrown during the table update that
    // is very difficult to recover from since the original field tables have
    // already been removed at that point.
    $field_storage_config = $form_state->getBuildInfo()['callback_object']->getEntity();
    $field_storage_config->set('propertyDefinitions', NULL);
  }

  /**
   * Checks if machine name already exists for field.
   *
   * @param string $value
   *   The value to compare.
   * @param array<string, mixed> $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return bool
   *   Returns TRUE if field exists, otherwise FALSE.
   */
  public function machineNameExists(string $value, array $form, FormStateInterface $form_state): bool {
    $count = 0;
    $parents = ['field_storage', 'subform', 'settings', 'items'];
    $settings = $form_state->getValue($parents) ?? [];
    foreach ($settings as $item) {
      if ($item['name'] == $value) {
        $count++;
      }
    }

    return $count > 1;
  }

  /**
   * {@inheritdoc}
   */
  public function isEmpty(): bool {
    $settings = $this->getSettings();
    $custom_items = $this->getCustomFieldManager()->getCustomFieldItems($settings);
    $emptyCounter = 0;
    $field_count = count($custom_items);
    foreach ($custom_items as $name => $custom_item) {
      $definition = $custom_item->getPluginDefinition();
      $check = $custom_item->checkEmpty();
      $no_check = is_array($definition) && array_key_exists('never_check_empty', $definition) && $definition['never_check_empty'];
      $item_value = $this->get($name)->getValue();
      if ($item_value === '' || ($item_value === NULL && !$no_check)) {
        $emptyCounter++;
        // If any of the empty check fields are filled or all fields are empty.
        if ($check || $emptyCounter === $field_count) {
          return TRUE;
        }
      }
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function onChange($property_name, $notify = TRUE): void {
    $settings = $this->getSettings();
    $custom_items = $this->getCustomFieldManager()->getCustomFieldItems($settings);
    foreach ($custom_items as $name => $custom_item) {
      if ($property_name === $name) {
        $custom_item->onChange($property_name, $notify, $this);
      }
    }
    parent::onChange($property_name, $notify);
  }

  /**
   * Callback for both ajax-enabled buttons in storage form.
   *
   * Selects and returns the fieldset with the names in it.
   *
   * @param array $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The ajax response.
   */
  public function actionCallback(array &$form, FormStateInterface $form_state): AjaxResponse {
    $trigger = $form_state->getTriggeringElement();
    if (isset($trigger['#delta'])) {
      $field = $trigger['#delta'];
      $field_settings = $form_state->getValue(['settings', 'field_settings']);
      if (!empty($field_settings[$field])) {
        unset($field_settings[$field]);
        $form_state->setValue(['settings', 'field_settings'], $field_settings);
      }
    }

    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand('#field-combined', $form));
    if ($trigger['#name'] === 'custom_field_add_item') {
      $sliced_parents = array_slice($trigger['#parents'], 0, -2);
      $items = Element::children(NestedArray::getValue($form, $sliced_parents)['items']);
      $last_item = end($items);
      $input = 'field_storage[subform][settings][items][' . $last_item . '][name]';
      // Set the focus to the name field for the last item added.
      $response->addCommand(new InvokeCommand('input[name="' . $input . '"]', 'focus'));
    }

    return $response;
  }

  /**
   * Submit handler for the "Add another" button.
   *
   * Triggers form state notice to add item and causes a form rebuild.
   *
   * @param array<string, mixed> $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public static function addSubmit(array &$form, FormStateInterface $form_state): void {
    $storage = $form_state->getValue('field_storage');
    $default_name = uniqid('value_');
    $default = static::defaultStorageSettings()['columns']['value'];
    unset($default['name']);
    $storage['subform']['settings']['items'][$default_name] = [
      'name' => $default_name,
    ] + $default;
    $form_state->setValue('field_storage', $storage);
    $form_state->setRebuild();
  }

  /**
   * Submit handler for the "Remove" button.
   *
   * Triggers form state notice to remove item and causes a form rebuild.
   *
   * @param array<string, mixed> $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public static function removeSubmit(array &$form, FormStateInterface $form_state): void {
    $remove = $form_state->getTriggeringElement()['#delta'];
    $settings = $form_state->getValue('settings');
    $storage = $form_state->getValue('field_storage');
    unset($storage['subform']['settings']['items'][$remove]);
    // Remove the field setting if it exists.
    if (isset($settings['field_settings'][$remove])) {
      unset($settings['field_settings'][$remove]);
      $form_state->setValue('settings', $settings);
    }
    $form_state->setValue('field_storage', $storage);
    $form_state->setRebuild();
  }

  /**
   * Get the existing custom field storage config options.
   *
   * @param string $entity_type_id
   *   The entity type to match for exclusion.
   *
   * @return array<string, mixed>
   *   An array of existing field configurations.
   */
  protected function getExistingCustomFieldStorageOptions(string $entity_type_id): array {
    $sources = [];
    $existingCustomFields = \Drupal::service('entity_field.manager')->getFieldMapByFieldType('custom');
    $existing_field_name = $this->getFieldDefinition()->getName();
    if (!empty($existingCustomFields)) {
      foreach ($existingCustomFields as $entity_type => $fields) {
        $bundleInfo = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type);
        foreach ($fields as $field_name => $info) {
          if ($entity_type === $entity_type_id && $existing_field_name == $field_name) {
            continue;
          }
          foreach ($info['bundles'] as $bundle) {
            $group = $bundleInfo[$bundle]['label'] . ' (' . $entity_type . ')';
            if ($info = FieldConfig::loadByName($entity_type, $bundle, $field_name)) {
              $sources[$group][$entity_type . '.' . $bundle . '.' . $info->getName()] = $info->getLabel();
            }
          }
        }
      }
    }
    return $sources;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, mixed>
   *   The default field settings.
   */
  public static function defaultFieldSettings(): array {
    return [
      'field_settings' => [],
      'add_more_label' => '',
    ] + parent::defaultFieldSettings();
  }

  /**
   * {@inheritdoc}
   *
   * @param array<string, mixed> $form
   *   The form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array<string, mixed>
   *   The field settings form.
   */
  public function fieldSettingsForm(array $form, FormStateInterface $form_state): array {
    $wrapper_id = 'custom-field-settings-wrapper';
    $is_cloning = FALSE;

    if ($form_state->isRebuilding()) {
      $is_cloning = !empty($form_state->getValue(['field_storage', 'subform', 'settings', 'clone']));
    }

    $element['add_more_label'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Add another button label'),
      '#description' => $this->t('The add button label for multiple items. Leave empty for default button text.'),
      '#weight' => -100,
      '#default_value' => $this->getSetting('add_more_label'),
      '#attributes' => [
        'disabled' => !$this->getFieldDefinition()->getFieldStorageDefinition()->isMultiple(),
      ],
    ];

    $element['field_settings'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Field settings'),
      '#weight' => -99,
      '#prefix' => '<div id="' . $wrapper_id . '">',
      '#suffix' => '</div>',
      '#access' => !$is_cloning,
    ];

    $current_settings = $form_state->get('current_settings') ?? $this->getSettings();
    $custom_items = $this->getCustomFieldManager()->getCustomFieldItems($current_settings);

    // Build the table rows and columns.
    foreach ($custom_items as $name => $custom_item) {
      $plugin_id = $custom_item->getPluginId();

      // UUid fields have no configuration.
      if ($plugin_id === 'uuid') {
        continue;
      }

      $element['field_settings'][$name] = [
        '#type' => 'details',
        '#title' => $this->t('@name', ['@name' => $name]),
      ];

      // Add our plugin field settings form.
      $element['field_settings'][$name] += $custom_item->fieldSettingsForm($form, $form_state);

      if ($custom_item->getSetting('never_check_empty')) {
        // Hide the check empty element for irrelevant types.
        $element['field_settings'][$name]['check_empty']['#type'] = 'value';
        $element['field_settings'][$name]['check_empty']['#value'] = FALSE;
      }
    }

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public static function calculateDependencies(FieldDefinitionInterface $field_definition): array {
    $dependencies = parent::calculateDependencies($field_definition);
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeManager $plugin_service */
    $plugin_service = \Drupal::service('plugin.manager.custom_field_type');
    $custom_items = $plugin_service->getCustomFieldItems($field_definition->getSettings());
    $default_value = $field_definition->getDefaultValueLiteral();

    foreach ($custom_items as $custom_item) {
      $plugin_dependencies = $custom_item->calculateDependencies($custom_item, $default_value);
      $dependencies = array_merge_recursive($dependencies, $plugin_dependencies);
    }

    return $dependencies;
  }

  /**
   * {@inheritdoc}
   */
  public static function calculateStorageDependencies(FieldStorageDefinitionInterface $field_definition): array {
    $dependencies = parent::calculateStorageDependencies($field_definition);
    $entity_type_manager = \Drupal::entityTypeManager();
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeManager $plugin_service */
    $plugin_service = \Drupal::service('plugin.manager.custom_field_type');
    $custom_items = $plugin_service->getCustomFieldItems($field_definition->getSettings());
    foreach ($custom_items as $custom_item) {
      if ($custom_item->getTargetType() !== NULL) {
        $target_entity_type_id = $custom_item->getTargetType();
        try {
          $target_entity_type = $entity_type_manager->getDefinition($target_entity_type_id);
          $plugin_dependencies['module'][] = $target_entity_type->getProvider();
          $dependencies = array_merge_recursive($dependencies, $plugin_dependencies);
        }
        catch (PluginNotFoundException $e) {
          // Plugin not found.
        }
      }
    }

    return $dependencies;
  }

  /**
   * {@inheritdoc}
   */
  public static function onDependencyRemoval(FieldDefinitionInterface $field_definition, array $dependencies): bool {
    $changed = parent::onDependencyRemoval($field_definition, $dependencies);
    $settings = $field_definition->getSettings();
    $columns = $settings['columns'];
    $field_settings = $settings['field_settings'];
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeManager $plugin_service */
    $plugin_service = \Drupal::service('plugin.manager.custom_field_type');
    /** @var \Drupal\custom_field\Plugin\CustomFieldTypeInterface[] $custom_items */
    $custom_items = $plugin_service->getCustomFieldItems($settings);
    $settings_changed = FALSE;

    // Try to update the default value config dependency, if possible.
    if ($default_value = $field_definition->getDefaultValueLiteral()) {
      $entity_type_manager = \Drupal::entityTypeManager();
      foreach ($default_value as $key => $value) {
        foreach ($value as $column_key => $column_value) {
          if (isset($columns[$column_key])) {
            $column = $columns[(string) $column_key];
            if (isset($column['target_type']) && !empty($column_value)) {
              $entity = $entity_type_manager->getStorage($column['target_type'])
                ->load($column_value);
              if ($entity && isset($dependencies[$entity->getConfigDependencyKey()][$entity->getConfigDependencyName()])) {
                $default_value[$key][$column_key] = NULL;
                $changed = TRUE;
              }
            }
          }
        }
      }
    }
    if ($changed && $field_definition instanceof FieldConfig) {
      $field_definition->setDefaultValue($default_value);
    }

    foreach ($custom_items as $name => $custom_item) {
      $widget_settings = $custom_item->onDependencyRemoval($custom_item, $dependencies);
      if (!empty($widget_settings)) {
        $field_settings[$name]['widget_settings']['settings'] = $widget_settings;
        $settings_changed = TRUE;
      }
    }

    if ($settings_changed && $field_definition instanceof FieldConfig) {
      $field_definition->setSetting('field_settings', $field_settings);
    }

    $changed |= $settings_changed;

    return (bool) $changed;
  }

  /**
   * Get the custom field_type manager plugin.
   *
   * @return \Drupal\custom_field\Plugin\CustomFieldTypeManagerInterface
   *   Returns the 'custom' field type plugin manager.
   */
  public function getCustomFieldManager(): CustomFieldTypeManagerInterface {
    return \Drupal::service('plugin.manager.custom_field_type');
  }

}

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

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