contacts_events-8.x-1.x-dev/modules/teams/src/Form/TicketFormAlter.php

modules/teams/src/Form/TicketFormAlter.php
<?php

namespace Drupal\contacts_events_teams\Form;

use Drupal\commerce_order\Entity\OrderItemInterface;
use Drupal\contacts_events\Entity\Ticket;
use Drupal\contacts_events\OrderEventTransitionTrait;
use Drupal\contacts_events\OrderStateTrait;
use Drupal\contacts_events_teams\Entity\TeamApplication;
use Drupal\contacts_events_teams\TeamQueries;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\state_machine\Event\WorkflowTransitionEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

/**
 * Form alter for the ticket form.
 */
class TicketFormAlter implements TrustedCallbackInterface {

  use OrderEventTransitionTrait;
  use OrderStateTrait;

  /**
   * Entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Current User.
   *
   * @var \Drupal\Core\Session\AccountProxyInterface
   */
  protected $currentUser;

  /**
   * Current user.
   *
   * @var \Drupal\contacts_events_teams\TeamQueries
   */
  protected $queries;

  /**
   * Event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected $eventDispatcher;

  /**
   * TicketFormAlter constructor.
   *
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   Current user.
   * @param \Drupal\contacts_events_teams\TeamQueries $queries
   *   Queries.
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   Event dispatcher.
   */
  public function __construct(EntityTypeManagerInterface $entityTypeManager, AccountProxyInterface $currentUser, TeamQueries $queries, EventDispatcherInterface $dispatcher) {
    $this->entityTypeManager = $entityTypeManager;
    $this->currentUser = $currentUser;
    $this->queries = $queries;
    $this->eventDispatcher = $dispatcher;
  }

  /**
   * Implements hook_contacts_events_ticket_form_alter().
   *
   * Alter the ticket form to handle the team application process.
   */
  public function alter(array &$form, FormStateInterface $form_state, Ticket $ticket, string $display_mode) {
    // Simply hide the is team flag for the cancel form.
    if ($display_mode == 'cancel') {
      $form['is_team_ticket']['#access'] = FALSE;
      return;
    }

    // Don't do anything if the team/is_team_ticket elements don't exist.
    if (!isset($form['is_team_ticket']) || !($form['is_team_ticket']['#access'] ?? TRUE)) {
      return;
    }

    $form['is_team_ticket']['team_heading'] = [
      '#type' => 'html_tag',
      '#tag' => 'h3',
      '#weight' => $form['is_team_ticket']['widget']['#weight'] - 0.1,
      '#value' => new TranslatableMarkup('Teams'),
    ];

    $form['is_team_ticket']['widget']['value']['#title'] = new TranslatableMarkup('This person would like to join this event as a team volunteer');

    // Prevent unchecking of `is_team_ticket` if a team application exists.
    if ($this->queries->getTeamApplicationForTicket($ticket)) {
      $form['is_team_ticket']['widget']['value']['#default_value'] = TRUE;
      $form['is_team_ticket']['widget']['value']['#disabled'] = TRUE;
      $form['is_team_ticket']['widget']['value']['#description'] .= '<em>'
        . new TranslatableMarkup('This ticket cannot be reset back to pending until the application is archived.')
        . '</em>';
    }

    $is_team_ticket_element_name = $this->getTeamTicketElementName($form);

    $form['team_container'] = [
      '#type' => 'container',
      '#weight' => $form['is_team_ticket']['#weight'] + 0.1,
      '#states' => [
        'visible' => [
          [':input[name="' . $is_team_ticket_element_name . '"]' => ['checked' => TRUE]],
        ],
      ],
      'info' => [
        '#theme' => 'item_list',
        '#type' => 'ul',
        '#items' => [
          new TranslatableMarkup('The price for this ticket will be set to zero whilst the application is considered.'),
          new TranslatableMarkup('An email will be sent to this person about how to complete their application.'),
        ],
      ],
    ];

    // If we're on the back-end, add the option for staff to choose whether
    // the user needs to fill out an application form or if they should be
    // accepted immediately.
    if ($this->currentUser->hasPermission('can manage bookings for contacts_events')) {
      $form['team_container']['team_status_container'] = [
        '#type' => 'fieldset',
        '#title' => new TranslatableMarkup('Changing Team'),
        // Make sure these options appear below the team dropdown.
        // (Remember team dropdown is moved to inside the team_container in
        // self::preRender).
        '#weight' => $form['team']['#weight'] + 0.1,
        'info' => [
          '#type' => 'html_tag',
          '#tag' => 'em',
          '#value' => new TranslatableMarkup('These settings are only relevant if the team is changed.'),
        ],
        'team_status' => [
          '#type' => 'radios',
          '#title' => new TranslatableMarkup('Team Status'),
          '#default_value' => 'new_application',
          '#options' => [
            'new_application' => new TranslatableMarkup('This person needs to fill out a new application form'),
            'approve' => new TranslatableMarkup('This person will be immediately accepted on team'),
          ],
        ],
      ];
      $this->addSetupTeamApplicationSubmitHandler($form);

      // Notify managers that team tickets will be automatically confirmed.
      $form['team_container']['info']['#items'][] = [
        '#type' => 'html_tag',
        '#tag' => 'strong',
        '#value' => new TranslatableMarkup('Team tickets added by event managers will be automatically confirmed.'),
      ];

      // Hide confirmation wrapper as it is irrelevant.
      if (!empty($form['confirm_wrapper'])) {
        $form['confirm_wrapper']['#states']['visible'][] = [':input[name="' . $is_team_ticket_element_name . '"]' => ['checked' => FALSE]];
      }
    }

    // Register the team fields to trigger a price update.
    /** @var \Drupal\contacts_events\Element\AjaxUpdate $price_updater */
    $price_updater = $form['price_update']['#element'];
    $price_updater->registerElementToRespondTo($form['team']['widget']);
    // Must explicitly provide the parents array for is_team_ticket
    // as #parents is defined on the widget itself, but the actual element
    // is on the 'value' child element.
    $price_updater->registerElementToRespondTo($form['is_team_ticket']['widget']['value'], [], $form['is_team_ticket']['widget']['#parents']);

    // Register teams list to be updated when DOB changes.
    // Must have an explicit ID for ajax.
    $form['team']['#id'] = $price_updater->getIdWithUniqueSuffix('ticket-team-wrapper');
    $price_updater->registerElementToUpdate($form['team']);
    $form['team']['widget']['#title'] = new TranslatableMarkup('Choose your team');
    $form['team']['widget']['#description'] = new TranslatableMarkup('Please select the team for which the delegate will be applying.');

    $form['team']['widget']['#states']['required'] = [
      [':input[name="' . $is_team_ticket_element_name . '"]' => ['checked' => TRUE]],
    ];

    // Adjust email label and register for updates as it may fail validation.
    $form['email']['widget'][0]['value']['#description'] = new TranslatableMarkup('If this ticket holder is applying for a team place, please enter their own email address unique to them.');
    $form['email']['widget'][0]['value']['#states']['required'] = [
      [':input[name="' . $is_team_ticket_element_name . '"]' => ['checked' => TRUE]],
    ];
    $form['email']['#id'] = $price_updater->getIdWithUniqueSuffix('ticket-email-wrapper');
    $price_updater->registerElementToUpdate($form['email']);

    // Register the date of birth to update as it may fail validation.
    $form['date_of_birth']['#id'] = $price_updater->getIdWithUniqueSuffix('ticket-dob-wrapper');
    $price_updater->registerElementToUpdate($form['date_of_birth']);

    $form['#element_validate'][] = [$this, 'validate'];
    $form['#pre_render'][] = [$this, 'preRender'];
    $form['#entity_builders'][] = [$this, 'buildTicket'];
  }

