Skip to main content
Drupal Commerce: Customizing the checkout process

The Drupal Commerce checkout process is compatible with most e-commerce businesses. One of our clients needs us to implement a checkout process that is quite different from the default one. 

Features include: : 

  • Offering the option to register for unregistered users.
  • Adding specific forms during the checkout process.
  • Modify the value of the submission buttons.

The checkout process consists of several steps. By default, there are five steps:

  • Login
  • Order information
  • review
  • payment
  • complete

See : commerce/modules/checkout/src/Plugin/Commerce/CheckoutFlow/MultistepDefault.php
 

Each step consists of one or more panes. For example, contact information is a pane.

<?php
namespace Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane;
use Drupal\Core\Form\FormStateInterface;
/**
 * Provides the contact information pane.
 *
 * @CommerceCheckoutPane(
 *   id = "contact_information",
 *   label = @Translation("Contact information"),
 *   default_step = "order_information",
 *   wrapper_element = "fieldset",
 * )
 */
class ContactInformation extends CheckoutPaneBase implements CheckoutPaneInterface {
...

We will create a pane in order to add the necessary code. The basic code for our pane looks like this:

<?php
namespace Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane;
use Drupal\Core\Form\FormStateInterface;
/**
 * Provides the contact information pane.
 *
 * @CommerceCheckoutPane(
 *   id = "custom_information",
 *   label = @Translation("Custom information"),
 *   default_step = "order_information",
 *   wrapper_element = "fieldset",
 * )
 */
class CustomInformation extends CheckoutPaneBase implements CheckoutPaneInterface {  
  /**
   *
   * {@inheritdoc}
   * @see \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface::buildPaneForm()
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array $complete_form) {
    return $pane_form;
  }
}

The buildPaneForm method will contain the additional form.
By default, it will be available at the "order_information" stage.
 

Adding a field to the Checkout Pane

We can improve our pane by adding a text area field:

  /**
   *
   * {@inheritdoc}
   * @see \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface::buildPaneForm()
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
    $pane_form['description_need'] = [
      "#type" => 'text_format',
      '#format' => 'basic_html',
      '#title' => $this->t('Describe your need'),
      '#default_value' => '',
      '#required' => true,
      '#description' => $this->t("Please bring out the necessary characteristics and information.")
    ];
    return $pane_form;
  }
  
 /**
   *
   * {@inheritdoc}
   */
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $values = $form_state->getValue($pane_form['#parents']);
    if (!empty($values['description_need']['value']) && \strlen($values['description_need']['value']) < 10) {
      $form_state->setError($pane_form, $this->t('Your description is short'));
    }
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $values = $form_state->getValue($pane_form['#parents']);
    // $values['description_need']
  }

We have added a validator (validatePaneForm) and a save method (submitPaneForm).


At this stage, we already have a rendering:



We want to save this information in the order, so we will add a field to our order via the module file and hook_entity_base_field_info.
 

/**
 * Implement hook_entity_base_field_info
 *
 * @param EntityTypeInterface $entity_type
 */
function [MODULE_NAME]_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = [];
  if ($entity_type->id() == 'commerce_order') {
    $fields['hbk_description_need'] = BaseFieldDefinition::create('text_long')->setLabel(" Description need ")->setDisplayOptions('form', [])->setDisplayOptions('view',
      [
        'label' => 'hidden',
        'type' => 'text_default',
        'weight' => 0
      ])->setDisplayConfigurable('view', TRUE)->setDisplayConfigurable('form', true);
  }
  return $fields;
}

Next, you need to apply these changes to the database. There are different ways to do this, but we opt for a drush entup command: 

vendor/bin/drush entup

We are updating our pane:

<?php
namespace Drupal\hbk_souscription_pfna\Plugin\Commerce\CheckoutPane;
use Drupal\Core\Form\FormStateInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface;
/**
 * Provides the contact information pane.
 *
 * @CommerceCheckoutPane(
 *   id = "custom_information",
 *   label = @Translation("Custom information"),
 *   default_step = "order_information",
 *   wrapper_element = "fieldset",
 * )
 */
