commerce_shipping-8.x-2.0-rc2/src/Plugin/Commerce/CheckoutPane/ShippingInformation.php

src/Plugin/Commerce/CheckoutPane/ShippingInformation.php
<?php

namespace Drupal\commerce_shipping\Plugin\Commerce\CheckoutPane;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\commerce\AjaxFormTrait;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
use Drupal\commerce_shipping\Entity\ShipmentInterface;
use Drupal\profile\Entity\ProfileInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Provides the shipping information pane.
 *
 * Collects the shipping profile, then the information for each shipment.
 * Assumes that all shipments share the same shipping profile.
 *
 * @CommerceCheckoutPane(
 *   id = "shipping_information",
 *   label = @Translation("Shipping information"),
 *   wrapper_element = "fieldset",
 * )
 */
class ShippingInformation extends CheckoutPaneBase implements ContainerFactoryPluginInterface {

  use AjaxFormTrait;

  /**
   * The entity type bundle info.
   *
   * @var \Drupal\Core\Entity\EntityTypeBundleInfo
   */
  protected $entityTypeBundleInfo;

  /**
   * The inline form manager.
   *
   * @var \Drupal\commerce\InlineFormManager
   */
  protected $inlineFormManager;

  /**
   * The packer manager.
   *
   * @var \Drupal\commerce_shipping\PackerManagerInterface
   */
  protected $packerManager;

