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