You are here

class ProfileFieldCopy in Commerce Shipping 8.2

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.

Hierarchy

Expanded class hierarchy of ProfileFieldCopy

1 string reference to 'ProfileFieldCopy'
commerce_shipping.services.yml in ./commerce_shipping.services.yml
commerce_shipping.services.yml
1 service uses ProfileFieldCopy
commerce_shipping.profile_field_copy in ./commerce_shipping.services.yml
Drupal\commerce_shipping\ProfileFieldCopy

File

src/ProfileFieldCopy.php, line 27

Namespace

Drupal\commerce_shipping
View source
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;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
ProfileFieldCopy::$currentUser protected property The current user.
ProfileFieldCopy::ajaxRefresh public static function Ajax callback.
ProfileFieldCopy::alterForm public function Alters the inline form. Overrides ProfileFieldCopyInterface::alterForm
ProfileFieldCopy::getBillingProfile protected static function Gets the billing profile from the inline form.
ProfileFieldCopy::getCopyLabel protected function Gets the copy label for the given inline form.
ProfileFieldCopy::getFormDisplay protected static function Gets the form display for the given profile and form mode.
ProfileFieldCopy::getOrder protected static function Gets the order from the parent form.
ProfileFieldCopy::getShippingProfile protected static function Gets the shipping profile from the parent form.
ProfileFieldCopy::submitForm public static function Submits the inline form.
ProfileFieldCopy::supportsForm public function Gets whether the given inline form is supported. Overrides ProfileFieldCopyInterface::supportsForm
ProfileFieldCopy::validateForm public static function Validates the inline form.
ProfileFieldCopy::__construct public function Constructs a new ProfileFieldCopy object.
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.