You are here

ProfileFieldCopy.php in Commerce Shipping 8.2

File

src/ProfileFieldCopy.php
View source
<?php

namespace Drupal\commerce_shipping;

use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityFormInterface;
use Drupal\Core\Form\BaseFormIdInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\profile\Entity\ProfileInterface;

/**
 * Default implementation of profile field copying ("Billing same as shipping").
 *
 * Supports copying at checkout and on the admin order edit page.
 * At checkout the shipping and billing panes can be on the same step, or
 * on separate steps, assuming that the shipping pane comes first.
 *
 * Note that a billing profile can either be populated from the shipping
 * profile or from the address book, never both at the same time.
 * When profile field copying is enabled, the address book elements are hidden
 * and the address book is not populated with the billing information.
 */
class ProfileFieldCopy implements ProfileFieldCopyInterface {
  use StringTranslationTrait;

  /**
   * The current user.
   *
   * @var \Drupal\Core\Session\AccountInterface
   */
  protected $currentUser;

  /**
   * Constructs a new ProfileFieldCopy object.
   *
   * @param \Drupal\Core\Session\AccountInterface $current_user
   *   The current user.
   */
  public function __construct(AccountInterface $current_user) {
    $this->currentUser = $current_user;
  }

  /**
   * {@inheritdoc}
   */
  public function supportsForm(array &$inline_form, FormStateInterface $form_state) {
    if (!isset($inline_form['#profile_scope']) || $inline_form['#profile_scope'] != 'billing') {
      return FALSE;
    }
    $order = static::getOrder($form_state);
    if (!$order) {

      // The inline form is being used outside of an order context
      // (e.g. the payment method add/edit screen).
      return FALSE;
    }
    if (!$order
      ->hasField('shipments')) {

      // The order is not shippable.
      return FALSE;
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function alterForm(array &$inline_form, FormStateInterface $form_state) {
    $shipping_profile = static::getShippingProfile($form_state);
    if (!$shipping_profile) {

      // No source information is available.
      return;
    }
    $billing_profile = static::getBillingProfile($inline_form);
    $shipping_form_display = static::getFormDisplay($shipping_profile, 'shipping');
    $shipping_fields = array_keys($shipping_form_display
      ->getComponents());
    $user_input = (array) NestedArray::getValue($form_state
      ->getUserInput(), $inline_form['#parents']);

    // Copying is enabled by default for new billing profiles.
    $enabled = $billing_profile
      ->getData('copy_fields', $billing_profile
      ->isNew());
    if ($user_input) {
      if (isset($user_input['copy_fields'])) {
        $enabled = (bool) $user_input['copy_fields']['enable'];
      }
      elseif (isset($user_input['select_address'])) {
        $enabled = FALSE;
      }
    }
    $inline_form['copy_fields'] = [
      '#parents' => array_merge($inline_form['#parents'], [
        'copy_fields',
      ]),
      '#type' => 'container',
      '#weight' => -1000,
      '#shipping_fields' => $shipping_fields,
      '#has_form' => FALSE,
    ];
    $inline_form['copy_fields']['enable'] = [
      '#type' => 'checkbox',
      '#title' => $this
        ->getCopyLabel($inline_form),
      '#default_value' => $enabled,
      '#ajax' => [
        'callback' => [
          get_class($this),
          'ajaxRefresh',
        ],
        'wrapper' => $inline_form['#id'],
      ],
    ];
    if ($enabled) {

      // Copy over the current shipping field values, allowing widgets such as
      // TaxNumberDefaultWidget to rely on them. These values might change
      // during submit, so the profile is populated again in submitForm().
      $billing_profile
        ->populateFromProfile($shipping_profile, $shipping_fields);

      // Disable address book copying and remove all existing fields.
      $inline_form['copy_to_address_book'] = [
        '#type' => 'value',
        '#value' => FALSE,
      ];
      foreach (Element::getVisibleChildren($inline_form) as $key) {
        if (!in_array($key, [
          'copy_fields',
          'copy_to_address_book',
        ])) {
          $inline_form[$key]['#access'] = FALSE;
        }
      }

      // Add field widgets for any non-copied billing fields.
      $form_display = static::getFormDisplay($billing_profile, 'billing', $shipping_fields);
      $billing_fields = array_keys($form_display
        ->getComponents());
      if ($billing_fields) {
        $form_display
          ->buildForm($billing_profile, $inline_form['copy_fields'], $form_state);
        $inline_form['copy_fields']['#has_form'] = TRUE;
      }

      // Replace the existing validate/submit handlers with custom ones.
      foreach ($inline_form['#element_validate'] as &$validate_handler) {
        if ($validate_handler[1] == 'runValidate') {
          $validate_handler = [
            get_class($this),
            'validateForm',
          ];
          break;
        }
      }
      foreach ($inline_form['#commerce_element_submit'] as &$submit_handler) {
        if ($submit_handler[1] == 'runSubmit') {
          $submit_handler = [
            get_class($this),
            'submitForm',
          ];
          break;
        }
      }
    }
    else {
      $billing_profile
        ->unsetData('copy_fields');
    }
  }

  /**
   * Gets the copy label for the given inline form.
   *
   * @param array $inline_form
   *   The inline form.
   *
   * @return string
   *   The copy label.
   */
  protected function getCopyLabel(array $inline_form) {

    /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $plugin */
    $plugin = $inline_form['#inline_form'];
    $configuration = $plugin
      ->getConfiguration();
    $is_owner = FALSE;
    if (empty($configuration['admin'])) {
      $is_owner = $this->currentUser
        ->id() == $configuration['address_book_uid'];
    }
    if ($is_owner) {
      $copy_label = $this
        ->t('My billing information is the same as my shipping information.');
    }
    else {
      $copy_label = $this
        ->t('Billing information is the same as the shipping information.');
    }
    return $copy_label;
  }

  /**
   * Ajax callback.
   */
  public static function ajaxRefresh(array &$form, FormStateInterface $form_state) {
    $triggering_element = $form_state
      ->getTriggeringElement();
    $inline_form = NestedArray::getValue($form, array_slice($triggering_element['#array_parents'], 0, -2));
    return $inline_form;
  }

  /**
   * Validates the inline form.
   *
   * @param array $inline_form
   *   The inline form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public static function validateForm(array &$inline_form, FormStateInterface $form_state) {
    $shipping_fields = $inline_form['copy_fields']['#shipping_fields'];
    if ($inline_form['copy_fields']['#has_form']) {
      $billing_profile = static::getBillingProfile($inline_form);
      $form_display = static::getFormDisplay($billing_profile, 'billing', $shipping_fields);
      $form_display
        ->extractFormValues($billing_profile, $inline_form['copy_fields'], $form_state);
      $form_display
        ->validateFormValues($billing_profile, $inline_form['copy_fields'], $form_state);
    }
  }

  /**
   * Submits the inline form.
   *
   * @param array $inline_form
   *   The inline form.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   */
  public static function submitForm(array &$inline_form, FormStateInterface $form_state) {
    $shipping_fields = $inline_form['copy_fields']['#shipping_fields'];
    $shipping_profile = static::getShippingProfile($form_state);
    $billing_profile = static::getBillingProfile($inline_form);
    $billing_profile
      ->populateFromProfile($shipping_profile, $shipping_fields);
    if ($inline_form['copy_fields']['#has_form']) {
      $form_display = static::getFormDisplay($billing_profile, 'billing', $shipping_fields);
      $form_display
        ->extractFormValues($billing_profile, $inline_form['copy_fields'], $form_state);
    }
    $billing_profile
      ->setData('copy_fields', TRUE);
    $billing_profile
      ->unsetData('copy_to_address_book');

    // Transfer the source address book ID to ensure that the right option
    // is preselected when the copy_fields checkbox is unchecked.
    $address_book_profile_id = $shipping_profile
      ->getData('address_book_profile_id');
    if ($address_book_profile_id && $shipping_profile
      ->bundle() == $billing_profile
      ->bundle()) {
      $billing_profile
        ->setData('address_book_profile_id', $address_book_profile_id);
    }
    $billing_profile
      ->save();
  }

  /**
   * Gets the billing profile from the inline form.
   *
   * @param array $inline_form
   *   The inline form.
   *
   * @return \Drupal\profile\Entity\ProfileInterface
   *   The profile.
   */
  protected static function getBillingProfile(array &$inline_form) {

    /** @var \Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormInterface $plugin */
    $plugin = $inline_form['#inline_form'];
    $profile = $plugin
      ->getEntity();
    assert($profile instanceof ProfileInterface);
    return $profile;
  }

  /**
   * Gets the shipping profile from the parent form.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return \Drupal\profile\Entity\ProfileInterface|null
   *   The shipping profile, or NULL if not found.
   */
  protected static function getShippingProfile(FormStateInterface $form_state) {
    if ($form_state
      ->has('shipping_profile')) {

      // Shipping information on the same step as the billing information.
      $shipping_profile = $form_state
        ->get('shipping_profile');
    }
    else {
      $order = static::getOrder($form_state);
      $profiles = $order
        ->collectProfiles();
      $shipping_profile = $profiles['shipping'] ?? NULL;
    }
    return $shipping_profile;
  }

  /**
   * Gets the order from the parent form.
   *
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The current state of the form.
   *
   * @return \Drupal\commerce_order\Entity\OrderInterface|null
   *   The order, or NULL if not found (unrecognized form).
   */
  protected static function getOrder(FormStateInterface $form_state) {
    $form_object = $form_state
      ->getFormObject();
    if (!$form_object instanceof BaseFormIdInterface) {
      return NULL;
    }
    $order = NULL;
    if ($form_object instanceof EntityFormInterface) {
      $entity = $form_object
        ->getEntity();
      if ($entity
        ->getEntityTypeId() == 'commerce_order') {
        $order = $entity;
      }
    }
    elseif ($form_object
      ->getBaseFormId() == 'commerce_checkout_flow') {

      /** @var \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $form_object */
      $order = $form_object
        ->getOrder();
    }
    return $order;
  }

  /**
   * Gets the form display for the given profile and form mode.
   *
   * @param \Drupal\profile\Entity\ProfileInterface $profile
   *   The profile.
   * @param string $form_mode
   *   The form mode.
   * @param string[] $remove_fields
   *   The fields to remove.
   *
   * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface
   *   The form display.
   */
  protected static function getFormDisplay(ProfileInterface $profile, $form_mode, array $remove_fields = []) {

    // @todo Investigate a static cache for form displays, since we load the
    //   billing/shipping ones twice (once in CustomerProfile, once here).
    $form_display = EntityFormDisplay::collectRenderDisplay($profile, $form_mode);
    $form_display
      ->removeComponent('revision_log_message');
    foreach ($form_display
      ->getComponents() as $name => $component) {
      if (in_array($name, $remove_fields)) {
        $form_display
          ->removeComponent($name);
      }
    }
    return $form_display;
  }

}

Classes

Namesort descending Description
ProfileFieldCopy Default implementation of profile field copying ("Billing same as shipping").