  /**
   * Element validation callback for teams selection.
   *
   * @param array $form
   *   The ticket form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function validate(array $form, FormStateInterface $form_state) {
    $ticket_data = $form_state->getValue($form['#parents']);

    if (!$ticket_data['is_team_ticket']['value']) {
      return;
    }

    // Ensure that we have an email address.
    if (empty($ticket_data['email'][0]['value'])) {
      $form_state->setError($form['email']['widget'][0]['value'], new TranslatableMarkup('An email address is required to apply for team.'));
    }

    // Ensure there's a team selected when submitting.
    if (empty($ticket_data['team'])) {
      if ($form_state->getTriggeringElement()['#type'] != 'ajax_update') {
        $form_state->setError($form['team']['widget'], new TranslatableMarkup('Please select a team.'));
      }
    }

    // Ensure that we have a date of birth.
    // Need an explicit instanceof check here as if one or more of the DOB
    // components is missing, the value will be an array of blank components
    // rather than null.
    if (empty($ticket_data['date_of_birth'][0]['value']) || !($ticket_data['date_of_birth'][0]['value'] instanceof DrupalDateTime)) {
      $form_state->setError($form['date_of_birth']['widget'][0]['value'], new TranslatableMarkup('A date of birth is required to apply for team.'));
    }
    // Ensure we are above the minimum age for the event.
    else {
      $ticket_dob = strtotime($ticket_data['date_of_birth'][0]['value']);
      $element = &$form['date_of_birth']['widget'][0]['value'];
      /** @var \Drupal\contacts_events\Entity\EventInterface $event */
      $event = $form['#entity']->event->entity;
      $event_date = strtotime($event->date->value);
      $has_error = FALSE;