  /**
   * The order shipment summary.
   *
   * @var \Drupal\commerce_shipping\OrderShipmentSummaryInterface
   */
  protected $orderShipmentSummary;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, ?CheckoutFlowInterface $checkout_flow = NULL) {
    $instance = parent::create($container, $configuration, $plugin_id, $plugin_definition, $checkout_flow);
    $instance->entityTypeBundleInfo = $container->get('entity_type.bundle.info');
    $instance->inlineFormManager = $container->get('plugin.manager.commerce_inline_form');
    $instance->packerManager = $container->get('commerce_shipping.packer_manager');
    $instance->orderShipmentSummary = $container->get('commerce_shipping.order_shipment_summary');

    return $instance;
  }

  /**
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'auto_recalculate' => TRUE,
      'require_shipping_profile' => TRUE,
    ] + parent::defaultConfiguration();
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationSummary() {
    $parent_summary = parent::buildConfigurationSummary();
    if (!empty($this->configuration['require_shipping_profile'])) {
      $summary = $this->t('Hide shipping costs until an address is entered: Yes') . '<br>';
    }
    else {
      $summary = $this->t('Hide shipping costs until an address is entered: No') . '<br>';
    }
    if (!empty($this->configuration['auto_recalculate'])) {
      $summary .= $this->t('Autorecalculate: Yes');
    }
    else {
      $summary .= $this->t('Autorecalculate: No');
    }

    return $parent_summary ? implode('<br>', [$parent_summary, $summary]) : $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
    $form = parent::buildConfigurationForm($form, $form_state);
    $form['require_shipping_profile'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Hide shipping costs until an address is entered'),
      '#default_value' => $this->configuration['require_shipping_profile'],
    ];
    $form['auto_recalculate'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Auto recalculate shipping costs when the shipping address changes'),
      '#default_value' => $this->configuration['auto_recalculate'],
      '#states' => [
        'visible' => [
          ':input[name="configuration[panes][shipping_information][configuration][require_shipping_profile]"]' => ['checked' => TRUE],
        ],
      ],
    ];

    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
    parent::submitConfigurationForm($form, $form_state);

    if (!$form_state->getErrors()) {
      $values = $form_state->getValue($form['#parents']);
      $this->configuration['require_shipping_profile'] = !empty($values['require_shipping_profile']);
      $this->configuration['auto_recalculate'] = !empty($values['auto_recalculate']) && $this->configuration['require_shipping_profile'];
    }
  }

  /**
   * {@inheritdoc}
   */
  public function isVisible() {
    if (!$this->order->hasField('shipments')) {
      return FALSE;
    }

    // The order must contain at least one shippable purchasable entity.
    foreach ($this->order->getItems() as $order_item) {
      $purchased_entity = $order_item->getPurchasedEntity();
      if ($purchased_entity && $purchased_entity->hasField('weight')) {
        return TRUE;
      }
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  public function buildPaneSummary() {
    $summary = [];
    if ($this->isVisible()) {
      $summary = $this->orderShipmentSummary->build($this->order, 'checkout');
    }
    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
    $store = $this->order->getStore();
    $available_countries = [];
    foreach ($store->get('shipping_countries') as $country_item) {
      $available_countries[] = $country_item->value;
    }
    /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $inline_form */
    $inline_form = $this->inlineFormManager->createInstance('customer_profile', [
      'profile_scope' => 'shipping',
      'available_countries' => $available_countries,
      'address_book_uid' => $this->order->getCustomerId(),
      // Don't copy the profile to address book until the order is placed.
      'copy_on_save' => FALSE,
    ], $this->getShippingProfile());

    // Prepare the form for ajax.
    // Not using Html::getUniqueId() on the wrapper ID to avoid #2675688.
    $pane_form['#wrapper_id'] = 'shipping-information-wrapper';
    $pane_form['#prefix'] = '<div id="' . $pane_form['#wrapper_id'] . '">';
    $pane_form['#suffix'] = '</div>';
    // Auto recalculation is enabled only when a shipping profile is required.
    $pane_form['#auto_recalculate'] = !empty($this->configuration['auto_recalculate']) && !empty($this->configuration['require_shipping_profile']);
    $pane_form['#after_build'][] = [static::class, 'autoRecalculateProcess'];

    $pane_form['shipping_profile'] = [
      '#parents' => array_merge($pane_form['#parents'], ['shipping_profile']),
      '#inline_form' => $inline_form,
    ];
    $pane_form['shipping_profile'] = $inline_form->buildInlineForm($pane_form['shipping_profile'], $form_state);
    $triggering_element = $form_state->getTriggeringElement();
    // The shipping_profile should always exist in form state (and not just
    // after "Recalculate shipping" is clicked).
    if (!$form_state->has('shipping_profile') ||
      // For some reason, when the address selected is changed, the shipping
      // profile in form state is stale.
      (isset($triggering_element['#parents']) && in_array('select_address', $triggering_element['#parents'], TRUE))) {
      $form_state->set('shipping_profile', $inline_form->getEntity());
    }

    $class = get_class($this);
    // Ensure selecting a different address refreshes the entire form.
    if (isset($pane_form['shipping_profile']['select_address'])) {
      $pane_form['shipping_profile']['select_address']['#ajax'] = [
        'callback' => [$class, 'ajaxRefreshForm'],
        'element' => $pane_form['#parents'],
      ];
      // Selecting a different address should trigger a recalculation.
      $pane_form['shipping_profile']['select_address']['#recalculate'] = TRUE;
    }

    $pane_form['recalculate_shipping'] = [
      '#type' => 'button',
      '#value' => $this->t('Recalculate shipping'),
      '#recalculate' => TRUE,
      '#ajax' => [
        'callback' => [$class, 'ajaxRefreshForm'],
        'element' => $pane_form['#parents'],
      ],
      // The calculation process only needs a valid shipping profile.
      '#limit_validation_errors' => [
        array_merge($pane_form['#parents'], ['shipping_profile']),
      ],
      '#after_build' => [
        [static::class, 'clearValues'],
      ],
    ];
    $pane_form['removed_shipments'] = [
      '#type' => 'value',
      '#value' => [],
    ];
    $pane_form['shipments'] = [
      '#type' => 'container',
    ];

    $shipping_profile = $form_state->get('shipping_profile');
    $shipments = $this->order->get('shipments')->referencedEntities();
    $recalculate_shipping = $form_state->get('recalculate_shipping');
    $can_calculate_rates = $this->canCalculateRates($shipping_profile);

    // If the shipping recalculation is triggered, ensure the rates can
    // be recalculated (i.e a valid address is entered).
    if ($recalculate_shipping && !$can_calculate_rates) {
      $recalculate_shipping = FALSE;
      $shipments = [];
    }

    // Ensure the profile is saved with the latest address, it's necessary
    // to do that in case the profile isn't new, otherwise the shipping profile
    // referenced by the shipment won't reflect the updated address.
    if (!$shipping_profile->isNew() &&
      $shipping_profile->hasTranslationChanges() &&
      $can_calculate_rates) {
      $shipping_profile->save();
      $inline_form->setEntity($shipping_profile);
    }

    $force_packing = empty($shipments) && $can_calculate_rates;
    if ($recalculate_shipping || $force_packing) {
      // We're still relying on the packer manager for packing the order since
      // we don't want the shipments to be saved for performance reasons.
      // The shipments are saved on pane submission.
      [$shipments, $removed_shipments] = $this->packerManager->packToShipments($this->order, $shipping_profile, $shipments);

      // Store the IDs of removed shipments for submitPaneForm().
      $pane_form['removed_shipments']['#value'] = array_map(function ($shipment) {
        /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
        return $shipment->id();
      }, $removed_shipments);
    }

    $single_shipment = count($shipments) === 1;
    foreach ($shipments as $index => $shipment) {
      /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
      $pane_form['shipments'][$index] = [
        '#parents' => array_merge($pane_form['#parents'], ['shipments', $index]),
        '#array_parents' => array_merge($pane_form['#parents'], ['shipments', $index]),
        '#type' => $single_shipment ? 'container' : 'fieldset',
        '#title' => $shipment->getTitle(),
      ];
      $form_display = EntityFormDisplay::collectRenderDisplay($shipment, 'checkout');
      $form_display->removeComponent('shipping_profile');
      $form_display->buildForm($shipment, $pane_form['shipments'][$index], $form_state);
      $pane_form['shipments'][$index]['#shipment'] = $shipment;
    }

    // Update the shipments and save the order if no rate was explicitly
    // selected, that usually occurs when changing addresses, this will ensure
    // the default rate is selected/applied.
    if (!$this->hasRateSelected($pane_form, $form_state) && ($recalculate_shipping || $force_packing)) {
      array_map(function (ShipmentInterface $shipment) {
        if (!$shipment->isNew()) {
          $shipment->save();
        }
      }, $shipments);
      $this->order->set('shipments', $shipments);
      $this->order->save();

      $shipment_storage = $this->entityTypeManager->getStorage('commerce_shipment');
      foreach ($shipments as $index => $shipment) {
        if ($shipment->isNew()) {
          continue;
        }
        // Reload the shipment in case it was updated e.g. the tax adjustments
        // were applied to the shipment.
        $pane_form['shipments'][$index]['#shipment'] = $shipment_storage->load($shipment->id());
      }
    }

    return $pane_form;
  }

  /**
   * Pane form #process callback: adds recalculation settings.
   *
   * @param array $element
   *   The form element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return array
   *   The modified element.
   */
  public static function autoRecalculateProcess(array $element, FormStateInterface $form_state) {
    if ($element['#auto_recalculate']) {
      $recalculate_button_selector = $element['recalculate_shipping']['#attributes']['data-drupal-selector'];
      $element['#attached']['library'][] = 'commerce_shipping/shipping_checkout';
      $element['#attached']['drupalSettings']['commerceShipping'] = [
        'wrapper' => $element['#wrapper_id'],
        'recalculateButtonSelector' => '[data-drupal-selector="' . $recalculate_button_selector . '"]',
      ];
      $element['recalculate_shipping']['#attributes']['class'][] = 'js-hide';
    }

    return $element;
  }

  /**
   * {@inheritdoc}
   */
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $shipment_indexes = Element::children($pane_form['shipments']);
    $triggering_element = $form_state->getTriggeringElement();
    $recalculate = !empty($triggering_element['#recalculate']);
    $button_type = $triggering_element['#button_type'] ?? '';
    /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $inline_form */
    $inline_form = $pane_form['shipping_profile']['#inline_form'];
    /** @var \Drupal\profile\Entity\ProfileInterface $profile */
    $profile = $inline_form->getEntity();

    // The profile in form state needs to reflect the submitted values,
    // for packers invoked on form rebuild, or "Billing same as shipping".
    $form_state->set('shipping_profile', $profile);

    $shipments = [];
    foreach ($shipment_indexes as $index) {
      if (!isset($pane_form['shipments'][$index]['#shipment'])) {
        continue;
      }
      $shipment = clone $pane_form['shipments'][$index]['#shipment'];
      $form_display = EntityFormDisplay::collectRenderDisplay($shipment, 'checkout');
      $form_display->removeComponent('shipping_profile');
      $form_display->extractFormValues($shipment, $pane_form['shipments'][$index], $form_state);
      $form_display->validateFormValues($shipment, $pane_form['shipments'][$index], $form_state);
      $shipment->setShippingProfile($profile);
      $shipments[] = $shipment;
    }

    if (!$recalculate && $button_type == 'primary' && !$shipments) {
      // The checkout step was submitted without shipping being calculated.
      // Force the recalculation now and reload the page.
      $recalculate = TRUE;
      $this->messenger()->addError($this->t('Please select a shipping method.'));
      $form_state->setRebuild(TRUE);
    }
    $form_state->set('recalculate_shipping', $recalculate);

    // If another rate was selected, update the shipments and refresh the order
    // to reflect the new rate in the order summary.
    if (!empty($triggering_element['#rate'])) {
      // Unfortunately, we have to save the shipment, otherwise
      // $order->get('shipments')->referencedEntities() will return stale
      // shipments in case the order is already referencing saved shipments.
      array_map(function (ShipmentInterface $shipment) {
        if (!$shipment->isNew()) {
          $shipment->save();
        }
      }, $shipments);
      $this->order->set('shipments', $shipments);
      $this->order->save();
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $inline_form */
    $inline_form = $pane_form['shipping_profile']['#inline_form'];
    /** @var \Drupal\profile\Entity\ProfileInterface $profile */
    $profile = $inline_form->getEntity();

    // Save the modified shipments.
    $shipments = [];
    foreach (Element::children($pane_form['shipments']) as $index) {
      if (!isset($pane_form['shipments'][$index]['#shipment'])) {
        continue;
      }

      /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
      $shipment = clone $pane_form['shipments'][$index]['#shipment'];
      $form_display = EntityFormDisplay::collectRenderDisplay($shipment, 'checkout');
      $form_display->removeComponent('shipping_profile');
      $form_display->extractFormValues($shipment, $pane_form['shipments'][$index], $form_state);
      $shipment->setShippingProfile($profile);
      $shipment->save();
      $shipments[] = $shipment;
    }
    $this->order->shipments = $shipments;

    // Delete shipments that are no longer in use.
    $removed_shipment_ids = $pane_form['removed_shipments']['#value'];
    if (!empty($removed_shipment_ids)) {
      $shipment_storage = $this->entityTypeManager->getStorage('commerce_shipment');
      $removed_shipments = $shipment_storage->loadMultiple($removed_shipment_ids);
      $shipment_storage->delete($removed_shipments);
    }
  }

  /**
   * Clears user input of selected shipping rates.
   *
   * This is required to prevent invalid options being selected is a shipping
   * rate is no longer available, when the selected address is updated or when
   * the rates recalculation is explicitly triggered.
   *
   * @param array $element
   *   The element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return array
   *   The element.
   */
  public static function clearValues(array $element, FormStateInterface $form_state) {
    $triggering_element = $form_state->getTriggeringElement();
    if (!$triggering_element) {
      return $element;
    }
    $triggering_element_name = end($triggering_element['#parents']);
    if (in_array($triggering_element_name, ['recalculate_shipping', 'select_address'], TRUE)) {
      $user_input = &$form_state->getUserInput();
      $parents = $element['#parents'];
      array_pop($parents);
      $parents[] = 'shipments';
      NestedArray::unsetValue($user_input, $parents);
    }
    return $element;
  }

  /**
   * Gets the shipping profile.
   *
   * The shipping profile is assumed to be the same for all shipments.
   * Therefore, it is taken from the first found shipment, or created from
   * scratch if no shipments were found.
   *
   * @return \Drupal\profile\Entity\ProfileInterface
   *   The shipping profile.
   */
  protected function getShippingProfile() {
    $shipping_profile = NULL;
    /** @var \Drupal\commerce_shipping\Entity\ShipmentInterface $shipment */
    foreach ($this->order->get('shipments')->referencedEntities() as $shipment) {
      $shipping_profile = $shipment->getShippingProfile();
      break;
    }
    if (!$shipping_profile) {
      $profile_type_id = 'customer';
      // Check whether the order type has another profile type ID specified.
      $order_type_id = $this->order->bundle();
      $order_bundle_info = $this->entityTypeBundleInfo->getBundleInfo('commerce_order');
      if (!empty($order_bundle_info[$order_type_id]['shipping_profile_type'])) {
        $profile_type_id = $order_bundle_info[$order_type_id]['shipping_profile_type'];
      }

      $shipping_profile = $this->entityTypeManager->getStorage('profile')->create([
        'type' => $profile_type_id,
        'uid' => 0,
      ]);
    }

    return $shipping_profile;
  }

  /**
   * Gets whether shipping rates can be calculated for the given profile.
   *
   * Ensures that a required shipping address is present and valid.
   *
   * @param \Drupal\profile\Entity\ProfileInterface $profile
   *   The profile.
   *
   * @return bool
   *   TRUE if shipping rates can be calculated, FALSE otherwise.
   */
  protected function canCalculateRates(ProfileInterface $profile) {
    if (!empty($this->configuration['require_shipping_profile'])) {
      $violations = $profile->get('address')->validate();
      return count($violations) === 0;
    }

    return TRUE;
  }

  /**
   * Determines whether a shipping rate is currently selected.
   *
   * @param array $pane_form
   *   The pane form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state of the parent form.
   *
   * @return bool
   *   Whether a shipping rate is currently selected.
   */
  protected function hasRateSelected(array $pane_form, FormStateInterface $form_state) {
    $user_input = NestedArray::getValue($form_state->getUserInput(), $pane_form['#parents']);

    if (empty($user_input['shipments'])) {
      return FALSE;
    }

    // Loop over the shipments input to see if a shipping rate was selected.
    foreach ($user_input['shipments'] as $values) {
      if (!empty(array_filter((array) $values['shipping_method']))) {
        return TRUE;
      }
    }

    return FALSE;
  }

}

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

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