Commit 0ce91738 authored by mxh's avatar mxh Committed by Jürgen Haas
Browse files

Issue #3261414 by mxh, jurgenhaas: Add or remove a value or values from a multi-value field

parent 0035a2ac
Loading
Loading
Loading
Loading
+209 −17
Original line number Diff line number Diff line
@@ -5,10 +5,15 @@ namespace Drupal\eca_content\Plugin\Action;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Field\EntityReferenceFieldItemListInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\TypedData\ListInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\eca\Plugin\Action\ActionBase;
use Drupal\eca\Service\Conditions;
use Drupal\eca\TypedData\PropertyPathTrait;
use Drupal\field\FieldStorageConfigInterface;
use Drupal\user\Entity\User;

/**
@@ -41,6 +46,7 @@ abstract class FieldUpdateActionBase extends ActionBase {
      'save_entity' => FALSE,
      'strip_tags' => FALSE,
      'trim' => FALSE,
      'method' => 'set:clear',
    ] + parent::defaultConfiguration();
  }

@@ -72,34 +78,220 @@ abstract class FieldUpdateActionBase extends ActionBase {
      $user = User::load($this->currentUser->id());
      $this->tokenServices->addTokenData($this->tokenServices->getTokenType($user), $user);
    }
    $method_settings = explode(':', $this->configuration['method'] ?? $this->defaultConfiguration()['method']);
    $all_entities_to_save = [];
    $options = ['auto_append' => TRUE, 'access' => 'update'];
    foreach ($this->getFieldsToUpdate() as $field => $value) {
    $entity_adapter = EntityAdapter::createFromEntity($entity);
    $values_changed = FALSE;
    foreach ($this->getFieldsToUpdate() as $field => $values) {
      $metadata = [];
      if (!($update_target = $this->getTypedProperty($entity_adapter, $field, $options, $metadata))) {
        throw new \InvalidArgumentException(sprintf("The provided field %s does not exist as a property path on the %s entity having ID %s.", $field, $entity->getEntityTypeId(), $entity->id()));
      }
      if (empty($metadata['entities'])) {
        throw new \RuntimeException(sprintf("The provided field %s does not resolve for entities to be saved from the %s entity having ID %s.", $field, $entity->getEntityTypeId(), $entity->id()));
      }
      $property_name = $update_target->getName();
      while ($update_target = $update_target->getParent()) {
        if ($update_target instanceof FieldItemListInterface) {
          break;
        }
      }
      if (!($update_target instanceof FieldItemListInterface)) {
        throw new \InvalidArgumentException(sprintf("The provided field %s does not resolve to a field on the %s entity having ID %s.", $field, $entity->getEntityTypeId(), $entity->id()));
      }
      if ($values instanceof ListInterface) {
        $values = $values->getValue();
      }
      elseif (!is_array($values)) {
        $values = [$values];
      }

      // Apply configured filters and normalize the array of values.
      foreach ($values as $i => $value) {
        if ($value instanceof TypedDataInterface) {
          $value = $value->getValue();
        }
        if (is_array($value)) {
          $value = array_key_exists($property_name, $value) ? $value[$property_name] : reset($value);
        }
        if (is_scalar($value) || is_null($value)) {
          if ($this->configuration['strip_tags'] === Conditions::OPTION_YES) {
        $value = preg_replace('/[\t\n\r\0\x0B]/', '', strip_tags($value));
            $value = preg_replace('/[\t\n\r\0\x0B]/', '', strip_tags((string) $value));
          }
          if ($this->configuration['trim'] === Conditions::OPTION_YES) {
        $value = trim($value);
            $value = trim((string) $value);
          }
          if ($value === '' || $value === NULL) {
            unset($values[$i]);
          }
          else {
            $values[$i] = [$property_name => $value];
          }
        }
        else {
          $values[$i] = $value;
        }
      }

      // Create a map of indices that refer to the already existing counterpart.
      $existing = [];
      /** @var \Drupal\Core\Field\FieldItemListInterface $update_target */
      $current_values = ($update_target instanceof EntityReferenceFieldItemListInterface) && (reset($values) instanceof EntityInterface) ? $update_target->referencedEntities() : $update_target->filterEmptyItems()->getValue();

      if (empty($values) && !empty($current_values) && ($this->configuration['method'] === 'set:clear')) {
        // Shorthand for setting a field to be empty.
        $current_values = [];
        $values_changed = TRUE;
        continue;
      }

      foreach ($current_values as $k => $current_item) {
        $current_value = !is_array($current_item) ? $current_item : (array_key_exists($property_name, $current_item) ? $current_item[$property_name] : reset($current_item));
        if (is_string($current_value)) {
          // Extra processing is needed for strings, in order to prevent false
          // comparison when dealing with values that are the same but
          // encoded differently.
          $current_value = nl2br(trim($current_value));
        }
        elseif ($current_value instanceof EntityInterface) {
          $current_value = $current_value->uuid() ?: $current_value;
        }

        foreach ($values as $i => $value) {
          $new_value = !is_array($value) ? $value : (array_key_exists($property_name, $value) ? $value[$property_name] : reset($value));
          if (is_string($new_value)) {
            $new_value = nl2br(trim($new_value));
          }
          elseif ($new_value instanceof EntityInterface) {
            $new_value = $new_value->uuid() ?: $new_value;
          }
          if ((is_object($new_value) && $current_value === $new_value) || $current_value == $new_value) {
            $existing[$i] = $k;
          }
        }
      }

      if ((reset($method_settings) !== 'remove') && (count($existing) === count($values))) {
        continue;
      }

      $cardinality = $update_target->getFieldDefinition()->getFieldStorageDefinition()->getCardinality();
      $is_unlimited = $cardinality === FieldStorageConfigInterface::CARDINALITY_UNLIMITED;
      foreach ($method_settings as $method_setting) {
        switch ($method_setting) {

          case 'clear':
            $keep = [];
            foreach ($existing as $k) {
              $keep[] = $current_values[$k];
            }
            if (count($current_values) !== count($keep)) {
              $values_changed = TRUE;
            }
            $current_values = $keep;
            break;

          case 'empty':
            if (!empty($current_values)) {
              continue 2;
            }
            break;

          case 'not_full':
            if (!$is_unlimited && !(count($current_values) < $cardinality)) {
              continue 2;
            }
            break;

          case 'drop_first':
            if (!$is_unlimited) {
              $num_required = count($values) - ($cardinality - count($current_values));
              $keep = array_flip($existing);
              reset($current_values);
              while ($num_required > 0 && ($k = key($current_values)) !== NULL) {
                next($current_values);
                $num_required--;
                if (!isset($keep[$k])) {
                  unset($current_values[$k]);
                  $values_changed = TRUE;
                }
      $metadata = [];
      if ($update_target = $this->getTypedProperty(EntityAdapter::createFromEntity($entity), $field, $options, $metadata)) {
        // Try to set the value. If that attempt fails, then it would throw an
        // exception, and the exception would be logged as an error.
        $update_target->setValue($value);
        if (empty($metadata['entities'])) {
          throw new \LogicException(sprintf("The provided field %s does not resolve for entities to be saved from the %s entity having ID %s.", $field, $entity->getEntityTypeId(), $entity->id()));
              }
            }
            break;

            case 'drop_last':
              if (!$is_unlimited) {
                $num_required = count($values) - ($cardinality - count($current_values));
                $keep = array_flip($existing);
                end($current_values);
                while ($num_required > 0 && ($k = key($current_values)) !== NULL) {
                  prev($current_values);
                  $num_required--;
                  if (!isset($keep[$k])) {
                    unset($current_values[$k]);
                    $values_changed = TRUE;
                  }
                }
              }
              break;

        }
      }

      foreach ($method_settings as $method_setting) {
        switch ($method_setting) {

          case 'append':
          case 'set':
            $current_num = count($current_values);
            foreach ($values as $i => $value) {
              if (!$is_unlimited && $cardinality <= $current_num) {
                break;
              }
              if (!isset($existing[$i])) {
                $current_values[] = $value;
                $current_num++;
                $values_changed = TRUE;
              }
            }
            break;

          case 'prepend':
            $current_num = count($current_values);
            foreach (array_reverse($values, TRUE) as $i => $value) {
              if (!$is_unlimited && $cardinality <= $current_num) {
                break;
              }
              if (!isset($existing[$i])) {
                array_unshift($current_values, $value);
                $current_num++;
                $values_changed = TRUE;
              }
            }
            break;

          case 'remove':
            foreach ($existing as $k) {
              unset($current_values[$k]);
              $values_changed = TRUE;
            }
            break;

        }
      }

      if ($values_changed) {
        // Try to set the values. If that attempt fails, then it would throw an
        // exception, and the exception would be logged as an error.
        $update_target->setValue(array_values($current_values));
        $update_target->filterEmptyItems();
        foreach ($metadata['entities'] as $entity_to_save) {
          if (!in_array($entity_to_save, $all_entities_to_save, TRUE)) {
            $all_entities_to_save[] = $entity_to_save;
          }
        }
      }
      else {
        // The property path does not exist, thus the provided field is not a
        // valid argument.
        throw new \InvalidArgumentException(sprintf("The provided field %s does not exist as a property path on the %s entity having ID %s.", $field, $entity->getEntityTypeId(), $entity->id()));
      }
    }
    foreach ($all_entities_to_save as $to_save) {
      $this->save($to_save);
+52 −4
Original line number Diff line number Diff line
@@ -3,6 +3,8 @@
namespace Drupal\eca_content\Plugin\Action;

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\eca\Plugin\OptionsInterface;

/**
 * Set the value of an entity field.
@@ -13,15 +15,30 @@ use Drupal\Core\Form\FormStateInterface;
 *   type = "entity"
 * )
 */
class SetFieldValue extends ConfigurableFieldUpdateActionBase {
class SetFieldValue extends ConfigurableFieldUpdateActionBase implements OptionsInterface {

  /**
   * {@inheritdoc}
   */
  protected function getFieldsToUpdate() {
    return [
      $this->tokenServices->replaceClear($this->configuration['field_name']) => $this->tokenServices->replaceClear($this->configuration['field_value']),
    ];
    $name = $this->tokenServices->replaceClear($this->configuration['field_name']);

    // Process the field values.
    $values = $this->configuration['field_value'];
    $use_token_replace = TRUE;
    // When the given input is not too large, check whether it wants to directly
    // use defined data.
    if ((strlen($values) <= 255) && ($data = $this->tokenServices->getTokenData($values))) {
      if (!($data instanceof TypedDataInterface) || !empty($data->getValue())) {
        $use_token_replace = FALSE;
        $values = $data;
      }
    }
    if ($use_token_replace) {
      $values = $this->tokenServices->replaceClear($values);
    }

    return [$name => $values];
  }

  /**
@@ -48,6 +65,12 @@ class SetFieldValue extends ConfigurableFieldUpdateActionBase {
      '#title' => $this->t('Field value'),
      '#default_value' => $this->configuration['field_value'],
    ];
    $form['method'] = [
      '#type' => 'select',
      '#title' => $this->t('Method'),
      '#options' => $this->getOptions('method'),
      '#default_value' => $this->configuration['method'],
    ];
    return $form;
  }

@@ -59,4 +82,29 @@ class SetFieldValue extends ConfigurableFieldUpdateActionBase {
    $this->configuration['field_value'] = $form_state->getValue('field_value');
  }

  /**
   * {@inheritdoc}
   */
  public function getOptions(string $id): ?array {
    switch ($id) {

      case 'method':
        return [
          'set:clear' => $this->t('Set and clear previous value'),
          'set:empty' => $this->t('Set only when empty'),
          'append:not_full' => $this->t('Append when not full yet'),
          'append:drop_first' => $this->t('Append and drop first when full'),
          'append:drop_last' => $this->t('Append and drop last when full'),
          'prepend:not_full' => $this->t('Prepend when not full yet'),
          'prepend:drop_first' => $this->t('Prepend and drop first when full'),
          'prepend:drop_last' => $this->t('Prepend and drop last when full'),
          'remove' => $this->t('Remove value instead of adding it'),
        ];

      default:
        return [];

    }
  }

}
+4 −0
Original line number Diff line number Diff line
@@ -363,6 +363,10 @@ trait TokenDecoratorTrait {
   */
  protected function normalizeKey(string $key): string {
    $key = mb_strtolower(trim($key));
    if (strpos($key, '.')) {
      // User input may use "." instead of ":".
      $key = str_replace('.', ':', $key);
    }
    if (substr($key, 0, 1) === '[') {
      // Using Token brackets is not officially supported, yet we still try to
      // handle the case a user accidentally submitted a key with brackets.