bulk_edit_terms-8.x-1.1/src/Form/NodeSelectTerms.php

src/Form/NodeSelectTerms.php
<?php

declare(strict_types=1);

namespace Drupal\bulk_edit_terms\Form;

use Drupal\bulk_edit_terms\UpdateAction;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Entity\EntityTypeManager;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use Drupal\Core\Url;
use Drupal\node\NodeInterface;
use Drupal\taxonomy\TermStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Response;

/**
 * Provides a form for bulk editing term assignments on nodes.
 */
class NodeSelectTerms extends ConfirmFormBase {

  /**
   * The array of nodes to edit terms from.
   *
   * @var array
   */
  protected array $nodes = [];

  public function __construct(
    protected PrivateTempStoreFactory $tempStoreFactory,
    protected EntityTypeManager $entityTypeManager,
    protected AccountInterface $currentUser,
    protected TimeInterface $time,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get('tempstore.private'),
      $container->get('entity_type.manager'),
      $container->get('current_user'),
      $container->get('datetime.time'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getFormId(): string {
    return 'node_select_terms';
  }

  /**
   * {@inheritdoc}
   */
  public function getQuestion(): TranslatableMarkup {
    return $this->t('Which taxonomy term assignments do you want to update?');
  }

  /**
   * {@inheritdoc}
   */
  public function getCancelUrl(): Url {
    return new Url('system.admin_content');
  }

  /**
   * {@inheritdoc}
   */
  public function getConfirmText(): TranslatableMarkup {
    return $this->t('Update terms');
  }

  /**
   * {@inheritdoc}
   */
  public function buildForm(array $form, FormStateInterface $form_state): array|Response {
    $node_ids = $this->tempStoreFactory->get('node_edit_terms')->get($this->currentUser->id());
    if (empty($node_ids)) {
      return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString());
    }

    $this->nodes = $this->loadNodes($node_ids);

    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    assert($term_storage instanceof TermStorageInterface);

    // Gather all taxonomy fields from the nodes and offer the options available
    // to select.
    $term_reference_fields = [];
    foreach ($this->nodes as $node) {
      assert($node instanceof NodeInterface);
      $fields = $node->getFieldDefinitions();
      foreach ($fields as $name => $field) {
        assert($field instanceof FieldDefinitionInterface);

        if ($field->getType() !== 'entity_reference' || !str_starts_with($name, 'field_')) {
          continue;
        }

        $field_settings = $field->getSettings();
        if ($field_settings['target_type'] !== 'taxonomy_term') {
          continue;
        }

        if (!$node->get($name)->access('edit', $this->currentUser, FALSE)) {
          continue;
        }

        if (!isset($term_reference_fields[$name])) {
          $term_reference_fields[$name] = [
            'used_on_bundles' => [],
            'field_instance' => $field,
          ];
        }
        if (!in_array($node->bundle(), $term_reference_fields[$name]['used_on_bundles'])) {
          $term_reference_fields[$name]['used_on_bundles'][] = $node->bundle();
        }
      }
    }

    $config = $this->config('bulk_edit_terms.settings');

    // Present a fieldset for each field to allow user to specify what action
    // to take.
    $content_type_manager = $this->entityTypeManager->getStorage('node_type');
    foreach ($term_reference_fields as $name => $data) {
      // Convert bundle machine names to their proper labels.
      $bundles = $data['used_on_bundles'];
      array_walk($bundles, function (&$bundle) use ($content_type_manager) {
        $bundle = $content_type_manager->load($bundle)->label();
      });

      $field = $data['field_instance'];

      $field_info = $field->getFieldStorageDefinition();
      $field_settings = $field->getSettings();
      $target_bundles = $field_settings['handler_settings']['target_bundles'];

      $auto_create = $field->getSettings()['handler_settings']['auto_create'] ?? FALSE;

      $single_value_only = $field_info->getCardinality() === 1;

      $form["{$name}_group"] = [
        '#type' => 'fieldset',
        '#title' => $field->getLabel() . ' (' . $this->t('used on') . ' ' . implode(', ', $bundles) . ')',
      ];

      if ($single_value_only && $target_bundles && !$auto_create) {
        $form["{$name}_group"]["{$name}_action"] = [
          '#type' => 'radios',
          '#title' => $this->t('Action'),
          '#options' => [
            UpdateAction::None->value => $this->t('No changes'),
            UpdateAction::Replace->value => $this->t('Replace with selected term'),
            UpdateAction::Clear->value => $this->t('Clear existing term'),
          ],
          '#default_value' => UpdateAction::None->value,
          '#required' => TRUE,
        ];

        // Search terms and create select element.
        $form["{$name}_group"][$name] = [
          '#type' => 'select',
          '#title' => $this->t('Term'),
          '#options' => $this->getTermsAsSelectOptions($target_bundles),
          '#empty_option' => $this->t('- Select -'),
          '#states' => [
            'visible' => [
              'input[name="' . $name . '_action"]' => ['value' => UpdateAction::Replace->value],
            ],
          ],
        ];
      }
      else {
        $form["{$name}_group"]["{$name}_action"] = [
          '#type' => 'radios',
          '#title' => $this->t('Action'),
          '#options' => [
            UpdateAction::None->value => $this->t('No changes'),
            UpdateAction::Replace->value => $this->t('Replace all existing term(s) with selected term(s)'),
            UpdateAction::Clear->value => $this->t('Clear all existing term(s)'),
            UpdateAction::Append->value => $this->t('Add selected term(s)'),
            UpdateAction::Remove->value => $this->t('Remove selected term(s)'),
          ],
          '#default_value' => UpdateAction::None->value,
          '#required' => TRUE,
        ];

        $states = [
          'invisible' => [
            [
              'input[name="' . $name . '_action"]' => ['value' => UpdateAction::Clear->value],
            ],
            [
              'input[name="' . $name . '_action"]' => ['value' => UpdateAction::None->value],
            ],
          ],
        ];
        if ($config->get('multi_value_widget_type') === 'entity_autocomplete') {
          $form["{$name}_group"][$name] = [
            '#type' => 'entity_autocomplete',
            '#target_type' => $field_settings['target_type'],
            '#title' => $this->t('Term(s)'),
            '#default_value' => FALSE,
            '#tags' => TRUE,
            '#selection_settings' => [
              'target_bundles' => $target_bundles,
            ],
            '#states' => $states,
          ];

          if ($auto_create) {
            $auto_create_bundle = $field_settings['handler_settings']['auto_create_bundle'] ?? NULL;
            if ($auto_create_bundle) {
              $form[$name]['#autocreate'] = [
                'bundle' => $auto_create_bundle,
              ];
            }
          }
        }
        else {
          $form["{$name}_group"][$name] = [
            '#type' => 'select',
            '#title' => $this->t('Term(s)'),
            '#options' => $this->getTermsAsSelectOptions($target_bundles),
            '#multiple' => TRUE,
            '#states' => $states,
          ];
        }
      }
    }

    // Back out now if we don't have any term reference fields that can be
    // modified.
    if (empty($term_reference_fields)) {
      $form['warning'] = [
        '#prefix' => '<p>',
        '#suffix' => '</p>',
        '#markup' => $this->t('No term reference fields were found in the selected nodes.'),
      ];
      return $form;
    }

    $form['intro'] = [
      '#prefix' => '<p>',
      '#suffix' => '</p>',
      '#markup' => $this->t('The following fields appear in the content types of at least one of the selected items.'),
      '#weight' => -1,
    ];

    $form['create_revision'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Create new revision for each update'),
      '#default_value' => TRUE,
    ];

    $form['revision_log_message'] = [
      '#type' => 'textarea',
      '#title' => $this->t('Revision log message'),
      '#rows' => 4,
      '#states' => [
        'visible' => [
          'input[name="create_revision"]' => ['checked' => TRUE],
        ],
      ],
    ];

    // Store list of field names we presented on the form so we can access
    // them in the form submit.
    $form_state->set('field_names', array_keys($term_reference_fields));

    return parent::buildForm($form, $form_state);
  }

  /**
   * Get list of terms in the given vids suitable for select form options.
   *
   * @param array $vids
   *   The list of taxonomy vocabularies to include.
   *
   * @return array
   *   The term options.
   */
  protected function getTermsAsSelectOptions(array $vids): array {
    $options = [];
    $term_storage = $this->entityTypeManager->getStorage('taxonomy_term');
    $vocab_storage = $this->entityTypeManager->getStorage('taxonomy_vocabulary');
    assert($term_storage instanceof TermStorageInterface);
    foreach ($vids as $vid) {
      $vocab = $vocab_storage->load($vid);
      $tree = $term_storage->loadTree($vid);
      if (!empty($tree)) {
        foreach ($tree as $item) {
          $label = str_repeat('-', $item->depth) . $item->name;
          if (count($vids) > 1) {
            // Group terms by their vocabulary.
            $options[$vocab->label()][$item->tid] = $label;
          }
          else {
            $options[$item->tid] = $label;
          }
        }
      }
    }
    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    foreach ($form_state->get('field_names') as $fieldName) {
      $field_value = $form_state->getValue($fieldName);
      $field_action = $form_state->getValue($fieldName . '_action');

      if (empty($field_action) && !empty($field_value)) {
        $form_state->setErrorByName($fieldName, $this->t('Select an action for each field you provided a value for.'));
      }
      $allowedEmptyActions = [
        UpdateAction::None->value,
        UpdateAction::Clear->value,
      ];
      if (empty($field_value) && !empty($field_action) && !in_array($field_action, $allowedEmptyActions)) {
        $form_state->setErrorByName($fieldName, $this->t('You must provide a value for each field.'));
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    if ($form_state->getValue('confirm') && !empty($this->nodes)) {
      $updated_nodes_count = 0;
      foreach ($this->nodes as $node) {
        assert($node instanceof NodeInterface);
        $changes_made_to_node = FALSE;
        foreach ($form_state->get('field_names') as $fieldName) {
          // Skip if this node doesn't have this field.
          $field = $node->getFieldDefinition($fieldName);
          if (!$field instanceof FieldDefinitionInterface) {
            continue;
          }

          // Skip if there's no action to take for this field.
          $field_value = $form_state->getValue($fieldName);
          $field_action = $form_state->getValue($fieldName . '_action');
          if (empty($field_action)) {
            continue;
          }

          // Skip if user has no access to update the field on this node.
          if (!$node->get($fieldName)->access('edit', $this->currentUser, FALSE)) {
            continue;
          }

          // The field value may be an array of target_ids if the form widget
          // was an entity autocomplete. Normalize to just a list of term
          // IDs if so.
          $submitted_tids = [];
          if (is_array($field_value)) {
            foreach ($field_value as $val) {
              $submitted_tids[] = (int) ($val['target_id'] ?? $val);
            }
          }
          else {
            $field_value = (int) $field_value;
          }
          sort($submitted_tids);

          $existing_tids = array_map(fn($val) => (int) $val['target_id'], $node->get($fieldName)->getValue());
          sort($existing_tids);

          $changes_made_to_field_value = FALSE;
          $value_to_set = NULL;
          $field_action = UpdateAction::from($field_action);
          switch ($field_action) {
            case UpdateAction::Clear:
              if (!$node->get($fieldName)->isEmpty()) {
                $value_to_set = [];
                $changes_made_to_field_value = TRUE;
              }
              break;

            case UpdateAction::Replace:
              // Only take action if there was no existing value or the existing
              // value doesn't match the new value.
              if ($node->get($fieldName)->isEmpty() || $existing_tids != $submitted_tids) {
                $value_to_set = $field_value;
                $changes_made_to_field_value = TRUE;
              }
              break;

            case UpdateAction::Remove:
              $value_to_set = array_diff($existing_tids, $submitted_tids);
              if ($value_to_set !== $existing_tids) {
                $changes_made_to_field_value = TRUE;
              }
              break;

            case UpdateAction::Append:
              $value_to_set = array_merge($existing_tids, $submitted_tids);
              $value_to_set = array_unique($value_to_set);
              if ($value_to_set !== $existing_tids) {
                $changes_made_to_field_value = TRUE;
              }
              break;

            default:
              break;
          }

          if ($changes_made_to_field_value) {
            $node->set($fieldName, $value_to_set);
            $changes_made_to_node = TRUE;
          }
        }

        if ($changes_made_to_node) {
          if ($form_state->getValue('create_revision')) {
            $msg = $form_state->getValue('revision_log_message');
            if (!empty($msg)) {
              $node->set('revision_log', $msg);
            }
            $node->setNewRevision();
            $node->setRevisionUserId($this->currentUser->id());
            $node->setRevisionCreationTime($this->time->getCurrentTime());
          }
          $node->save();
          $updated_nodes_count++;
        }
      }

      $this->messenger()->addStatus($this->formatPlural($updated_nodes_count, '1 node was updated.', '@count nodes were updated.'));
    }
  }

  /**
   * Load latest revisions of the provided node ids.
   *
   * It's important we load the latest version instead of the default revision.
   * Sites using content moderation may have forward drafts of a node which
   * should be edited, not the default/published version. Basically, we want
   * to mimic what would be edited had the user edited the node directly via
   * the node form.
   *
   * @param array $nids
   *   The node ids.
   *
   * @return array
   *   The loaded nodes.
   */
  protected function loadNodes(array $nids): array {
    $node_storage = $this->entityTypeManager->getStorage('node');
    $nodes = [];
    foreach ($nids as $nid) {
      $latest_vid = $node_storage->getLatestRevisionId($nid);
      $nodes[] = $node_storage->loadRevision($latest_vid);
    }
    return $nodes;
  }

}

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

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