contacts_events-8.x-1.x-dev/src/Plugin/Field/FieldWidget/OrderItemTicketInlineEntityWidget.php
src/Plugin/Field/FieldWidget/OrderItemTicketInlineEntityWidget.php
<?php
namespace Drupal\contacts_events\Plugin\Field\FieldWidget;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowBase;
use Drupal\commerce_order\Entity\OrderInterface;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\inline_entity_form\Element\InlineEntityForm;
use Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormBase;
use Drupal\inline_entity_form\Plugin\Field\FieldWidget\InlineEntityFormComplex;
/**
* Inline widget for tickets.
*
* @FieldWidget(
* id = "inline_entity_form_order_item_tickets",
* label = @Translation("Booking Tickets"),
* field_types = {
* "entity_reference"
* },
* multiple_values = true
* )
*/
class OrderItemTicketInlineEntityWidget extends InlineEntityFormComplex {
/**
* {@inheritdoc}
*/
protected function getTargetBundles() {
// Don't allow creation of any other order item type other than ticket.
return ['contacts_ticket'];
}
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return ['form_mode' => 'booking'] + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$element = parent::settingsForm($form, $form_state);
$element['allow_new']['#access'] = FALSE;
$element['allow_existing']['#access'] = FALSE;
$element['match_operator']['#access'] = FALSE;
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
return InlineEntityFormBase::settingsSummary();
}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
// Set up a few bits early so we can pre-open a specific order item.
if ($form_state->get('inline_entity_form_order_item_tickets')) {
$early_open = $form_state->get('inline_entity_form_order_item_tickets');
}
else {
$query_params = \Drupal::request()->query->all();
if (!empty($query_params['op']) && !empty($query_params['id'])) {
$early_open = [
'op' => $query_params['op'],
'id' => $query_params['id'],
];
}
}
if (!empty($early_open)) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$parents = array_merge($element['#field_parents'], [$items->getName(), 'form']);
$this->setIefId(sha1(implode('-', $parents)));
// Only act if entities aren't already initialised.
$location = ['inline_entity_form', $this->getIefId(), 'entities'];
if ($form_state->get($location) === NULL) {
$this->prepareFormState($form_state, $items, $this->isTranslating($form_state));
$entities = $form_state->get($location);
foreach ($entities as $delta => $entity) {
if ($entity['entity']->id() == $early_open['id']) {
$location[] = $delta;
$location[] = 'form';
$form_state->set($location, $early_open['op']);
break;
}
}
}
}
$element = parent::formElement($items, $delta, $element, $form, $form_state);
// Disable the table drag as we don't want that.
$element['entities']['#cache']['contexts'][] = 'user.permissions';
$element['entities']['#disable_tabledrag'] = TRUE;
// Override the field title as we are only dealing with tickets.
$element['#field_title'] = $this->t('Tickets');
if ($items->offsetExists($delta)) {
// If the item exists but doesn't have an entity then remove the item.
if (!$items[$delta]->entity) {
unset($items[$delta]);
unset($element['entities'][$delta]);
}
}
foreach (Element::children($element['entities']) as $delta) {
if (!isset($items[$delta]->entity) || $items[$delta]->entity->bundle() != 'contacts_ticket') {
unset($element['entities'][$delta]);
}
// Check if the entities need reloading due to price change.
elseif ($form_state->get('inline_entity_form_order_item_tickets_reload')) {
$entity_id = $element['entities'][$delta]['#entity']->id();
$entity_unchanced = \Drupal::entityTypeManager()->getStorage('commerce_order_item')->load($entity_id);
$element['entities'][$delta]['#entity'] = $entity_unchanced;
}
}
// Build a parents array for this element's values in the form.
$parents = array_merge($element['#field_parents'], [
$items->getName(),
'form',
]);
// Loop over the tickets and add the cancel form and operation as required.
$entities = $form_state->get([
'inline_entity_form',
$this->getIefId(),
'entities',
]);
$workflow = NULL;
$has_form = (bool) $form_state
->get(['inline_entity_form', $this->getIefId(), 'form']);
foreach (Element::children($element['entities']) as $delta) {
$entity_element = &$element['entities'][$delta];
$entity = $entity_element['#entity'];
$entity_form = $entities[$delta]['form'] ?? FALSE;
// If we are showing the row, update the AJAX settings to hide the form
// actions.
if (!$entity_form) {
// Make each entity form action hide the submit buttons.
foreach (Element::children($entity_element['actions']) as $key) {
$type = $entity_element['actions'][$key]['#type'] ?? NULL;
if ($type == 'submit' && isset($entity_element['actions'][$key]['#ajax'])) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$entity_element['actions'][$key]['#ajax']['callback'] = [static::class, 'ajaxCallback'];
}
}
}
// Otherwise hide the add new action so we don't have multiple open.
else {
$has_form = TRUE;
}
if ($entity->getPurchasedEntity()->access('transfer')) {
// Add the transfer operation.
if (!$entity_form) {
$entity_element['actions']['transfer'] = [
'#type' => 'link',
'#title' => $this->t('Transfer ticket'),
'#url' => Url::fromRoute('entity.contacts_ticket.transfer_form', ['contacts_ticket' => $entity->getPurchasedEntityId()]),
'#attributes' => [
'class' => [
'button',
'btn',
'btn-primary',
'order-lg-last',
'mb-1',
'mb-sm-2',
],
],
];
}
}
// Check if we have cancel access.
if (!$entity->access('cancel')) {
continue;
}
// Add the cancel operation if we're not in a form already.
if (!$entity_form) {
$entity_element['actions']['cancel'] = [
'#type' => 'submit',
'#value' => $this->t('Cancel ticket'),
'#name' => 'ief-' . $this->getIefId() . '-entity-edit-' . $delta,
'#limit_validation_errors' => [],
'#ajax' => [
'callback' => [static::class, 'ajaxCallback'],
'wrapper' => 'inline-entity-form-' . $this->getIefId(),
],
'#submit' => [[$this, 'cancelSubmit']],
'#ief_row_delta' => $delta,
'#ief_row_form' => 'cancel',
];
}
// Show the cancel form if it has already been expanded.
elseif ($entity_form == 'cancel') {
$entity_element['form'] = [
'#type' => 'container',
'#attributes' => ['class' => ['ief-form', 'ief-form-row']],
// Used by Field API and controller methods to find the relevant
// values in $form_state.
'#parents' => array_merge($parents, ['entities', $delta, 'form']),
// Store the entity on the form, later modified in the controller.
'#entity' => $entity,
// Identifies the IEF widget to which the form belongs.
'#ief_id' => $this->getIefId(),
// Identifies the table row to which the form belongs.
'#ief_row_delta' => $delta,
];
$parent_langcode = $items->getEntity()->language()->getId();
$this->buildCancelForm($entity_element['form'], $delta, $parent_langcode, $parents);
}
}
// If we have an expanded item, hide all other operations.
if ($has_form) {
// Use a process to disable the outer form actions.
$element['#process'][] = [static::class, 'disableActions'];
// Disable IEF actions such as 'Add'.
$element['actions']['#disabled'] = TRUE;
// Loop over the rows to disable row operations.
foreach (Element::children($element['entities']) as $delta) {
$element['entities'][$delta]['actions']['#disabled'] = TRUE;
}
}
// Set the IEF actions to use the ajax callback.
foreach (Element::children($element['actions']) as $key) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$element['actions'][$key]['#ajax']['callback'] = [static::class, 'ajaxCallback'];
}
return $element;
}
/**
* Process callback to disable the outer form actions.
*/
public static function disableActions(array $element, FormStateInterface $form_state, &$complete_form) {
foreach (Element::children($complete_form['actions']) as $key) {
$complete_form['actions'][$key]['#attributes']['disabled'] = TRUE;
}
return $element;
}
/**
* Submission handler for cancel button.
*
* @param array $form
* The form being submitted.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the form being submitted.
*/
public function cancelSubmit(array $form, FormStateInterface $form_state) {
$element = inline_entity_form_get_element($form, $form_state);
$ief_id = $element['#ief_id'];
$delta = $form_state->getTriggeringElement()['#ief_row_delta'];
$form_state->setRebuild();
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$form_state->set(['inline_entity_form', $ief_id, 'entities', $delta, 'form'], $form_state->getTriggeringElement()['#ief_row_form']);
}
/**
* {@inheritdoc}
*/
public static function buildEntityFormActions($element) {
$element = parent::buildEntityFormActions($element);
foreach (Element::children($element['actions']) as $key) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$element['actions'][$key]['#submit'][] = [static::class, 'clearTicketFromState'];
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$element['actions'][$key]['#ajax']['callback'] = [static::class, 'ajaxCallback'];
if ($key == 'ief_cancel_save') {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$element['actions'][$key]['#submit'][] = [static::class, 'clearTicketFromState'];
$element['actions'][$key]['#value'] = new TranslatableMarkup('Cancel ticket');
}
if ($key == 'ief_cancel_cancel') {
$element['actions'][$key]['#value'] = new TranslatableMarkup('Close');
}
}
return $element;
}
/**
* Submission callback to clear the ticket from the form state.
*
* @param array $entity_form
* The entity form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public static function clearTicketFromState(array &$entity_form, FormStateInterface $form_state) {
$form_state->set('ticket', NULL);
}
/**
* {@inheritdoc}
*/
protected function getEntityTypeLabels() {
// The admin has specified the exact labels that should be used.
if ($this->getSetting('override_labels')) {
return [
'singular' => $this->getSetting('label_singular'),
'plural' => $this->getSetting('label_plural'),
];
}
else {
return [
'singular' => $this->t('ticket'),
'plural' => $this->t('tickets'),
];
}
}
/**
* {@inheritdoc}
*/
public static function isApplicable(FieldDefinitionInterface $field_definition) {
// Only allow for order items on the contacts booking bundle of orders.
return $field_definition->getTargetEntityTypeId() == 'commerce_order'
&& $field_definition->getName() == 'order_items';
}
/**
* {@inheritdoc}
*/
public static function submitSaveEntity($entity_form, FormStateInterface $form_state) {
// TicketInlineForm::save handles saving new order items, but we need to
// make sure we track the correct item, so always pull from the ticket form.
$ticket_form = $entity_form['purchased_entity']['widget'][0]['inline_entity_form'];
$order_item = $ticket_form['#entity']->getOrderItem();
$entity_form['#entity'] = $order_item;
parent::submitSaveEntity($entity_form, $form_state);
$form_object = $form_state->getFormObject();
// The form object may be a CheckoutFlowBase object which does not extend
// FormBase so needs another method for accessing the order.
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $form_object instanceof CheckoutFlowBase ? $form_object->getOrder() : $form_object->getEntity();
// We're explicitly updating the order items, so skip refreshing. Otherwise
// we get a double save and the entity in the form is out of date.
$order->setRefreshState(OrderInterface::REFRESH_SKIP);
// Ensure the item is added to the order and the total recalculated.
$order->addItem($order_item)
->save();
// Allow form element to rebuild entities after price recalculation.
$form_state->set('inline_entity_form_order_item_tickets_reload', TRUE);
}
/**
* Builds cancel form.
*
* @param array $form
* Form array structure.
* @param string $bundle
* Entity bundle.
* @param string $parent_langcode
* The parent language code.
* @param array $parents
* Array of parent element names.
*/
protected function buildCancelForm(array &$form, $bundle, $parent_langcode, array $parents) {
/** @var \Drupal\Core\Entity\EntityInterface $entity */
$entity = $form['#entity'];
$entity_label = $this->inlineFormHandler->getEntityLabel($entity);
$labels = $this->getEntityTypeLabels();
if ($entity_label) {
$message = $this->t('Are you sure you want to cancel %label?', ['%label' => $entity_label]);
}
else {
$message = $this->t('Are you sure you want to cancel this %entity_type?', ['%entity_type' => $labels['singular']]);
}
$form['message'] = [
'#theme_wrappers' => ['container'],
'#markup' => $message,
];
$this->setSetting('form_mode', 'cancel');
$form['inline_entity_form'] = $this->getInlineEntityForm(
'cancel',
$entity->bundle(),
$parent_langcode,
$bundle,
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
array_merge($parents, ['inline_entity_form', 'entities', $bundle, 'form']),
$entity
);
$form['inline_entity_form']['#process'] = [
[InlineEntityForm::class, 'processEntityForm'],
[get_class($this), 'addIefSubmitCallbacks'],
[get_class($this), 'buildEntityFormActions'],
];
}
/**
* {@inheritdoc}
*/
public static function addIefSubmitCallbacks($element) {
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$element['#ief_element_submit'][] = [get_called_class(), 'submitConfirmCancel'];
$element = parent::addIefSubmitCallbacks($element);
return $element;
}
/**
* Cancel form submit callback.
*
* The row is identified by #ief_row_delta stored on the triggering
* element.
* This isn't an #element_validate callback to avoid processing the
* cancel form when the main form is submitted.
*
* @param array $element
* The complete parent form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state of the parent form.
*/
public static function submitConfirmCancel(array &$element, FormStateInterface $form_state) {
$cancel_button = $form_state->getTriggeringElement();
// Only trigger on corrent form operation and trigger button.
if ($element['#op'] !== 'cancel' || end($cancel_button['#parents']) !== 'ief_cancel_save') {
return;
}
$delta = $cancel_button['#ief_row_delta'];
/** @var \Drupal\Core\Entity\EntityInterface $entity */
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $entity */
$entity = $element['#entity'];
$entity_id = $entity->id();
$form_values = NestedArray::getValue($form_state->getValues(), $element['#parents']);
$form_state->setRebuild();
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$widget_state = $form_state->get(['inline_entity_form', $element['#ief_id']]);
// This entity hasn't been saved yet, we can just unlink it.
if (empty($entity_id) || ($cancel_button['#allow_existing'] && empty($form_values['delete']))) {
unset($widget_state['entities'][$delta]);
}
// The entity has been saved and OrderItemCancelForm::submitForm() isn't
// called by IEF so update the order item and ticket accordingly.
else {
/** @var \Drupal\state_machine\Plugin\Field\FieldType\StateItem $state */
$state = $entity->get('state')->first();
$state->applyTransitionById('cancel');
/** @var \Drupal\contacts_events\Entity\Ticket $ticket */
$ticket = $entity->getPurchasedEntity();
if (!$entity->get('mapped_price')->isEmpty()) {
$mapped_price = $entity->get('mapped_price')->first()->getValue();
$mapped_price['class_overridden'] = TRUE;
$entity->set('mapped_price', $mapped_price);
$ticket->setMappedPrice($mapped_price);
}
$entity->save();
// Clear contact and email so they could be re-booked on to event.
$ticket->set('contact', NULL);
$ticket->set('email', NULL);
$ticket->save();
// The order item's state change won't be reflected in the UI
// unless the order item is explicitly set back into the form for IEF
// to pick up.
$widget_state['entities'][$delta]['entity'] = $entity;
}
// Show status message to user.
\Drupal::messenger()->addStatus(new TranslatableMarkup('You have successfully cancelled this ticket.'));
// phpcs:ignore Drupal.Arrays.Array.LongLineDeclaration
$form_state->set(['inline_entity_form', $element['#ief_id']], $widget_state);
$form_state->setRebuild();
}
/**
* {@inheritdoc}
*/
public static function submitConfirmRemove($form, FormStateInterface $form_state) {
parent::submitConfirmRemove($form, $form_state);
$element = inline_entity_form_get_element($form, $form_state);
$remove_button = $form_state->getTriggeringElement();
$delta = $remove_button['#ief_row_delta'];
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $element['entities'][$delta]['form']['#entity'];
// Get the right order entity.
$form_object = $form_state->getFormObject();
// The form object may be a CheckoutFlowBase object which does not extend
// FormBase so needs another method for accessing the order.
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $form_object instanceof CheckoutFlowBase ? $form_object->getOrder() : $form_object->getEntity();
// Remove the order item from the order, save the order and delete the order
// item. We do this immediately so you don't have to separate save the page.
$order->removeItem($order_item);
$order->save();
$order_item->delete();
// Show status message to user.
\Drupal::messenger()->addStatus(new TranslatableMarkup('This unconfirmed ticket has been removed.'));
}
/**
* Ajax callback to show or hide outer form actions.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* An AJAX response containing both the default IEF update and the commands
* for showing or hiding the outer form actions.
*/
public static function ajaxCallback(array $form, FormStateInterface $form_state) {
// Wrap the inline entity form ajax callback so we can add commands.
// To do that, build a response from the callback.
// @see FormAjaxResponseBuilder::buildResponse
$form = inline_entity_form_get_element($form, $form_state);
$request = \Drupal::request();
$route_match = \Drupal::routeMatch();
$response = \Drupal::service('main_content_renderer.ajax')->renderResponse($form, $request, $route_match);
// Replace the outer form actions.
$complete_form = $form_state->getCompleteForm();
$actions = [];
foreach (Element::children($complete_form['actions']) as $key) {
$actions[$key] = $complete_form['actions'][$key];
}
$response->addCommand(new HtmlCommand('#edit-actions', $actions));
return $response;
}
}