      // Check the event minimum age.
      $event_min_age = $event->getSetting('teams.min_age');
      if ($event_min_age) {
        $min_dob = strtotime("- {$event_min_age} years", $event_date);
        if ($min_dob <= $ticket_dob) {
          $has_error = TRUE;
          $form_state->setError($element, new TranslatableMarkup('You must be at least %min_age to apply for team.', [
            '%min_age' => $event_min_age,
          ]));
        }
      }

      // Check the team minimum age if we don't already have DOB errors.
      if (!$has_error && $ticket_data['team']) {
        $team = $this->entityTypeManager
          ->getStorage('c_events_team')
          ->load($ticket_data['team'][0]['target_id']);
        if ($team->age_min->value) {
          $min_dob = strtotime("- {$team->age_min->value} years", $event_date);
          if ($min_dob <= $ticket_dob) {
            $form_state->setError($element, new TranslatableMarkup('You must be at least %min_age to apply for %team.', [
              '%team' => $team->label(),
              '%min_age' => $team->age_min->value,
            ]));
          }
        }
      }
    }
  }

  /**
   * Moves the team element into the team container.
   *
   * @param array $form
   *   Complete form.
   *
   * @return array
   *   The form array.
   */
  public function preRender(array $form) {
    $form['team_container']['team'] = $form['team'];
    unset($form['team']);
    return $form;
  }

  /**
   * Ensures team value is kept in sync with is_team_ticket.
   *
   * @param string $entity_type_id
   *   The entity type id.
   * @param \Drupal\contacts_events\Entity\Ticket $ticket
   *   The ticket instance.
   * @param array $form
   *   Form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   */
  public function buildTicket($entity_type_id, Ticket $ticket, array &$form, FormStateInterface $form_state) {
    if (!$ticket->get('is_team_ticket')->value) {
      $ticket->set('team', NULL);
    }
  }

  /**
   * Extracts the correct client-side element name for the is_team_ticket field.
   *
   * @param array $form
   *   Form array.
   *
   * @return string
   *   Element name.
   */
  private function getTeamTicketElementName(array &$form) {
    $is_team_ticket_parents = $form['is_team_ticket']['widget']['#parents'];
    $is_team_ticket_element_name = array_shift($is_team_ticket_parents);
    if (count($is_team_ticket_parents)) {
      $is_team_ticket_element_name .= '[' . implode('][', $is_team_ticket_parents) . ']';
    }
    $is_team_ticket_element_name .= '[value]';
    return $is_team_ticket_element_name;
  }

  /**
   * Adds the submit handler for setting up the team application.
   *
   * @param array $form
   *   The form.
   */
  protected function addSetupTeamApplicationSubmitHandler(array &$form) {
    // This handler needs to run *before* TicketInlineForm::submitConfirmTicket.
    // to ensure that team emails are sent out correctly.
    // This handler will only be already present if the ticket is not
    // yet confirmed.
    // Check to see if the handler's already in place. If so, insert our
    // handler before it. If not, put it at the end.
    $new_handler = [$this, 'submitSetupTeamApplication'];
    $handler_added = FALSE;

    if (isset($form['#ief_element_submit'])) {
      foreach ($form['#ief_element_submit'] as $delta => $handler) {
        // Find position of submitConfirmTicket, and insert before that.
        if (is_array($handler) && $handler[1] == 'submitConfirmTicket') {
          array_splice($form['#ief_element_submit'], $delta, 0, [$new_handler]);
          $handler_added = TRUE;
          break;
        }
      }
    }

    if (!$handler_added) {
      // If the TicketInlineForm didn't add the submitConfirmTicket handler
      // (if the ticket was already confirmed)
      // then put our one at the end.
      $form['#ief_element_submit'][] = $new_handler;
    }
  }

  /**
   * Handles changing the team for the ticket.
   *
   * @param array $entity_form
   *   Form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   */
  public function submitSetupTeamApplication(array &$entity_form, FormStateInterface $form_state) {
    /** @var \Drupal\contacts_events\Entity\TicketInterface $ticket */
    $ticket = $entity_form['#entity'];
    $ticket_data = $form_state->getValue($entity_form['#parents']);
    $order_item = $ticket->getOrderItem();

    // Check if a team application is being undone.
    if (!$ticket->get('is_team_ticket')->value || $ticket->get('team')->isEmpty()) {
      // If ticket was team, set back to pending.
      if ($order_item->get('state')->value == 'team_app_in_progress') {
        $this->applyTransitionIfAllowed($order_item->get('state')->first(), 'team_app_back_to_pending', TRUE);
      }

      // Don't need to do any processing if this isn't a team ticket.
      return;
    }

    /** @var \Drupal\contacts_events_teams\Entity\Team $selected_team */
    $selected_team = $ticket->get('team')->entity;
    $existing_application = $this->queries->getTeamApplicationForTicket($ticket);
    // The final status of the team application depends on the mode selected
    // (either 'new_application' or 'approve')
    $auto_accept = $ticket_data['team_container']['team_status_container']['team_status'] == 'approve';

    // When editing an existing ticket, don't need to do any processing if
    // the team hasn't changed.
    if ($existing_application != NULL && ($existing_application->getTeam() && $existing_application->getTeam()->id() == $ticket->get('team')->target_id)) {
      return;
    }

    // If we require a new team application form, transition ticket to
    // team_app_in_progress state. If we're automatically accepting the
    // application then TeamTicketStateSubscriber::updateTicketState will
    // handle setting the ticket to paid_in_full, so don't need to do it
    // explicitly here. This must happen before the application is saved.
    if (!$auto_accept) {
      $original_state = $order_item->get('state')->value;
      $order_item
        ->set('state', 'team_app_in_progress')
        ->save();

      // If we're going from pending -> team_app_in_progress the event
      // subscriber will be correctly fired. However, if the current state is
      // paid_in_full or confirmed (or something else) then we need to manually
      // fire off the transition event as this isn't a normal/valid transition.
      // If we don't do this, the team application email won't be sent.
      if ($original_state != 'pending') {
        $this->fireTeamAppInProgresstransition($order_item);
      }
    }

    if ($existing_application) {
      $existing_status = $existing_application->get('state')->value;
      // changeTeam saves both the existing an new applications. No need to
      // explicit save.
      $application = TeamApplication::changeTeam($existing_application, $selected_team, $auto_accept);

      // Manually fire the approve event if we need to, as some combinations
      // of state  change for the existing application won't trigger this
      // automatically.
      // For example going draft (on the original application) to approved
      // is not usually valid, so we have to fire it manually to ensure
      // the ticket subscriber runs.
      if ($auto_accept) {
        $this->fireApproveIfNeeded($application, $existing_status, $order_item);
        // Explicitly reload the ticket so IEF gets the version containing
        // the order item modified by the subscribers.
        $entity_form['#entity'] = $this->entityTypeManager->getStorage('contacts_ticket')->loadUnchanged($ticket->id());
      }
    }
    else {
      $application = TeamApplication::createForTicket($ticket, $selected_team, $auto_accept);
      $application->save();

      if ($auto_accept) {
        // If auto accepting a new application, the 'approve' transition
        // won't automatically fire as draft -> approve isn't usually valid.
        // Fire the event manually.
        $this->fireApproveIfNeeded($application, 'draft', $order_item);
        // Explicitly reload the ticket so IEF gets the version containing
        // the order item modified by the subscribers.
        $entity_form['#entity'] = $this->entityTypeManager->getStorage('contacts_ticket')->loadUnchanged($ticket->id());
      }
    }
  }

  /**
   * Fires the "team_app_in_progress" transition on an order item.
   *
   * @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
   *   Order item.
   *
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   */
  protected function fireTeamAppInProgresstransition(OrderItemInterface $order_item) {
    /** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItem $state */
    $state = $order_item->get('state')->first();
    $workflow = $state->getWorkflow();
    $transition_id = 'team_app_in_progress';
    $transition = $workflow->getTransition($transition_id);
    $event = new WorkflowTransitionEvent($transition, $workflow, $order_item, 'state');
    $this->dispatchTransitionEvent($transition_id, 'post_transition', $event, $order_item);
  }

  /**
   * Fires the "approve" transition on a team application.
   *
   * @param \Drupal\contacts_events_teams\Entity\TeamApplication $app
   *   Team application.
   * @param string $original_status
   *   The previous state of the team application (if any).
   * @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
   *   Order item associated with the application.
   *
   * @throws \Drupal\Core\TypedData\Exception\MissingDataException
   */
  protected function fireApproveIfNeeded(TeamApplication $app, $original_status, OrderItemInterface $order_item) {
    /** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItem $state */
    $state = $app->get('state')->first();
    $workflow = $state->getWorkflow();

    if ($transition = $workflow->findTransition($original_status, $state->value)) {
      // If the state change leads to an approve transition, no need to fire it
      // manually.
      if ($transition->getId() == 'approve') {
        return;
      }
    }

    // Before approving the team application, ensure that the order item
    // is already in the confirmed state (as the TeamTicketStateSubscriber
    // will only move the ticket to paid_in_full if it's confirmed already).
    if ($order_item->get('state')->value == 'pending') {
      $order_item->set('state', 'confirmed');
      $order_item->save();
    }

    $transition_id = 'approve';
    $transition = $workflow->getTransition($transition_id);
    $event = new WorkflowTransitionEvent($transition, $workflow, $app, 'state');
    $this->dispatchTransitionEvent($transition_id, 'post_transition', $event, $app);
  }

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return [
      'preRender',
    ];
  }

}

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

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