Custom checkout processes with Drupal Commerce 2
Sascha Grossenbacher
https://slides.com/saschagrossenbacher/commerce
1. Start Checkout
2. Checkout
3. Processing
1. Start Checkout
Products with Cart
- Default Behavior of Commerce 2
- Product is added to cart, message is shown
- User goes to cart, confirms again and starts checkout
- Useful when users might want to buy multiple products
Products without Cart
- From Product (variant) directly to checkout
- #2810723 Allow order types to have no carts
- Useful when users will not buy more than one product, e.g. subscriptions and memberships
Checkout without products
- For custom processes like donations, dynamic membership/subscription fees or buying products without a product page
- Requires custom code
Code examples
Step 1: Create Order Item
// With a product:
$order_item = OrderItem::create([
'type' => 'default',
'purchased_entity' => $commerce_product->getDefaultVariation()->id(),
'quantity' => 2,
]);
// Arbitrary amount and title
order_item = OrderItem::create([
'type' => 'custom_item_type',
'title' => 'Subscription fee',
'unit_price' => [
'number' => 50,
'currency_code' => 'CHF',
],
'quantity' => 1,
''
]);
Step 2: Create Order And start checkout
$store = \Drupal::service('commerce_store.current_store')->getStore();
$order = Order::create([
'type' => 'default',
'mail' => $this->currentUser()->getEmail(),
'uid' => $this->currentUser()->id(),
'ip_address' => \Drupal::request()->getClientIp(),
'store_id' => $store->id(),
'order_items' => [$order_item],
'cart' => FALSE,
]);
$order->save();
// In a controller.
return $this->redirect('commerce_checkout.form', ['commerce_order' => $order->id()]);
// In a form submit callback.
$form_state->setRedirect('commerce_checkout.form', ['commerce_order' => $order->id()]);
Anonymous Checkout
$cart_session = \Drupal::service('commerce_cart.cart_session');
$cart_session->addCartId($order->id(), CartSessionInterface::COMPLETED);
- Anonymous users require cart session to access order
- Add order as a completed cart, to
allow checkout access without showing up as a cart
- Add order as a completed cart, to
2. Checkout customization
Types & Plugins
- Almost every concept in Commerce 2 can have multiple types/variants
- Products and Variants
- Orders and Order Items
- Checkout flows
- Shops and Shop Types
- Allows for different configuration and custom code
to act on certain types/plugins
Connections
- The Product Type defines the Variation Type
- A Variation Type defines the Order Item Type
- The Order Item Type defines the Order Type
- The Order Type defines the Checkout Flow
- A Checkout Flow has a Checkout Flow Plugin
Connections #2
- A Checkout Flow Plugin defines the available Checkout Steps (Pages)
- Display is up to the Plugin, must not be an actual page, e.g. Sidebar Step
- The Checkout Flow entity assigns Panes to Steps and stores their configuration
- Checkout Panes are plugins
Customization examples
Customize Checkout steps
/**
* @CommerceCheckoutFlow(
* id = "custom_multistep",
* label = "Multistep - Custom",
* )
*/
class CustomMultiStep extends MultistepDefault {
/**
* {@inheritdoc}
*/
public function getSteps() {
$steps = parent::getSteps();
$steps['complete']['label'] = $this->t('Thanks for subscribing!');
return $steps;
}
// Alternatively switch out the default class with yours.
function custom_commerce_checkout_flow_info_alter(&$definitions) {
$definitions['multistep_default']['class'] = MultiStepCustom::class;
}
Custom Checkout Panes
/**
* @CommerceCheckoutPane(
* id = "points_payment",
* label = @Translation("Points payment"),
* default_step = "_sidebar",
* wrapper_element = "container",
* )
*/
class PointsPayment extends CheckoutPaneBase {
/**
* {@inheritdoc}
*/
public function buildPaneForm(array $pane_form,
FormStateInterface $form_state, array &$complete_form) {
// ...
return $pane_form;
}
}
Define custom processes or sublcass and repace default panes to customize them
Hide most blocks during checkout
function custom_block_access(Block $block, $operation, AccountInterface $account) {
$route_match = \Drupal::routeMatch();
if ($route_match->getRouteName() == 'commerce_checkout.form'
&& $route_match->getParameter('commerce_order') instanceof OrderInterface) {
$order = $route_match->getParameter('commerce_order');
// Hide blocks in certain regions during checkout (show on complete page).
if ($order->getState()->getId() == 'draft') {
$config = \Drupal::configFactory()->get('custom.settings');
$regions = $config->get('checkout_empty_regions');
$whitelisted_blocks = $config->get('checkout_whitelisted_blocks');
if (in_array($block->getRegion(), $regions)
&& !in_array($block->id(), $whitelisted_blocks)) {
return AccessResult::forbidden()->addCacheContexts(['url.path']);
}
}
}
}
}
3. Processing
React to paid orders
- Event Subscriber on OrderEvents::ORDER_PAID
- Requires payment gateway that process a payment
transaction and confirm it as paid.
- Requires payment gateway that process a payment
- Recommendation: Use a queue for slow operations
Using a workflow
- Use a Workflow state to handle more complex processes. Can be a mix of manual and automated steps.
- Commerce uses the State Machine project, workflows are defined in a yourmodule.workflows.yml file
- Events can then listen to state changes, do something and update the state
public function enqueueOrder(WorkflowTransitionEvent $transition_event) {
$order = $transition_event->getEntity();
if ($order instanceof OrderInterface &&
$order->getState()->getWorkflow()->getId() == 'order_aboware'
&& $transition_event->getTransition()->getToState()->getId() == 'needs_sync') {
Examples
Questions?
Commerce Customization
By Sascha Grossenbacher
Commerce Customization
- 2,301