class BuyXGetY in Commerce Core 8.2
Provides the "Buy X Get Y" offer for orders.
Examples:
- "Buy 1 t-shirt, get 1 hat for $10 less"
- "Buy 3 t-shirts, get 2 t-shirts free (100% off)"
The cheapest items are always discounted first. The offer applies multiple times ("Buy 3 Get 1" will discount 2 items when 6 are bought).
Decimal quantities are supported.
Plugin annotation
@CommercePromotionOffer(
id = "order_buy_x_get_y",
label = @Translation("Buy X Get Y"),
entity_type = "commerce_order",
)
Hierarchy
- class \Drupal\Component\Plugin\PluginBase implements DerivativeInspectionInterface, PluginInspectionInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
- class \Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferBase implements PromotionOfferInterface, ContainerFactoryPluginInterface
- class \Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\OrderPromotionOfferBase implements OrderPromotionOfferInterface
- class \Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\BuyXGetY
- class \Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\OrderPromotionOfferBase implements OrderPromotionOfferInterface
- class \Drupal\commerce_promotion\Plugin\Commerce\PromotionOffer\PromotionOfferBase implements PromotionOfferInterface, ContainerFactoryPluginInterface
- class \Drupal\Core\Plugin\PluginBase uses DependencySerializationTrait, MessengerTrait, StringTranslationTrait
Expanded class hierarchy of BuyXGetY
File
- modules/
promotion/ src/ Plugin/ Commerce/ PromotionOffer/ BuyXGetY.php, line 43
Namespace
Drupal\commerce_promotion\Plugin\Commerce\PromotionOfferView source
class BuyXGetY extends OrderPromotionOfferBase {
/**
* The condition manager.
*
* @var \Drupal\commerce\ConditionManagerInterface
*/
protected $conditionManager;
/**
* The chain base price resolver.
*
* @var \Drupal\commerce_price\Resolver\ChainPriceResolverInterface
*/
protected $chainPriceResolver;
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new BuyXGetY object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The pluginId for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\commerce_price\RounderInterface $rounder
* The rounder.
* @param \Drupal\commerce_order\PriceSplitterInterface $splitter
* The splitter.
* @param \Drupal\commerce\ConditionManagerInterface $condition_manager
* The condition manager.
* @param \Drupal\commerce_price\Resolver\ChainPriceResolverInterface $chain_price_resolver
* The chain price resolver.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RounderInterface $rounder, PriceSplitterInterface $splitter, ConditionManagerInterface $condition_manager, ChainPriceResolverInterface $chain_price_resolver, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $rounder, $splitter);
$this->conditionManager = $condition_manager;
$this->chainPriceResolver = $chain_price_resolver;
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container
->get('commerce_price.rounder'), $container
->get('commerce_order.price_splitter'), $container
->get('plugin.manager.commerce_condition'), $container
->get('commerce_price.chain_price_resolver'), $container
->get('entity_type.manager'));
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'buy_quantity' => 1,
'buy_conditions' => [],
'get_quantity' => 1,
'get_conditions' => [],
'get_auto_add' => FALSE,
'offer_type' => 'percentage',
'offer_percentage' => '0',
'offer_amount' => NULL,
'offer_limit' => '0',
] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form += parent::buildConfigurationForm($form, $form_state);
// Remove the main fieldset.
$form['#type'] = 'container';
$form['buy'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Customer buys'),
'#collapsible' => FALSE,
];
$form['buy']['quantity'] = [
'#type' => 'commerce_number',
'#title' => $this
->t('Quantity'),
'#default_value' => $this->configuration['buy_quantity'],
'#min' => 1,
];
$form['buy']['conditions'] = [
'#type' => 'commerce_conditions',
'#title' => $this
->t('Matching any of the following'),
'#parent_entity_type' => 'commerce_promotion',
'#entity_types' => [
'commerce_order_item',
],
'#default_value' => $this->configuration['buy_conditions'],
];
$form['get'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Customer gets'),
'#collapsible' => FALSE,
];
$form['get']['quantity'] = [
'#type' => 'commerce_number',
'#title' => $this
->t('Quantity'),
'#default_value' => $this->configuration['get_quantity'],
'#min' => 1,
];
$form['get']['conditions'] = [
'#type' => 'commerce_conditions',
'#title' => $this
->t('Matching any of the following'),
'#parent_entity_type' => 'commerce_promotion',
'#entity_types' => [
'commerce_order_item',
],
'#default_value' => $this->configuration['get_conditions'],
];
$states = $this
->getAutoAddStatesVisibility();
$form['get']['auto_add_help'] = [
'#type' => 'item',
'#title' => $this
->t('Behavior'),
'#states' => [
'visible' => [
$states,
],
],
];
$form['get']['auto_add'] = [
'#type' => 'checkbox',
'#title' => $this
->t("Automatically add the offer product to the cart if it isn't in it already"),
'#description' => $this
->t('This behavior will only work when a single product variation (or a single product with only one variation) is specified.'),
'#default_value' => $this->configuration['get_auto_add'],
'#states' => [
'visible' => [
$states,
],
],
];
$parents = array_merge($form['#parents'], [
'offer',
'type',
]);
$selected_offer_type = NestedArray::getValue($form_state
->getUserInput(), $parents);
$selected_offer_type = $selected_offer_type ?: $this->configuration['offer_type'];
$offer_wrapper = Html::getUniqueId('buy-x-get-y-offer-wrapper');
$form['offer'] = [
'#type' => 'fieldset',
'#title' => $this
->t('At a discounted value'),
'#collapsible' => FALSE,
'#prefix' => '<div id="' . $offer_wrapper . '">',
'#suffix' => '</div>',
];
$form['offer']['type'] = [
'#type' => 'radios',
'#title' => $this
->t('Discounted by a'),
'#title_display' => 'invisible',
'#options' => [
'percentage' => $this
->t('Percentage'),
'fixed_amount' => $this
->t('Fixed amount'),
],
'#default_value' => $selected_offer_type,
'#ajax' => [
'callback' => [
get_called_class(),
'ajaxRefresh',
],
'wrapper' => $offer_wrapper,
],
];
if ($selected_offer_type == 'percentage') {
$form['offer']['percentage'] = [
'#type' => 'commerce_number',
'#title' => $this
->t('Percentage off'),
'#default_value' => Calculator::multiply($this->configuration['offer_percentage'], '100'),
'#maxlength' => 255,
'#min' => 0,
'#max' => 100,
'#size' => 4,
'#field_suffix' => $this
->t('%'),
'#required' => TRUE,
];
}
else {
$form['offer']['amount'] = [
'#type' => 'commerce_price',
'#title' => $this
->t('Amount off'),
'#default_value' => $this->configuration['offer_amount'],
'#required' => TRUE,
];
}
$form['limit'] = [
'#type' => 'fieldset',
'#title' => $this
->t('Offer limit'),
'#description' => $this
->t('The number of times this offer can apply to the same order.'),
'#collapsible' => FALSE,
];
$form['limit']['amount'] = [
'#type' => 'radios',
'#title' => $this
->t('Limited to'),
'#title_display' => 'invisible',
'#options' => [
0 => $this
->t('Unlimited'),
1 => $this
->t('Limited number of uses'),
],
'#default_value' => $this->configuration['offer_limit'] ? 1 : 0,
];
$form['limit']['offer_limit'] = [
'#type' => 'number',
'#title' => $this
->t('Number of uses'),
'#title_display' => 'invisible',
'#default_value' => $this->configuration['offer_limit'] ?: 1,
'#states' => [
'invisible' => [
':input[name="offer[0][target_plugin_configuration][order_buy_x_get_y][limit][amount]"]' => [
'value' => 0,
],
],
],
];
return $form;
}
/**
* Ajax callback.
*/
public static function ajaxRefresh(array $form, FormStateInterface $form_state) {
$parents = $form_state
->getTriggeringElement()['#array_parents'];
$parents = array_slice($parents, 0, -2);
return NestedArray::getValue($form, $parents);
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::validateConfigurationForm($form, $form_state);
$values = $form_state
->getValue($form['#parents']);
if ($values['offer']['type'] == 'percentage' && empty($values['offer']['percentage'])) {
$form_state
->setError($form, $this
->t('Percentage must be a positive number.'));
}
if ($values['get']['auto_add']) {
// Ensure that at least one compatible condition enabled.
$valid_condition_ids = array_keys($this
->getPurchasableEntityConditions());
$has_valid_condition = FALSE;
foreach ($values['get']['conditions']['products'] as $condition_id => $condition_values) {
if (in_array($condition_id, $valid_condition_ids) && (bool) $condition_values['enable']) {
$has_valid_condition = TRUE;
break;
}
}
// We can't automatically add the "get" product if no valid conditions are
// selected.
if (!$has_valid_condition) {
$values['get']['auto_add'] = FALSE;
$form_state
->setValue($form['#parents'], $values);
return;
}
// Ensure that the offer is a 100% discount.
if ($values['offer']['type'] === 'fixed_amount' || $values['offer']['type'] === 'percentage' && $values['offer']['percentage'] != '100') {
$form_state
->setError($form['offer'], $this
->t('The "auto-add" offer can only be enabled for fully discounted products (i.e with a 100% discount).'));
}
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
parent::submitConfigurationForm($form, $form_state);
if (!$form_state
->getErrors()) {
$values = $form_state
->getValue($form['#parents']);
$this->configuration['buy_quantity'] = $values['buy']['quantity'];
$this->configuration['buy_conditions'] = $values['buy']['conditions'];
$this->configuration['get_quantity'] = $values['get']['quantity'];
$this->configuration['get_conditions'] = $values['get']['conditions'];
$this->configuration['get_auto_add'] = $values['get']['auto_add'];
$this->configuration['offer_type'] = $values['offer']['type'];
if ($this->configuration['offer_type'] == 'percentage') {
$this->configuration['offer_percentage'] = Calculator::divide((string) $values['offer']['percentage'], '100');
$this->configuration['offer_amount'] = NULL;
}
else {
$this->configuration['offer_percentage'] = NULL;
$this->configuration['offer_amount'] = $values['offer']['amount'];
}
$this->configuration['offer_limit'] = $values['limit']['amount'] == 0 ? 0 : $values['limit']['offer_limit'];
}
}
/**
* {@inheritdoc}
*/
public function apply(EntityInterface $entity, PromotionInterface $promotion) {
$this
->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $entity;
$order_items = $order
->getItems();
$buy_conditions = $this
->buildConditionGroup($this->configuration['buy_conditions']);
$buy_order_items = $this
->selectOrderItems($order_items, $buy_conditions, 'DESC');
$buy_quantities = array_map(function (OrderItemInterface $order_item) {
return $order_item
->getQuantity();
}, $buy_order_items);
if (array_sum($buy_quantities) < $this->configuration['buy_quantity']) {
return;
}
$get_conditions = $this
->buildConditionGroup($this->configuration['get_conditions']);
if ($this->configuration['get_auto_add'] && ($get_purchasable_entity = $this
->findSinglePurchasableEntity($get_conditions))) {
$order_item = $this
->findOrCreateOrderItem($get_purchasable_entity, $order_items);
$expected_get_quantity = $this
->calculateExpectedGetQuantity($buy_quantities, $order_item);
// If the expected get quantity is non-zero, we need to update the
// quantity of the 'get' order item accordingly.
if (Calculator::compare($expected_get_quantity, '0') !== 0) {
if (Calculator::compare($order_item
->getQuantity(), $expected_get_quantity) === -1) {
$order_item
->setQuantity($expected_get_quantity);
// Ensure that order items which are 'touched' by this promotion can
// not be edited by the customer, either by changing their quantity or
// removing them from the cart.
$order_item
->lock();
}
// Keep track of the quantity that was auto-added to this order item so
// we can subtract it (or remove the order item completely) if the buy
// conditions are no longer satisfied on the next order refresh.
$order_item
->setData("promotion:{$promotion->id()}:auto_add_quantity", $expected_get_quantity);
$time = $order
->getCalculationDate()
->format('U');
$context = new Context($order
->getCustomer(), $order
->getStore(), $time);
$unit_price = $this->chainPriceResolver
->resolve($get_purchasable_entity, $order_item
->getQuantity(), $context);
$order_item
->setUnitPrice($unit_price);
if ($order_item
->isNew()) {
$order_item
->set('order_id', $order
->id());
$order_item
->save();
$order
->addItem($order_item);
$order_items = $order
->getItems();
}
}
}
$get_order_items = $this
->selectOrderItems($order_items, $get_conditions, 'ASC');
$get_quantities = array_map(function (OrderItemInterface $order_item) {
return $order_item
->getQuantity();
}, $get_order_items);
if (empty($get_quantities)) {
return;
}
// It is possible for $buy_quantities and $get_quantities to overlap (have
// the same order item IDs). For example, in a "Buy 3 Get 1" scenario with
// a single T-shirt order item of quantity: 8, there are 6 bought and 2
// discounted products, in this order: 3, 1, 3, 1. To ensure the specified
// results, $buy_quantities must be processed group by group, with any
// overlaps immediately removed from the $get_quantities (and vice-versa).
// Additionally, ensure that any items from $buy_quantities that overlap
// with $get_quantities are processed last, in order to accommodate the case
// when $buy_conditions (or the lack thereof) are satisfied by the other
// (non-overlapping) $buy_quantity items.
foreach ($buy_quantities as $id => $quantity) {
if (isset($get_quantities[$id])) {
unset($buy_quantities[$id]);
$buy_quantities[$id] = $quantity;
}
}
$final_quantities = [];
$i = 0;
while (!empty($buy_quantities)) {
$selected_buy_quantities = $this
->sliceQuantities($buy_quantities, $this->configuration['buy_quantity']);
if (array_sum($selected_buy_quantities) < $this->configuration['buy_quantity']) {
break;
}
$get_quantities = $this
->removeQuantities($get_quantities, $selected_buy_quantities);
$selected_get_quantities = $this
->sliceQuantities($get_quantities, $this->configuration['get_quantity']);
$buy_quantities = $this
->removeQuantities($buy_quantities, $selected_get_quantities);
// Merge the selected get quantities into a final list, to ensure that
// each order item only gets a single adjustment.
$final_quantities = $this
->mergeQuantities($final_quantities, $selected_get_quantities);
// Determine whether the offer reached its limit.
if ($this->configuration['offer_limit'] == ++$i) {
break;
}
}
foreach ($final_quantities as $order_item_id => $quantity) {
$order_item = $get_order_items[$order_item_id];
$adjustment_amount = $this
->buildAdjustmentAmount($order_item, $quantity);
$order_item
->addAdjustment(new Adjustment([
'type' => 'promotion',
'label' => $promotion
->getDisplayName() ?: $this
->t('Discount'),
'amount' => $adjustment_amount
->multiply('-1'),
'source_id' => $promotion
->id(),
]));
}
}
/**
* {@inheritdoc}
*/
public function clear(EntityInterface $entity, PromotionInterface $promotion) {
parent::clear($entity, $promotion);
$this
->assertEntity($entity);
/** @var \Drupal\commerce_order\Entity\OrderInterface $order */
$order = $entity;
// Check if we have any order item whose quantity has been changed by this
// promotion, and subtract that amount. If the promotion still applies, the
// necessary quantity will be added back in ::apply(). Order items that will
// end up with a quantity of 0 will be removed from the order by
// \Drupal\commerce_order\OrderRefresh::refresh().
if ($this->configuration['get_auto_add']) {
$promotion_data_key = "promotion:{$promotion->id()}:auto_add_quantity";
$auto_add_order_items = array_filter($order
->getItems(), function (OrderItemInterface $order_item) use ($promotion_data_key) {
return $order_item
->getData($promotion_data_key);
});
foreach ($auto_add_order_items as $order_item) {
$new_quantity = Calculator::subtract($order_item
->getQuantity(), $order_item
->getData($promotion_data_key));
$order_item
->setQuantity($new_quantity);
$order_item
->unsetData($promotion_data_key);
}
}
}
/**
* Builds a condition group for the given condition configuration.
*
* @param array $condition_configuration
* The condition configuration.
*
* @return \Drupal\commerce\ConditionGroup
* The condition group.
*/
protected function buildConditionGroup(array $condition_configuration) {
$conditions = [];
foreach ($condition_configuration as $condition) {
if (!empty($condition['plugin'])) {
$conditions[] = $this->conditionManager
->createInstance($condition['plugin'], $condition['configuration']);
}
}
return new ConditionGroup($conditions, 'OR');
}
/**
* Selects non-free order items that match the given conditions.
*
* Selected order items are sorted by unit price in the specified direction.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface[] $order_items
* The order items.
* @param \Drupal\commerce\ConditionGroup $conditions
* The conditions.
* @param string $sort_direction
* The sort direction.
* Use 'ASC' for least expensive first, 'DESC' for most expensive first.
*
* @return \Drupal\commerce_order\Entity\OrderItemInterface[]
* The selected order items, keyed by order item ID.
*/
protected function selectOrderItems(array $order_items, ConditionGroup $conditions, $sort_direction = 'ASC') {
$selected_order_items = [];
foreach ($order_items as $index => $order_item) {
if ($order_item
->getAdjustedTotalPrice()
->isZero()) {
continue;
}
if (!$conditions
->evaluate($order_item)) {
continue;
}
$selected_order_items[$order_item
->id()] = $order_item;
}
uasort($selected_order_items, function (OrderItemInterface $a, OrderItemInterface $b) use ($sort_direction) {
if ($sort_direction == 'ASC') {
$result = $a
->getUnitPrice()
->compareTo($b
->getUnitPrice());
}
else {
$result = $b
->getUnitPrice()
->compareTo($a
->getUnitPrice());
}
return $result;
});
return $selected_order_items;
}
/**
* Finds the configured purchasable entity amongst the given conditions.
*
* @param \Drupal\commerce\ConditionGroup $conditions
* The condition group.
*
* @return \Drupal\commerce\PurchasableEntityInterface|null
* The purchasable entity, or NULL if not found in the conditions.
*/
protected function findSinglePurchasableEntity(ConditionGroup $conditions) {
foreach ($conditions
->getConditions() as $condition) {
if ($condition instanceof PurchasableEntityConditionInterface) {
$purchasable_entity_ids = $condition
->getPurchasableEntityIds();
if (count($purchasable_entity_ids) === 1) {
$purchasable_entities = $condition
->getPurchasableEntities();
return reset($purchasable_entities);
}
}
}
return NULL;
}
/**
* Attempt to find the given purchasable entity amongst the given order items.
*
* If the given purchasable entity isn't referenced by any order item, create
* an order item referencing it so we can automatically add it to the order.
*
* @param \Drupal\commerce\PurchasableEntityInterface $get_purchasable_entity
* The "get" purchasable entity.
* @param \Drupal\commerce_order\Entity\OrderItemInterface[] $order_items
* The order items.
*
* @return \Drupal\commerce_order\Entity\OrderItemInterface
* An order item referencing the given purchasable entity.
*/
protected function findOrCreateOrderItem(PurchasableEntityInterface $get_purchasable_entity, array $order_items) {
foreach ($order_items as $order_item) {
$purchased_entity = $order_item
->getPurchasedEntity();
if ($purchased_entity
->getEntityTypeId() == $get_purchasable_entity
->getEntityTypeId() && $purchased_entity
->id() == $get_purchasable_entity
->id()) {
return $order_item;
}
}
/** @var \Drupal\commerce_order\OrderItemStorageInterface $storage */
$storage = $this->entityTypeManager
->getStorage('commerce_order_item');
$order_item = $storage
->createFromPurchasableEntity($get_purchasable_entity, [
'quantity' => 0,
]);
return $order_item;
}
/**
* Calculates the expected get quantity.
*
* @param array $buy_quantities
* An array of buy quantities.
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The order item.
*
* @return string
* The expected get quantity.
*/
protected function calculateExpectedGetQuantity(array $buy_quantities, OrderItemInterface $order_item) {
$expected_get_quantity = '0';
// Ensure that any possible "get" quantity already in the order is always
// processed last.
if (!$order_item
->isNew()) {
if (isset($buy_quantities[$order_item
->id()])) {
$quantity = $buy_quantities[$order_item
->id()];
unset($buy_quantities[$order_item
->id()]);
$buy_quantities[$order_item
->id()] = $quantity;
}
}
$i = 0;
while (!empty($buy_quantities)) {
$this
->sliceQuantities($buy_quantities, $this->configuration['buy_quantity']);
$expected_get_quantity = Calculator::add($expected_get_quantity, $this->configuration['get_quantity']);
// Determine whether the offer reached its limit.
if ($this->configuration['offer_limit'] == ++$i) {
break;
}
// If the "get" purchasable entity is already in the order, we need to
// ensure that the already discounted quantity is not counted towards the
// buy quantities.
if (!$order_item
->isNew()) {
$buy_quantities = $this
->removeQuantities($buy_quantities, [
$order_item
->id() => $this->configuration['get_quantity'],
]);
}
if (array_sum($buy_quantities) < $this->configuration['buy_quantity']) {
break;
}
}
return $expected_get_quantity;
}
/**
* Gets the #states visibility array for the 'auto_add' form element.
*
* @return array
* An array of visibility options for a form element's #state property.
*/
protected function getAutoAddStatesVisibility() {
// The 'auto_add' form element has to be shown if _any_ condition that
// provides a purchasable entity is enabled for the 'get' conditions. This
// means we need to construct a list of OR statements for #states, which
// looks like this:
// @code
// '#states' => [
// 'visible' => [
// [':input[name="some_element"]' => ['checked' => TRUE]],
// 'or',
// [':input[name="another_element"]' => ['checked' => TRUE]],
// ...
// ],
// ],
// @endcode
$conditions = $this
->getPurchasableEntityConditions();
$states_visibility = array_map(function ($value) {
return [
':input[name="offer[0][target_plugin_configuration][order_buy_x_get_y][get][conditions][products][' . $value . '][enable]"]' => [
'checked' => TRUE,
],
];
}, array_keys($conditions));
for ($i = 0; $i < count($conditions) - 1; $i++) {
array_splice($states_visibility, $i + 1, 0, 'or');
}
return $states_visibility;
}
/**
* Takes a slice from the given quantity list.
*
* For example, ['1' => '10', '2' => '5'] sliced for total quantity '11'
* will produce a ['1' => '10', '2' => '1'] slice, leaving ['2' => '4']
* in the original list.
*
* @param array $quantities
* The quantity list. Modified by reference.
* @param string $total_quantity
* The total quantity of the new slice.
*
* @return array
* The quantity list slice.
*/
protected function sliceQuantities(array &$quantities, $total_quantity) {
$remaining_quantity = $total_quantity;
$slice = [];
foreach ($quantities as $order_item_id => $quantity) {
if ($quantity <= $remaining_quantity) {
$remaining_quantity = Calculator::subtract($remaining_quantity, $quantity);
$slice[$order_item_id] = $quantity;
unset($quantities[$order_item_id]);
if ($remaining_quantity === '0') {
break;
}
}
else {
$slice[$order_item_id] = $remaining_quantity;
$quantities[$order_item_id] = Calculator::subtract($quantity, $remaining_quantity);
break;
}
}
return $slice;
}
/**
* Removes the second quantity list from the first quantity list.
*
* For example: ['1' => '10', '2' => '20'] - ['1' => '10', '2' => '17']
* will result in ['2' => '3'].
*
* @param array $first_quantities
* The first quantity list.
* @param array $second_quantities
* The second quantity list.
*
* @return array
* The new quantity list.
*/
protected function removeQuantities(array $first_quantities, array $second_quantities) {
foreach ($second_quantities as $order_item_id => $quantity) {
if (isset($first_quantities[$order_item_id])) {
$first_quantities[$order_item_id] = Calculator::subtract($first_quantities[$order_item_id], $second_quantities[$order_item_id]);
if ($first_quantities[$order_item_id] <= 0) {
unset($first_quantities[$order_item_id]);
}
}
}
return $first_quantities;
}
/**
* Merges the first quantity list with the second quantity list.
*
* Quantities belonging to shared order item IDs will be added together.
*
* For example: ['1' => '10'] and ['1' => '10', '2' => '17']
* will merge into ['1' => '20', '2' => '17'].
*
* @param array $first_quantities
* The first quantity list.
* @param array $second_quantities
* The second quantity list.
*
* @return array
* The new quantity list.
*/
protected function mergeQuantities(array $first_quantities, array $second_quantities) {
foreach ($second_quantities as $order_item_id => $quantity) {
if (!isset($first_quantities[$order_item_id])) {
$first_quantities[$order_item_id] = $quantity;
}
else {
$first_quantities[$order_item_id] = Calculator::add($first_quantities[$order_item_id], $second_quantities[$order_item_id]);
}
}
return $first_quantities;
}
/**
* Builds an adjustment amount for the given order item and quantity.
*
* @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item
* The order item.
* @param string $quantity
* The quantity.
*
* @return \Drupal\commerce_price\Price
* The adjustment amount.
*/
protected function buildAdjustmentAmount(OrderItemInterface $order_item, $quantity) {
if ($this->configuration['offer_type'] == 'percentage') {
$percentage = (string) $this->configuration['offer_percentage'];
$total_price = $order_item
->getTotalPrice();
if ($order_item
->getQuantity() != $quantity) {
// Calculate a new total for just the quantity that will be discounted.
$total_price = $order_item
->getUnitPrice()
->multiply($quantity);
$total_price = $this->rounder
->round($total_price);
}
$adjustment_amount = $total_price
->multiply($percentage);
}
else {
$amount = Price::fromArray($this->configuration['offer_amount']);
$adjustment_amount = $amount
->multiply($quantity);
}
$adjustment_amount = $this->rounder
->round($adjustment_amount);
return $adjustment_amount;
}
/**
* Gets the purchasable entity condition plugin definitions.
*
* @return array
* The purchasable entity condition plugin definitions.
*/
protected function getPurchasableEntityConditions() {
return array_filter($this->conditionManager
->getFilteredDefinitions('commerce_promotion', [
'commerce_order_item',
]), function ($definition) {
return is_subclass_of($definition['class'], PurchasableEntityConditionInterface::class);
});
}
}
Members
Name | Modifiers | Type | Description | Overrides |
---|---|---|---|---|
BuyXGetY:: |
protected | property | The chain base price resolver. | |
BuyXGetY:: |
protected | property | The condition manager. | |
BuyXGetY:: |
protected | property | The entity type manager. | |
BuyXGetY:: |
public static | function | Ajax callback. | |
BuyXGetY:: |
public | function |
Applies the offer to the given entity. Overrides PromotionOfferInterface:: |
|
BuyXGetY:: |
protected | function | Builds an adjustment amount for the given order item and quantity. | |
BuyXGetY:: |
protected | function | Builds a condition group for the given condition configuration. | |
BuyXGetY:: |
public | function |
Form constructor. Overrides PromotionOfferBase:: |
|
BuyXGetY:: |
protected | function | Calculates the expected get quantity. | |
BuyXGetY:: |
public | function |
Allows an offer to clean up any modifications done to the given entity. Overrides PromotionOfferBase:: |
|
BuyXGetY:: |
public static | function |
Creates an instance of the plugin. Overrides OrderPromotionOfferBase:: |
|
BuyXGetY:: |
public | function |
Gets default configuration for this plugin. Overrides PromotionOfferBase:: |
|
BuyXGetY:: |
protected | function | Attempt to find the given purchasable entity amongst the given order items. | |
BuyXGetY:: |
protected | function | Finds the configured purchasable entity amongst the given conditions. | |
BuyXGetY:: |
protected | function | Gets the #states visibility array for the 'auto_add' form element. | |
BuyXGetY:: |
protected | function | Gets the purchasable entity condition plugin definitions. | |
BuyXGetY:: |
protected | function | Merges the first quantity list with the second quantity list. | |
BuyXGetY:: |
protected | function | Removes the second quantity list from the first quantity list. | |
BuyXGetY:: |
protected | function | Selects non-free order items that match the given conditions. | |
BuyXGetY:: |
protected | function | Takes a slice from the given quantity list. | |
BuyXGetY:: |
public | function |
Form submission handler. Overrides PromotionOfferBase:: |
|
BuyXGetY:: |
public | function |
Form validation handler. Overrides PromotionOfferBase:: |
|
BuyXGetY:: |
public | function |
Constructs a new BuyXGetY object. Overrides OrderPromotionOfferBase:: |
|
DependencySerializationTrait:: |
protected | property | An array of entity type IDs keyed by the property name of their storages. | |
DependencySerializationTrait:: |
protected | property | An array of service IDs keyed by property name used for serialization. | |
DependencySerializationTrait:: |
public | function | 1 | |
DependencySerializationTrait:: |
public | function | 2 | |
MessengerTrait:: |
protected | property | The messenger. | 29 |
MessengerTrait:: |
public | function | Gets the messenger. | 29 |
MessengerTrait:: |
public | function | Sets the messenger. | |
OrderPromotionOfferBase:: |
protected | property | The price splitter. | |
PluginBase:: |
protected | property | Configuration information passed into the plugin. | 1 |
PluginBase:: |
protected | property | The plugin implementation definition. | 1 |
PluginBase:: |
protected | property | The plugin_id. | |
PluginBase:: |
constant | A string which is used to separate base plugin IDs from the derivative ID. | ||
PluginBase:: |
public | function |
Gets the base_plugin_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the derivative_id of the plugin instance. Overrides DerivativeInspectionInterface:: |
|
PluginBase:: |
public | function |
Gets the definition of the plugin implementation. Overrides PluginInspectionInterface:: |
3 |
PluginBase:: |
public | function |
Gets the plugin_id of the plugin instance. Overrides PluginInspectionInterface:: |
|
PluginBase:: |
public | function | Determines if the plugin is configurable. | |
PromotionOfferBase:: |
protected | property | The rounder. | |
PromotionOfferBase:: |
protected | function | Asserts that the given entity is of the expected type. | |
PromotionOfferBase:: |
public | function |
Gets this plugin's configuration. Overrides ConfigurableInterface:: |
|
PromotionOfferBase:: |
public | function |
Gets the offer entity type ID. Overrides PromotionOfferInterface:: |
|
PromotionOfferBase:: |
public | function |
Sets the configuration for this plugin instance. Overrides ConfigurableInterface:: |
|
StringTranslationTrait:: |
protected | property | The string translation service. | 1 |
StringTranslationTrait:: |
protected | function | Formats a string containing a count of items. | |
StringTranslationTrait:: |
protected | function | Returns the number of plurals supported by a given language. | |
StringTranslationTrait:: |
protected | function | Gets the string translation service. | |
StringTranslationTrait:: |
public | function | Sets the string translation service to use. | 2 |
StringTranslationTrait:: |
protected | function | Translates a string to the current language or to a given language. |