class CustomInformation extends CheckoutPaneBase implements CheckoutPaneInterface {
  
  /**
   *
   * {@inheritdoc}
   * @see \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface::buildPaneForm()
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
    $pane_form['description_need'] = [
      "#type" => 'text_format',
      '#format' => 'basic_html',
      '#title' => $this->t('Describe your need'),
      '#default_value' => $this->order->get('hbk_description_need')->value,
      '#required' => true,
      '#description' => $this->t("Please bring out the necessary characteristics and information.")
    ];
    return $pane_form;
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $values = $form_state->getValue($pane_form['#parents']);
    if (!empty($values['description_need']['value']) && \strlen($values['description_need']['value']) < 10) {
      $form_state->setError($pane_form, $this->t('Your description is short'));
    }
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $values = $form_state->getValue($pane_form['#parents']);
    $this->order->set('hbk_description_need', $values['description_need']['value']);
  }
}

The approach we have just outlined is suitable if you have few fields, but if you have a large number of fields, you should use a profile.
 

Adding a profile in the Checkout Pane

Adding a profile follows the same logic as adding a field, but there are some important differences because there are two forms to manage: the default form (checkout) and the profile form.

Definition of requirements:
We want to add an additional form using a profile. The user fills it out once and can then update their information.
 

/**
 * Implement hook_entity_base_field_info
 *
 * @param EntityTypeInterface $entity_type
 */
function hbk_souscription_pfna_entity_base_field_info(EntityTypeInterface $entity_type) {
  $fields = [];
  if ($entity_type->id() == 'commerce_order') {
    $fields['hbk_description_need'] = BaseFieldDefinition::create('text_long')->setLabel(" Description need ")->setDisplayOptions('form', [])->setDisplayOptions('view',
      [
        'label' => 'hidden',
        'type' => 'text_default',
        'weight' => 0
      ])->setDisplayConfigurable('view', TRUE)->setDisplayConfigurable('form', true);
    $fields['hbk_custom_profile'] = BaseFieldDefinition::create('entity_reference')->setLabel(t('Custom profile'))->setSetting('target_type', 'profile')->setSetting('handler', 'default')->setDisplayOptions(
      'form', [
        'type' => 'entity_reference_autocomplete',
        'weight' => 5,
        'settings' => [
          'match_operator' => 'CONTAINS',
          'size' => '60',
          'autocomplete_type' => 'tags',
          'placeholder' => ''
        ]
      ])->setDisplayConfigurable('form', TRUE)->setDisplayConfigurable('view', TRUE)->setCardinality(1);
  }
  return $fields;

We are adding a new field “hbk_custom_profile” to the commerce entity. This field will contain the profile ID.
 

<?php
namespace Drupal\hbk_souscription_pfna\Plugin\Commerce\InlineForm;
use Drupal\commerce\Plugin\Commerce\InlineForm\EntityInlineFormBase;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\profile\Entity\ProfileInterface;
/**
 * Provides an inline form for managing a customer profile.
 *
 * Allows copying values to and from the customer's address book.
 *
 * Supports two modes, based on the profile type setting:
 * - Single: The customer can have only a single profile of this type.
 * - Multiple: The customer can have multiple profiles of this type.
 *
 * @CommerceInlineForm(
 *   id = "hbk_custom_profile",
 *   label = @Translation("Hbk custom profile"),
 * )
 */
class HbkCustomerProfile extends EntityInlineFormBase {
  
  /**
   *
   * {@inheritdoc}
   */
  public function defaultConfiguration() {
    return [
      'form_mode' => 'default',
      'skip_save' => FALSE
    ];
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function buildInlineForm(array $inline_form, FormStateInterface $form_state) {
    $inline_form = parent::buildInlineForm($inline_form, $form_state);
    assert($this->entity instanceof ProfileInterface);
    
    $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->configuration['form_mode']);
    $form_display->buildForm($this->entity, $inline_form, $form_state);
    
    return $inline_form;
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function submitInlineForm(array &$inline_form, FormStateInterface $form_state) {
    parent::submitInlineForm($inline_form, $form_state);
    assert($this->entity instanceof ProfileInterface);
    
    $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->configuration['form_mode']);
    $form_display->extractFormValues($this->entity, $inline_form, $form_state);
    
    if (empty($this->configuration['skip_save'])) {
      $this->entity->save();
    }
  }
}

We create the CommerceInlineForm plugin to render the profile in the CheckoutPane.
 

$customProfile = null;
    // On recupere le custom profile, s'il existe.
    $id_profile = $this->order->get("hbk_custom_profile")->target_id;
    if ($id_profile) {
      $this->messenger()->addStatus("Le profile est deja rataché à la commande ");
      $customProfile = $this->entityTypeManager->getStorage("profile")->load($id_profile);
    }
    if (!$customProfile) {
      
      $profile_storage = $this->entityTypeManager->getStorage("profile");
      // On verifie si profile existe deja.
      $customProfiles = $this->entityTypeManager->getStorage("profile")->loadByProperties([
        'uid' => \Drupal::currentUser()->id(),
        'type' => 'informations_personnelles'
      ]);
      if ($customProfiles) {
        $this->messenger()->addStatus("Le profile a deja été creer ");
        $customProfile = reset($customProfiles);
      }
      else {
        $this->messenger()->addStatus(" Creation d'un nouveau profile ");
        $customProfile = $profile_storage->create([
          'type' => 'informations_personnelles',
          'uid' => \Drupal::currentUser()->id()
        ]);
      }
    }
    /**
     *
     * @var \Drupal\hbk_souscription_pfna\Plugin\Commerce\InlineForm\HbkCustomerProfile $inline_form
     */
    $inline_form = $this->inlineFormManager->createInstance('hbk_custom_profile', [
      'form_mode' => 'default',
      'skip_save' => FALSE
    ], $customProfile);
    
    $pane_form['custom_profile'] = [
      '#parents' => array_merge($pane_form['#parents'], [
        'profile'
      ]),
      '#inline_form' => $inline_form
    ];
    $pane_form['custom_profile'] = $inline_form->buildInlineForm($pane_form['custom_profile'], $form_state);
    

We add an entry to the form in order to render the profile.

  /**
   *
   * {@inheritdoc}
   */
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    /** @var \Drupal\hbk_souscription_pfna\Plugin\Commerce\InlineForm\HbkCustomerProfile $inline_form */
    $inline_form = $pane_form['custom_profile']['#inline_form'];
    // on met à jour l'entité profile.
    $inline_form->submitInlineForm($pane_form['custom_profile'], $form_state);
    /**
     *
     * @var \Drupal\profile\Entity\Profile $profile
     */
    $profile = $inline_form->getEntity();
    //
    $values = $form_state->getValue($pane_form['#parents']);
    $this->order->set('hbk_description_need', $values['description_need']['value']);
    $this->order->set('hbk_custom_profile', $profile->id());
  }

We are updating the backup.

The final code for our CheckoutPane:

<?php
namespace Drupal\hbk_souscription_pfna\Plugin\Commerce\CheckoutPane;
use Drupal\Core\Form\FormStateInterface;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneBase;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\commerce\InlineFormManager;
use Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
/**
 * Provides the contact information pane.
 *
 * @CommerceCheckoutPane(
 *   id = "custom_information",
 *   label = @Translation("Custom information"),
 *   default_step = "order_information",
 *   wrapper_element = "fieldset",
 * )
 */
class CustomInformation extends CheckoutPaneBase implements CheckoutPaneInterface {
  
