All the examples used in the presentation can be downloaded here:
Recommended to install with Composer.
Note: Requires Drupal core >=8.6.0 (as of 2.15 this will be >=8.7.0)
php_packages_extra:
- php7.2-bcmath
- php7.2-soap
Commerce is separated into "submodules"
Commerce order:
dependencies:
- commerce:commerce
- commerce:commerce_price
- commerce:commerce_store
Drupal\[YOUR MODULE]\Plugin\Commerce\Condition
/**
* Provides the quantity condition for order items.
*
* @CommerceCondition(
* id = "order_item_quantity",
* label = @Translation("Quantity"),
* display_label = @Translation("Limit by quantity"),
* category = @Translation("Product"),
* entity_type = "commerce_order_item",
* )
*/
/**
* {@inheritdoc}
*/
public function evaluate(EntityInterface $entity) {
$this->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $entity;
$quantity = $order_item->getQuantity();
switch ($this->configuration['operator']) {
case '==':
return $quantity == $this->configuration['quantity'];
// ... more code
}
}
Drupal\[YOUR MODULE]\Plugin\Commerce\PromotionOffer
/**
* Provides the percentage off offer for order items.
*
* @CommercePromotionOffer(
* id = "order_item_percentage_off",
* label = @Translation("Percentage off each matching product"),
* entity_type = "commerce_order_item",
* )
*/
class OrderItemPercentageOff extends OrderItemPromotionOfferBase {
/**
* {@inheritdoc}
*/
public function apply(EntityInterface $entity, PromotionInterface $promotion) {
$this->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = $entity;
$adjustment_amount = $order_item->getUnitPrice()->multiply($this->getPercentage());
$adjustment_amount = $this->rounder->round($adjustment_amount);
$order_item->addAdjustment(new Adjustment([
'type' => 'promotion',
// @todo Change to label from UI when added in #2770731.
'label' => t('Discount'),
'amount' => $adjustment_amount->multiply('-1'),
'percentage' => $this->getPercentage(),
'source_id' => $promotion->id(),
]));
}
}
Drupal\[YOUR MODULE]\Plugin\Commerce\CheckoutFlow
/**
* Provides the default multistep checkout flow.
*
* @CommerceCheckoutFlow(
* id = "multistep_default",
* label = "Multistep - Default",
* )
*/
class MyFlow extends MultistepDefault {
/**
* {@inheritdoc}
*/
public function getSteps() {
$steps = parent::getSteps();
$steps['my_custom_step'] = [
'label' => $this->t('Label'),
'previous_label' => $this->t('Go back'),
'next_label' => $this->t('Continue'),
'has_sidebar' => TRUE,
];
return $steps;
}
}
Drupal\[YOUR MODULE]\Plugin\Commerce\CheckoutPane
/**
* Provides an extra checkout pane.
*
* @CommerceCheckoutPane(
* id = "my_very_cool_pane",
* label = @Translation("Pane title"),
* default_step = "review", // "_sidebar" to use in sidebar
* wrapper_element = "container", // or "fieldset"
* )
*/
class MyVeryCoolPane extends CheckoutPaneBase {
/**
* {@inheritdoc}
*/
public function buildConfigurationSummary() {
return '';
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
return $form;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {}
}
class MyVeryCoolPane extends CheckoutPaneBase {
/**
* {@inheritdoc}
*/
public function isVisible() {
if ($this->someCondition()) {
return TRUE;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function buildPaneForm(array $pane_form, FormStateInterface $form_state, array &$complete_form) {
$pane_form['element']['#markup'] = $this->t('My simple pane');
return $pane_form;
}
}
Drupal\[YOUR MODULE]\Plugin\Commerce\TaxType
/**
* @CommerceTaxType(
* id = "my_custom_vat",
* label = "My custom VAT",
* )
*/
class BelgianTaxType extends TaxTypeBase {
/**
* {@inheritdoc}
*/
public function apply(OrderInterface $order) {
foreach ($order->getItems() as $order_item) {
// Add fix 21% VAT rate for all order items.
$order_item->addAdjustment(new Adjustment([
'type' => 'tax',
'label' => 'VAT (21%)',
'amount' => $order_item->getTotalPrice()->multiply('0.21'),
'percentage' => '0.21',
'source_id' => $this->entityId . '|be|0.21',
'included' => $this->isDisplayInclusive(),
]));
}
}
}
Drupal\[YOUR MODULE]\Plugin\Commerce\PaymentGateway
/**
* Provides the cheque bancair payment gateway.
*
* @CommercePaymentGateway(
* id = "cheque_bancair",
* label = "Cheque Bancair",
* display_label = "Cheque Bancair",
* modes = {
* "n/a" = @Translation("N/A"),
* },
* forms = {
* "add-payment" = "Drupal\[YOUR MODULE]\PluginForm\ManualPaymentAddForm",
* "receive-payment" = "Drupal\[YOUR MODULE]\PluginForm\PaymentReceiveForm",
* },
* payment_type = "payment_manual",
* )
*/
Extend OffsitePaymentGatewayBase
class CustomPaymentGateway extends OffsitePaymentGatewayBase {
/**
* {@inheritdoc}
*/
public function onNotify(Request $request) {}
/**
* {@inheritdoc}
*/
public function onReturn(OrderInterface $order, Request $request) {}
/**
* {@inheritdoc}
*/
public function onCancel(OrderInterface $order, Request $request) {}
}
No, ofcourse not
/**
* Adds order processors to the PriceCalculator, grouped by adjustment type.
*/
class PriceCalculatorPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
...
foreach ($container->findTaggedServiceIds('commerce_order.order_processor') as $id => $attributes) {
...
$attribute = $attributes[0];
if (empty($attribute['adjustment_type'])) {
continue;
}
$processors[$id] = [
'priority' => isset($attribute['priority']) ? $attribute['priority'] : 0,
'adjustment_type' => $attribute['adjustment_type'],
];
}
}
Naming convention:
[YOUR_MODULE].commerce_adjustment_types.yml
Contents:
custom_adjustment:
label: 'Custom adjustment'
singular_label: 'Custom price adjustment'
plural_label: 'Custom price adjustments'
has_ui: true
weight: 200
Drupal\[YOUR MODULE]\[YOUR MODULE].services.yml
services:
your_module.order_processor:
class: '\Drupal\[YOUR MODULE]\CommerceOrderProcessor'
arguments: []
tags:
- { name: 'commerce_order.order_processor', priority: 10, adjustment_type: 'custom_adjustment' }
class CommerceOrderProcessor implements OrderProcessorInterface {
/**
* {@inheritdoc}
*/
public function process(OrderInterface $order) {
if($this->someComplicatedCondition()){
// Apply adjustment.
$order->addAdjustment(new Adjustment([
'type' => 'custom_adjustment',
'label' => 'Custom adjustment',
'amount' => new Price('2.00', 'EUR'),
]));
}
}
}
A service that provides an "answer" to a potentially complex question.
Allows you to offer different prices for the same product based on conditions.
Note: some of the conditions can be achieved through the Commerce Promotion module
Drupal\[YOUR MODULE]\[YOUR MODULE].services.yml
services:
your_module.price_resolver:
class: '\Drupal\[YOUR MODULE]\CustomPriceResolver'
tags:
- { name: 'commerce_price.price_resolver', priority: 100}
class CustomPriceResolver implements PriceResolverInterface {
/**
* {@inheritdoc}
*/
public function resolve(PurchasableEntityInterface $entity, $quantity, Context $context) {
if(1 <= $quantity && $quantity < 50){ // Quantity between 1 & 49
// Use default price.
return $entity->getPrice();
}elseif(50 <= $quantity && $quantity < 250){ // Quantity between 50 & 249
// Give a 10% discount.
return $entity->getPrice()->multiply('0.90');
}elseif($quantity >= 250){ // Quantity is 250 or higher
// Give a 15% discount.
return $entity->getPrice()->multiply('0.85');
}
// Something funky is going on. Use default price.
return $entity->getPrice();
}
}
In this case, the same could be achieved with an order processor:
class OrderProcessor implements OrderProcessorInterface {
/**
* {@inheritdoc}
*/
public function process(OrderInterface $order) {
$order_items = $order->getItems();
foreach ($order_items as $order_item) {
// Insert quantity logic here.
...
// If set to TRUE, price resolvers won't be executed anymore.
// This way you can make sure this price will be used as order item price.
$order_item->setUnitPrice($price, FALSE);
}
}
}
class CustomPriceResolver implements PriceResolverInterface {
/**
* {@inheritdoc}
*/
public function resolve(PurchasableEntityInterface $entity, $quantity, Context $context) {
if($this->account->hasRole('vip'){
// Give 5% discount.
return $entity->getPrice()->multiply('0.95');
}
// Use default price
return $entity->getPrice();
}
}
Note: This can be achieved with the Promotion module
Partially replaced hooks
const CART_EMPTY = 'commerce_cart.cart.empty';
const CART_ENTITY_ADD/UPDATE/REMOVE = 'commerce_cart.entity.add/update/remove';
const ORDER_ITEM_COMPARISON_FIELDS = 'commerce_cart.order_item.comparison_fields';
const ORDER_ASSIGN = 'commerce_order.order.assign';
const PRODUCT_VARIATION_AJAX_CHANGE = 'commerce_product.commerce_product_variation.ajax_change';
const FILTER_VARIATIONS = 'commerce_product.filter_variations';
const CUSTOMER_PROFILE = 'commerce_tax.customer_profile';
// The format for adding a state machine event to subscribe to is:
// {group}.{transition key}.pre_transition or {group}.{transition key}.post_transition
// depending on when you want to react.
$events = ['commerce_order.place.pre_transition' => 'onPreOrderPlace'];
$events = ['commerce_order.place.post_transition' => 'onOrderPlace'];
Drupal\[YOUR MODULE]\[YOUR MODULE].services.yml
services:
your_module.event_subscriber:
class: '\Drupal\[YOUR MODULE]\EventSubscriber\CommerceCartSubscriber'
tags:
- { name: 'event_subscriber'}
class CommerceCartSubscriber implements EventSubscriberInterface {
/**
* Callback for ORDER_ITEM_COMPARISON_FIELDS event.
*/
public function onOrderItemComparison(OrderItemComparisonFieldsEvent $event) {
$fields = $event->getComparisonFields(); // Fetch the existing comparison fields.
$order_item = $event->getOrderItem();
if($order_item->bundle() == 'my_special_order_item_type'){
// This order item can only be combined if my extra custom field is the same.
// Add extra field to the comparison array.
$fields[] = 'field_order_item_custom';
}
$event->setComparisonFields($fields);
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[CartEvents::ORDER_ITEM_COMPARISON_FIELDS][] = ['onOrderItemComparison'];
return $events;
}
}
Introduced the concept of a workflow based on the State machine
Drupal\[YOUR MODULE]\[YOUR MODULE].workflows.yml
order_with_backlog:
id: order_with_backlog
group: commerce_order
label: 'Default, with backlog functionality'
states:
draft:
label: Draft
backlog:
label: Backlog
completed:
label: Completed
canceled:
label: Canceled
transitions:
place:
label: 'Place order'
from: [draft]
to: completed
in_backlog:
label: 'In backlog'
from: [draft]
to: backlog
backlog_to_order:
label: 'Backlog to order'
from: [backlog]
to: draft
cancel:
label: 'Cancel order'
from: [draft]
to: canceled
Thank you for your attention, are there any questions?