It would appear the commerce_product_default_variation_alter() hook is now dead, and \Drupal\commerce_product\Entity\Product->getDefaultVariation() blindly returns the first active variation.

How should I go about altering this behaviour?

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

John Pitcairn created an issue. See original summary.

bojanz’s picture

Title: How to alter the product default variation? » Allow altering the default variation
Category: Support request » Feature request

Not possible, we always take the first one. So, retitling the issue.

What's your use case?

John Pitcairn’s picture

Thanks. It was possible in Commerce 1.x, via the hook mentioned.

Use case is that the client needs to display a different variation of a course product, depending on which industry sector the (anonymous) user has indicated they belong to earlier in their site visit.

agoradesign’s picture

Another use case would be to determine the default variation based on the attribute weights, rather than the variation's position in the product's entity reference field

John Pitcairn’s picture

Or in the case of stocking systems, avoid displaying a default variation that is out of stock.

vasike’s picture

Assigned: Unassigned » vasike

i'll take a look

vasike’s picture

Status: Active » Needs review
FileSize
1.91 KB

there is a patch for this.

Didn't use alter, but a hook

bojanz’s picture

We need an event, Commerce has no hooks.

vasike’s picture

here is a new patch using an Event

vasike’s picture

Assigned: vasike » Unassigned
jespermb’s picture

We have tried this patch on our current project and can confirm that it works. Nice work. :)

Hubbs’s picture

Status: Needs review » Needs work

This patch now fails to apply so setting back to needs work.

Unfortunately, I'm not able to provide more details as to why. My knowledge of development is quite limited.

flocondetoile’s picture

Status: Needs work » Needs review
FileSize
4.13 KB

patch #9 rerolled

Hubbs’s picture

Patch applies now.

getDefaultVariation() does return the variation object.

I'm not able to use the setDefaultVariation() function. I tried setDefaultVariation() on a product entity but couldn't see any results when debugging the product entity. Error log shows:

Error: Call to undefined method Drupal\commerce_product\Entity\Product::setDefaultVariation().

Maico de Jong’s picture

#9 worked for me! :)

batkor’s picture

Hi!
#13 worked for me! TY!
Will help someone
1. drush gen event-subscriber
2. Correct subscriber class

public static function getSubscribedEvents() {
    return [
      ProductEvents::PRODUCT_DEFAULT_VARIATION => ['myLogicalForGetDefaultVariation', -100],
    ];
  }

3. Create metod myLogicalForGetDefaultVariation

public function myLogicalForGetDefaultVariation(ProductDefaultVariationEvent $event){
if ($event->getProduct()->bundle() == 'you_product_type'){
      $variations = $event->getProduct()->getVariations();
      /** @var ProductVariation $variation */
      foreach ($variations as $variation) {
        // Find your variation
      }
     $event->setDefaultVariation($variation);
    }
}
kmajzlik’s picture

Works nice for me.

a.dmitriiev’s picture

Patch 13 works for me as well. Very elegant solution with EventSubscriber.

Usefull link for generating the subscriber with drupal console https://hechoendrupal.gitbooks.io/drupal-console/content/en/commands/gen...

But I had to reroll that patch so it is applied to current dev branch

arpad.rozsa’s picture

Status: Needs review » Reviewed & tested by the community

I can also say that, this works for me as well. Setting to RTBC since it's sitting here for a while now and would be nice to get it committed.

The last submitted patch, 9: default_variation_hook-2835629-9.patch, failed testing. View results

yonailo’s picture

Yes this patch is very useful.

I am trying to understand how the view's button 'add_to_cart' is able to add the right product-variation to the cart. Reading the code inside ProductLazyBuilders there is a call to 'getDefaultVariation()' that always returns the first variation. I don't understand how and where views handles this.

Any help would be appreciated.

yonailo’s picture

In fact, I have just realized that my view does not work, I always get the first variation even when I click on the add-to-cart button of the second variation.

motame’s picture

Does someone propose an example where to place such code ? the two class methods proposed by batkor in the comment 16
I want to set the default price as the one of the variation with the lowest price.

a.dmitriiev’s picture

@motame you need in your module create EventSubscriber here: `your_module/src/EventSubscriber/YourEventSubscriber.php` with 2 methods from the comment #16 and also register that subscriber in your_module.services.yml file:

services:
  your_module.default_variation:
    class: Drupal\your_module\EventSubscriber\YourEventSubscriber
    tags: [{ name: event_subscriber }]

Or use instructions in comment #16 using drush to generate the subscriber automatically.

Don't forget to clean the caches afterwards.

motame’s picture

With the service included in my_module.services.yml, it works and it is indeed a very elegant way to do it (using a service instead of a hook) !
Thank you very much a.dmitriiev.

kmajzlik’s picture

Commerce guys should look on it and merge. It is boring to still re-roll and check for patch changes on each composer update ...

agoradesign’s picture

there are no Commerce Guys anymore ;)))) you have to call for Centarro ;)

flocondetoile’s picture

Also, adding tests to this new feature should help a lot to get this patch committed.

dench0’s picture