  /**
   * The inline form manager.
   *
   * @var \Drupal\commerce\InlineFormManager
   */
  protected $inlineFormManager;
  
  /**
   * Constructs a new BillingInformation object.
   *
   * @param array $configuration
   *        A configuration array containing information about the plugin
   *        instance.
   * @param string $plugin_id
   *        The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *        The plugin implementation definition.
   * @param \Drupal\commerce_checkout\Plugin\Commerce\CheckoutFlow\CheckoutFlowInterface $checkout_flow
   *        The parent checkout flow.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *        The entity type manager.
   * @param \Drupal\commerce\InlineFormManager $inline_form_manager
   *        The inline form manager.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow, EntityTypeManagerInterface $entity_type_manager, InlineFormManager $inline_form_manager) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $checkout_flow, $entity_type_manager);
    
    $this->inlineFormManager = $inline_form_manager;
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, CheckoutFlowInterface $checkout_flow = NULL) {
    return new static($configuration, $plugin_id, $plugin_definition, $checkout_flow, $container->get('entity_type.manager'), $container->get('plugin.manager.commerce_inline_form'));
  }
  
  /**
   *
   * {@inheritdoc}
   * @see \Drupal\commerce_checkout\Plugin\Commerce\CheckoutPane\CheckoutPaneInterface::buildPaneForm()
   */
  public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
    $pane_form['description_need'] = [
      "#type" => 'text_format',
      '#format' => 'basic_html',
      '#title' => $this->t('Describe your need'),
      '#default_value' => $this->order->get('hbk_description_need')->value,
      '#required' => true,
      '#description' => $this->t("Please bring out the necessary characteristics and information.")
    ];
    $customProfile = null;
    // On recupere le custom profile, s'il existe.
    $id_profile = $this->order->get("hbk_custom_profile")->target_id;
    if ($id_profile) {
      $customProfile = $this->entityTypeManager->getStorage("profile")->load($id_profile);
    }
    if (!$customProfile) {
      
      $profile_storage = $this->entityTypeManager->getStorage("profile");
      // On verifie si profile existe deja.
      $customProfiles = $this->entityTypeManager->getStorage("profile")->loadByProperties([
        'uid' => \Drupal::currentUser()->id(),
        'type' => 'informations_personnelles'
      ]);
      if ($customProfiles) {
        $customProfile = reset($customProfiles);
      }
      else {
        $customProfile = $profile_storage->create([
          'type' => 'informations_personnelles',
          'uid' => \Drupal::currentUser()->id()
        ]);
      }
    }
    /**
     *
     * @var \Drupal\hbk_souscription_pfna\Plugin\Commerce\InlineForm\HbkCustomerProfile $inline_form
     */
    $inline_form = $this->inlineFormManager->createInstance('hbk_custom_profile', [
      'form_mode' => 'default',
      'skip_save' => FALSE
    ], $customProfile);
    
