diff -u b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php --- b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationAttributesWidget.php @@ -85,26 +85,6 @@ } } - $variation_types = []; - foreach ($variations as $variation) { - $variation_types[$variation->bundle()] = $variation->bundle(); - if (count($variation_types) > 1) { - break; - } - } - - // If there are multiple variation types, fallback to the title widget. - if (count($variation_types) > 1) { - $options = [ - 'field_definition' => $this->fieldDefinition, - 'configuration' => [ - 'type' => 'commerce_product_variation_title', - ], - ]; - $fallback_widget = $this->fieldWidgetManager->getInstance($options); - return $fallback_widget->formElement($items, $delta, $element, $form, $form_state); - } - // Build the full attribute form. $wrapper_id = Html::getUniqueId('commerce-product-add-to-cart-form'); $form += [ diff -u b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php --- b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php +++ b/modules/product/src/Plugin/Field/FieldWidget/ProductVariationWidgetBase.php @@ -109,31 +109,6 @@ * Assumes the existence of a 'selected_variation' in $form_state. */ public static function ajaxRefresh(array $form, FormStateInterface $form_state) { - $variation = ProductVariation::load($form_state->get('selected_variation')); - /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ - $product = $form_state->get('product'); - if ($variation->hasTranslation($product->language()->getId())) { - $variation = $variation->getTranslation($product->language()->getId()); - } - - // Check if we are switching to a new product variation that relates to a - // different order item type and if so, create a new order item and rebuild - // the form with its order item type form display. - $form_object = $form_state->getFormObject(); - $previous_order_item = $form_object->getEntity(); - if ($previous_order_item->bundle() != $variation->getOrderItemTypeId()) { - /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */ - $order_item_storage = \Drupal::service('entity_type.manager')->getStorage('commerce_order_item'); - $order_item = $order_item_storage->createFromPurchasableEntity($variation); - $form_object->setEntity($order_item); - $form_display = EntityFormDisplay::collectRenderDisplay($order_item, 'add_to_cart'); - $form_object->setFormDisplay($form_display, $form_state); - /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ - $form_builder = \Drupal::service('form_builder'); - $form_state->setRebuild(); - $form = $form_builder->rebuildForm($form_object->getFormId(), $form_state, $form); - } - /** @var \Drupal\Core\Render\MainContent\MainContentRendererInterface $ajax_renderer */ $ajax_renderer = \Drupal::service('main_content_renderer.ajax'); $request = \Drupal::request(); @@ -141,6 +116,12 @@ /** @var \Drupal\Core\Ajax\AjaxResponse $response */ $response = $ajax_renderer->renderResponse($form, $request, $route_match); + $variation = ProductVariation::load($form_state->get('selected_variation')); + /** @var \Drupal\commerce_product\Entity\ProductInterface $product */ + $product = $form_state->get('product'); + if ($variation->hasTranslation($product->language()->getId())) { + $variation = $variation->getTranslation($product->language()->getId()); + } /** @var \Drupal\commerce_product\ProductVariationFieldRendererInterface $variation_field_renderer */ $variation_field_renderer = \Drupal::service('commerce_product.variation_field_renderer'); $view_mode = $form_state->get('view_mode'); only in patch2: unchanged: --- a/modules/cart/src/Form/AddToCartForm.php +++ b/modules/cart/src/Form/AddToCartForm.php @@ -5,13 +5,15 @@ namespace Drupal\commerce_cart\Form; use Drupal\commerce\Context; use Drupal\commerce_cart\CartManagerInterface; use Drupal\commerce_cart\CartProviderInterface; +use Drupal\commerce_order\Entity\OrderItemInterface; use Drupal\commerce_order\Resolver\OrderTypeResolverInterface; use Drupal\commerce_price\Resolver\ChainPriceResolverInterface; use Drupal\commerce_store\CurrentStoreInterface; use Drupal\commerce_store\SelectStoreTrait; use Drupal\Component\Datetime\TimeInterface; use Drupal\Core\Entity\ContentEntityForm; -use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\Display\EntityFormDisplayInterface; +use Drupal\Core\Entity\Entity\EntityFormDisplay; use Drupal\Core\Entity\EntityRepositoryInterface; use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Form\FormStateInterface; @@ -124,14 +126,6 @@ class AddToCartForm extends ContentEntityForm implements AddToCartFormInterface ); } - /** - * {@inheritdoc} - */ - public function setEntity(EntityInterface $entity) { - $this->entity = $entity; - return $this; - } - /** * {@inheritdoc} */ @@ -167,16 +161,68 @@ class AddToCartForm extends ContentEntityForm implements AddToCartFormInterface * {@inheritdoc} */ public function buildForm(array $form, FormStateInterface $form_state) { + $original_form = $form; $form = parent::buildForm($form, $form_state); + /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */ + $order_item = $this->entity; // The widgets are allowed to signal that the form should be hidden // (because there's no purchasable entity to select, for example). if ($form_state->get('hide_form')) { $form['#access'] = FALSE; } + else { + $selected_variation = $form_state->get('selected_variation'); + // If the order item references a different variation than the one + // currently selected, and the selected variation is supposed to use + // a different order item type, rebuild the form. + if ($selected_variation && $order_item->getPurchasedEntityId() != $selected_variation) { + /** @var \Drupal\commerce_product\Entity\ProductVariationInterface $selected_variation */ + $selected_variation = $this->entityTypeManager->getStorage('commerce_product_variation')->load($selected_variation); + if ($selected_variation->getOrderItemTypeId() !== $order_item->bundle()) { + /** @var \Drupal\commerce_order\OrderItemStorageInterface $order_item_storage */ + $order_item_storage = $this->entityTypeManager->getStorage('commerce_order_item'); + $order_item = $order_item_storage->createFromPurchasableEntity($selected_variation); + $this->prepareOrderItem($order_item); + $this->setEntity($order_item); + $form_display = EntityFormDisplay::collectRenderDisplay($order_item, $this->operation); + $this->setFormDisplay($form_display, $form_state); + $form = $original_form; + $form = parent::buildForm($form, $form_state); + } + } + } return $form; } + /** + * {@inheritdoc} + */ + public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state) { + $component = $form_display->getComponent('purchased_entity'); + // If the product references variations of a different type, fallback + // to using the title widget as the attributes widget cannot properly + // work. + if ($component['type'] === 'commerce_product_variation_attributes') { + $product = $form_state->get('product'); + /** @var \Drupal\commerce_product\ProductVariationStorageInterface $product_variation_storage */ + $product_variation_storage = $this->entityTypeManager->getStorage('commerce_product_variation'); + $variations = $product_variation_storage->loadEnabled($product); + + $variation_types = []; + foreach ($variations as $variation) { + $variation_types[$variation->bundle()] = $variation->bundle(); + if (count($variation_types) > 1) { + $component['type'] = 'commerce_product_variation_title'; + $form_display->setComponent('purchased_entity', $component); + $this->setFormDisplay($form_display, $form_state); + break; + } + } + } + return parent::setFormDisplay($form_display, $form_state); + } + /** * {@inheritdoc} */ @@ -221,23 +267,32 @@ class AddToCartForm extends ContentEntityForm implements AddToCartFormInterface public function buildEntity(array $form, FormStateInterface $form_state) { /** @var \Drupal\commerce_order\Entity\OrderItemInterface $entity */ $entity = parent::buildEntity($form, $form_state); + $this->prepareOrderItem($entity); + return $entity; + } + + /** + * Helper function used to prepare the order item for add to cart. + * + * @param \Drupal\commerce_order\Entity\OrderItemInterface $order_item + * The order item to prepare. + */ + protected function prepareOrderItem(OrderItemInterface $order_item) { // Now that the purchased entity is set, populate the title and price. - $purchased_entity = $entity->getPurchasedEntity(); - $entity->setTitle($purchased_entity->getOrderItemTitle()); - if (!$entity->isUnitPriceOverridden()) { + $purchased_entity = $order_item->getPurchasedEntity(); + $order_item->setTitle($purchased_entity->getOrderItemTitle()); + if (!$order_item->isUnitPriceOverridden()) { $store = $this->selectStore($purchased_entity); $context = new Context($this->currentUser, $store); - $resolved_price = $this->chainPriceResolver->resolve($purchased_entity, $entity->getQuantity(), $context); - $entity->setUnitPrice($resolved_price); + $resolved_price = $this->chainPriceResolver->resolve($purchased_entity, $order_item->getQuantity(), $context); + $order_item->setUnitPrice($resolved_price); } - $order_type_id = $this->orderTypeResolver->resolve($entity); + $order_type_id = $this->orderTypeResolver->resolve($order_item); $store = $this->selectStore($purchased_entity); $cart = $this->cartProvider->getCart($order_type_id, $store); if ($cart) { - $entity->set('order_id', $cart->id()); + $order_item->set('order_id', $cart->id()); } - - return $entity; } } only in patch2: unchanged: --- a/modules/product/src/ProductVariationStorage.php +++ b/modules/product/src/ProductVariationStorage.php @@ -21,6 +21,13 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr */ protected $requestStack; + /** + * Static cache of enabled variations. + * + * @var array + */ + protected $enabledVariations = []; + /** * {@inheritdoc} */ @@ -61,6 +68,10 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr * {@inheritdoc} */ public function loadEnabled(ProductInterface $product) { + // Check if the enabled variations were already determined for this product. + if (array_key_exists($product->id(), $this->enabledVariations)) { + return $this->enabledVariations[$product->id()]; + } $ids = []; foreach ($product->variations as $variation) { $ids[$variation->target_id] = $variation->target_id; @@ -73,6 +84,7 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr ->condition('variation_id', $ids, 'IN'); $result = $query->execute(); if (empty($result)) { + $this->enabledVariations[$product->id()] = []; return []; } // Restore the original sort order. @@ -90,6 +102,9 @@ class ProductVariationStorage extends CommerceContentEntityStorage implements Pr unset($enabled_variations[$variation_id]); } } + // Populate the static cache so that the next time this method is called + // we don't perform the same expansive logic during the same request. + $this->enabledVariations[$product->id()] = $enabled_variations; return $enabled_variations; }