@bojanz One of use case for this: is to show by default variation with available stock level. With current code - it almost impossible to do this and by default always showing first variation, even if it is out of the stock. But with #18 patch it possible to accomplish this with just dozen lines of code.

mglaman’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs tests

Hm, taking a look. We could fire an event. However, we usually recommend using \Drupal\commerce_product\ProductVariationStorage::loadFromContext and \Drupal\commerce_product\ProductVariationStorage::loadEnabled.

Quickly looking over things:

  • ProductVariationStorageInterface::loadFromContext gets the variation from the current query parameter, the fallback is to use ProductInterface::getDefaultVariation
  • ProductInterface::getDefaultVariation loops through all variations and returns the first entity that is published and accessible
  • ProductVariationStorageInterface::loadEnabled performs checks like ProductInterface::getDefaultVariation - it loads all published and accessible variations. But it also fires ProductEvents::FILTER_VARIATIONS to limit the allowed variations.

So we do have a possible problem where getDefaultVariation would allow a default variation that isn't available from ProductVariationStorageInterface::loadEnabled. That can be a separate issue to file, as it doesn't regard altering the default but instead what is available to select from the defaults.

For example, filtering variations wouldn't fix #29 because you may want to still display it as an option, but not the default one.

I do, however, have to kick it to Needs Work because there are no tests covering the event.

Also: I wonder if there are any performance implications? We fire getDefaultVariation when rendering the add to cart form, and the product view builder. It is also invoked by \Drupal\commerce_product\ProductVariationStorage::loadFromContext which is called a few times. I wonder if we can use a static variable to prevent duplicated event dispatches. I can be a follow-up, as it's even an improvement to our current operations.

So, to summarize next steps:

  • Open an issue that the variations used to determine the default variations may not match the output of loadEnabled
  • Open an issue to investigate adding a static cache to getDefaultVariation to reduce duplicate variation access checks when getting the default variation
  • Add a test for the event by implementing a test module which performs a subscription to the event and changes the default variation
mglaman’s picture

Linking to #3135918: Use a static cache in getDefaultVariation for performance. I'd definitely want some kind of protection from firing the event multiple times for a single order in one request.

mglaman’s picture

Here's a rerolled patch now that #3135918: Use a static cache in getDefaultVariation for performance landed. I'll work on a test.

mglaman’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
9 KB

And here's a test.

jsacksick’s picture

Status: Needs review » Reviewed & tested by the community

Patch looks good to me! Tests are passing, we're probably just missing a @coversDefaultClass?

  • mglaman committed 21f5b63 on 8.x-2.x authored by vasike
    Issue #2835629 by vasike, mglaman, flocondetoile, a.dmitriiev: Allow...
mglaman’s picture

Status: Reviewed & tested by the community » Fixed

re: @coversDefaultClass we're not really testing that class specifically, but the integration of it.

Giving authorship to vasike, since he wrote the first patch. Thanks, everyone else for the rerolls and feedback.

Committed! This will go out in the next release.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

ConradFlashback’s picture

I have a custom code similar to #16.

class PrezziSubscriber implements EventSubscriberInterface {


  /**
   * Constructs a new PrezziSubscriber object.
   */
  public function __construct() {

  }

  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {

    $events[ProductEvents::PRODUCT_DEFAULT_VARIATION][] = [
      'setDefaultVariationInStock',
      -100,
    ];
    return $events;
  }

  /**
   * @param \Drupal\commerce_product\Event\ProductDefaultVariationEvent $event
   *
   * Set default variation the first one that is not out of stock, or the last variation
   * if all out of stock
   */
public function setDefaultVariationInStock(ProductDefaultVariationEvent $event){
    if ($event->getProduct()->bundle() == 'default'){
      $variations = $event->getProduct()->getVariations();
      $defaultVariation = null;
      $firstVariation = null;
	 
      foreach ($variations as $variation) {
        $defaultVariation = $variation;
        if ($variation->hasField('field_stock') && $variation->field_stock->value != NULL) {

          $stock     = (integer) $variation->field_stock->value;
	  \Drupal::logger('pricevariation')->info('Var stock '.$stock);
		  
          if ($stock <= 0) {
            continue;
          }

          break;
        }
      }
    if ($defaultVariation!=null){
	  $skudefaultVariation = $defaultVariation->getSku();
      $event->setDefaultVariation($defaultVariation);
	  \Drupal::logger('pricevariation')->info('Var default '.$skudefaultVariation);

    }
  }
}
}

With my last Drupal 8 version everything works well.
I have updated now to D9.33 and PHP 8.1 but it doesn't show add to cart button for products that use $event->setDefaultVariation($defaultVariation);
But setDefaultVariation works well because custom logs show correct values.
No php error in log.

Any idea?

agoradesign’s picture

maybe the problem isn't caused by your event handler, but rather with the display -> there's this recent fix #3257191: PHP 8.1 "Error: Nesting level too deep" in ProductVariationWidgetBase.php with preselected variation that fixes an error with PHP 8.1

ConradFlashback’s picture

@agoradesign
Yes, this is the error!
Thanks! You save my day.

agoradesign’s picture

you're welcome :)