    $pane_form['custom_profile'] = [
      '#parents' => array_merge($pane_form['#parents'], [
        'profile'
      ]),
      '#inline_form' => $inline_form
    ];
    $pane_form['custom_profile'] = $inline_form->buildInlineForm($pane_form['custom_profile'], $form_state);
    
    return $pane_form;
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function validatePaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    $values = $form_state->getValue($pane_form['#parents']);
    if (!empty($values['description_need']['value']) && \strlen($values['description_need']['value']) < 10) {
      $form_state->setError($pane_form, $this->t('Your description is short'));
    }
  }
  
  /**
   *
   * {@inheritdoc}
   */
  public function submitPaneForm(array &$pane_form, FormStateInterface $form_state, array &$complete_form) {
    /** @var \Drupal\hbk_souscription_pfna\Plugin\Commerce\InlineForm\HbkCustomerProfile $inline_form */
    $inline_form = $pane_form['custom_profile']['#inline_form'];
    // on met à jour l'entité profile.
    $inline_form->submitInlineForm($pane_form['custom_profile'], $form_state);
    /**
     *
     * @var \Drupal\profile\Entity\Profile $profile
     */
    $profile = $inline_form->getEntity();
    //
    $values = $form_state->getValue($pane_form['#parents']);
    $this->order->set('hbk_description_need', $values['description_need']['value']);
    $this->order->set('hbk_custom_profile', $profile->id());
  }
}

Elsewhere on the web:
https://www.adimeo.com/blog-technique/checkout-panes-custom-drupal-commerce

Profile picture for user admin Stephane K

Écrit le

Il y'a 1 year
Modifié
Il y'a 2 weeks
Loading ...
WhatsApp
Support Habeuk : +237 694 900 622
WhatsApp Send