contacts_events-8.x-1.x-dev/src/Form/TicketInlineForm.php
src/Form/TicketInlineForm.php
<?php namespace Drupal\contacts_events\Form; use Drupal\Component\Utility\NestedArray; use Drupal\contacts_events\Entity\TicketInterface; use Drupal\contacts_events\Event\TicketPrepopulateEvent; use Drupal\contacts_events\OrderStateTrait; use Drupal\contacts_events\PriceCalculator; use Drupal\Core\Config\ImmutableConfig; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\inline_entity_form\Element\InlineEntityForm; use Drupal\inline_entity_form\Form\EntityInlineForm; use Drupal\user\UserInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * The inline form for Ticket entities. */ class TicketInlineForm extends EntityInlineForm { use TicketFormHookTrait; use OrderStateTrait; /** * The price calculator service. * * @var \Drupal\contacts_events\PriceCalculator */ protected $priceCalculator; /** * Configuration. * * @var \Drupal\Core\Config\ImmutableConfig */ protected $bookingSettings; /** * Event dispatcher. * * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface */ protected $dispatcher; /** * {@inheritdoc} */ public function __construct(EntityFieldManagerInterface $entity_field_manager, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, EntityTypeInterface $entity_type, PriceCalculator $price_calculator, ImmutableConfig $booking_settings, EventDispatcherInterface $dispatcher) { parent::__construct($entity_field_manager, $entity_type_manager, $module_handler, $entity_type); $this->priceCalculator = $price_calculator; $this->bookingSettings = $booking_settings; $this->dispatcher = $dispatcher; } /** * {@inheritdoc} */ public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { return new static( $container->get('entity_field.manager'), $container->get('entity_type.manager'), $container->get('module_handler'), $entity_type, $container->get('contacts_events.price_calculator'), $container->get('config.factory')->get('contacts_events.booking_settings'), $container->get('event_dispatcher') ); } /** * {@inheritdoc} * * @see hook_contacts_events_ticket_form_alter() */ public function entityForm(array $entity_form, FormStateInterface $form_state) { // Get the entity we're building. The item in form state is going to be the // most up to date, if it's available. if ($ticket = $form_state->get('ticket')) { $entity_form['#entity'] = $ticket; } else { $ticket = $entity_form['#entity']; } /** @var \Drupal\contacts_events\Entity\TicketInterface $ticket */ // Ensure we have the order item set on the ticket. if (!$ticket->getOrderItem()) { // Get the order item form element. $parents = array_slice($entity_form['#array_parents'], 0, -4); $order_item_element = &NestedArray::getValue($form_state->getCompleteForm(), $parents); $ticket->set('order_item', $order_item_element['#entity']); } // If this is the first ticket in the booking, and auto-fill first ticket is // enabled, then pre-populate the ticket info from the booking manager's // individual profile. if ($ticket->isNew() && $this->bookingSettings->get('autofill_first_ticket')) { $order = $ticket->getOrderItem()->getOrder(); if (count($order->getItems()) == 0) { $this->copyUserDetailsToTicket($order->getCustomer(), $ticket); } } $entity_form = parent::entityForm($entity_form, $form_state); // Add checkbox for manually confirming the ticket. $this->addConfirmCheckbox($entity_form, $ticket, $form_state); // Add our update handler for the price. TicketForm::addPriceAjax( $entity_form, [[static::class, 'rebuildEntity']], $ticket->id() ?? 'new' ); $this->alter($entity_form, $form_state, $ticket, $entity_form['#form_mode']); return $entity_form; } /** * Adds a checkbox allowing staff to manually confirm a ticket. * * @param array $form * Form array. * @param \Drupal\contacts_events\Entity\TicketInterface $ticket * Ticket being added/edited. * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. */ protected function addConfirmCheckbox(array &$form, TicketInterface $ticket, FormStateInterface $form_state) { $form_mode = $form['#form_mode']; // Add in an option that allows staff to confirm tickets. if ($form_mode == 'booking_admin' && \Drupal::currentUser()->hasPermission('can manage bookings for contacts_events')) { // Only show the 'confirm ticket' option if the ticket is pending. if ($ticket->getStatus() == 'pending') { $form['confirm_wrapper'] = [ '#type' => 'fieldset', '#weight' => 100, '#title' => new TranslatableMarkup('Confirm Ticket'), ]; $booking_state = $ticket->getBooking()->getState()->value; if ($booking_state == 'confirmed' || $booking_state == 'modified') { $form['confirm_wrapper']['confirm_ticket'] = [ '#type' => 'checkbox', '#title' => new TranslatableMarkup('By ticking this box, the ticket will be moved from the pending state to the Confirmed state'), ]; // Add a new submit handler to handle confirming the ticket. // This is done as an ief_element_submit to ensure it runs // after the 'save' method has finished. $form['#ief_element_submit'][] = [$this, 'submitConfirmTicket']; } else { // If the booking is unconfirmed, show a message saying the booking // needs to be confirmed. $form['confirm_wrapper']['info'] = ['#markup' => new TranslatableMarkup('The booking has not been confirmed. Confirming this booking will also mark the ticket as confirmed. You should do this on the "View" tab.')]; } } } } /** * Submission handler to rebuild the form with submitted values. * * @param array $form * The form array. * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state. */ public static function rebuildEntity(array $form, FormStateInterface $form_state) { // Get the entity form. $entity_form_parents = $form_state->getTriggeringElement()['#array_parents']; array_pop($entity_form_parents); $entity_form = NestedArray::getValue($form, $entity_form_parents); /** @var \Drupal\contacts_events\Entity\TicketInterface $ticket */ $ticket = $entity_form['#entity']; // Get the handler and trigger the rebuild. $inline_form_handler = InlineEntityForm::getInlineFormHandler($entity_form['#entity_type']); $inline_form_handler->buildEntity($entity_form, $ticket, $form_state); // Put the most up to date version into the form state and ensure the order // item is up to date. $form_state->set('ticket', $ticket); $form_state->setRebuild(); } /** * {@inheritdoc} */ public function buildEntity(array $entity_form, ContentEntityInterface $entity, FormStateInterface $form_state) { assert($entity instanceof TicketInterface); parent::buildEntity($entity_form, $entity, $form_state); // @todo Abstract ticket form acquisition into a trait for use with both // Ticket forms and Inline Entity forms. // Clear out contact for new tickets to allow for ajax updates. if ($entity->isNew()) { $entity->set('contact', NULL); } // Run an early acquisition, as ticket classes may respond to the contact. $entity->acquire(TRUE); // Recalculate the price of the ticket using the form handler. // @todo Something to optimize this and prevent it being calculated // mulitple time in a single request when nothing changes - perhaps an // onChange in the item and ticket? $this->priceCalculator->calculatePrice($entity->getOrderItem()); } /** * {@inheritdoc} */ public function save(EntityInterface $entity) { /** @var \Drupal\contacts_events\Entity\TicketInterface $entity */ $order_item = $entity->getOrderItem(); // Update the order item title. $order_item->set('title', $entity->getOrderItemTitle()); // New ticket and order items reference each other and end in a loop. To // avoid that, clear the purchased entity off the order item, save it, // restore it and then save again. // OrderItemTicketInlineEntityWidget::submitSaveEntity ensures the correct // order item is tracked in the form. if ($entity->isNew() && $order_item && $order_item->isNew()) { // Also set the title, as doing it this way results in it not being // properly set. $order_item ->set('purchased_entity', NULL) ->save(); $order_item ->set('purchased_entity', $entity) ->save(); } // If it's not new, we can simply save. elseif ($order_item) { $order_item->save(); } // Ensure the right order item entity is on the ticket. $entity->setOrderItem($order_item); // Now we can proceed to save the ticket. $entity->save(); } /** * Callback for handling confirming the ticket. * * If the user selected the 'confirm ticket' checkbox, then confirm it. * Note that this runs after the save method, so the ticket will already * have been saved once by the time this is called. * * @param array $entity_form * The entity form array. * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state. */ public function submitConfirmTicket(array $entity_form, FormStateInterface $form_state) { $ticket_data = $form_state->getValue($entity_form['#parents']); if ($ticket_data['confirm_wrapper']['confirm_ticket'] ?? FALSE) { /** @var \Drupal\contacts_events\Entity\TicketInterface $ticket */ $ticket = $entity_form['#entity']; /** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItem $state */ $state = $ticket->getOrderItem()->get('state')->first(); $this->applyTransitionIfAllowed($state, 'confirm', TRUE); } } /** * Copies information from the individual profile to the ticket. * * @param \Drupal\user\UserInterface $user * The user to copy. * @param \Drupal\contacts_events\Entity\TicketInterface $ticket * The target ticket. */ protected function copyUserDetailsToTicket(UserInterface $user, TicketInterface $ticket) { if (isset($user->profile_crm_indiv->entity)) { $profile = $user->get('profile_crm_indiv')->entity; $ticket->set('contact', $user); $ticket->set('name', $profile->get('crm_name')->getValue()); $ticket->set('email', $profile->getOwner()->getEmail()); } // Allow other modules to perform their own prepopulate. $this->dispatcher->dispatch(TicketPrepopulateEvent::NAME, new TicketPrepopulateEvent($ticket, $user)); } }