Problem/Motivation

Seeing this issue during checkout:

EntityMetadataWrapperException: Missing data values. in EntityMetadataWrapper->value() (line 83 of /.../sites/all/modules/entity/includes/entity.wrapper.inc).

There are likely many other ways to reproduce this issue, but one sure fire way is as follows:

First, you need a drupal install that includes the following enabled modules.

Steps to reproduce:

  1. Create a registration enabled product (a product with a registration field attached).
  2. Ensure that the registration settings for the product has the "Enabled" checkbox unchecked so that essentially registration is not available.
  3. Create a new order through the order admin UI.
  4. Add a line item for the disabled registration product.
  5. Set the order status to "Checkout: Registration" and optionally choose an owner user for the order.
  6. As the user that owns the order go to /checkout/
  7. You should see the EntityMetadataWrapperException error.

The issue is not inherently Commerce, but the avenues Commerce has allowed through its hooks for an order to be manipulated during the order entity loading before the checkout routing process. In the example used to reproduce the issue it is the Commerce Rules Extra and Commerce Registration module that ultimately leads to the order becoming out of sync during the initial loading of the order for commerce_checkout_router().

When the drupal menu system first loads the order object to pass to commerce_checkout_router() there are various steps that occur, and the call stack can be summarized as follows:

  • {{menu routing}} ->
  • menu_get_object() ->
  • commerce_order_load() ->
  • commerce_cart_commerce_order_load() ->
  • commerce_cart_order_is_cart("up to this point the order object has been passed along") ->
  • commerce_order_statuses("order is not passed") ->
  • {{order status hooks}} ->
  • commerce_rules_extra_commerce_checkout_pane_info_alter() ->
  • menu_get_object('commerce_order')
  • ...

So in the last call shown above the commerce_rules_extra_commerce_checkout_pane_info_alter tries to get the order object again and at that point the order object loading process starts again and gets fullfilled and the commerce_order_load() -> commerce_cart_commerce_order_load() steps are called again and there are other hooks such as hook_commerce_cart_order_refresh() that allow the order object to be altered. Since this latter version of the order object is a newer revision and disconnected from the original that ultimately gets passed to commerce_checkout_router() the data is out of sync. In the example commerce registration has removed the line item for the registration product because it is not allowed to register to a product that is disabled. This results in the EntityMetadataWrapper to have a stale reference to a no longer existing line item and hence the Exception when trying to access it.

Wow, that totally sucked debugging that!

Proposed resolution

This isn't a commerce_registration issue because the point of hook_commerce_cart_order_refresh() is to be able to modify the order in the cart/checkout process.

This is kind of a commerce_rules_extra issue because it loads the order the second time, but really what else can it do at that point to give contextual data about the current order and I can't see a way around it.

So this brings us to the commerce_checkout_router() function. It doesn't make sense that you would ever want to use an order that is NOT the current revision during checkout so lets just add a check to make sure that the order passed to the function is indeed the latest and greatest. Problem solved!

Remaining tasks

Contributor tasks needed
Task Contributor instructions Complete?
Create a patch Instructions YES
Review patch to ensure that it fixes the issue, stays within scope, is properly documented, and follows coding standards Instructions
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

acrazyanimal created an issue. See original summary.

acrazyanimal’s picture

Issue summary: View changes
acrazyanimal’s picture

Here is a patch for checking that the order passed to commerce_checkout_router() is the latest revision and loading a fresh copy of the order object if it is not.

acrazyanimal’s picture

Issue summary: View changes
jbabiak’s picture

Status: Needs review » Reviewed & tested by the community

Confirmed EntityMetadataWrapperException will show up when trying to load an order with revisions following the testing steps outlined above. Tested with patch and the order loads fine without error. Changing states to RTBC

torgosPizza’s picture

This is really interesting, thanks. Possibly related to #1804592: During AJAX form submission in checkout, the $order argument passed by the Form API is incorrect.? (I'm not sure offhand if the routing system is as coupled to ajax panes as it is to just regular checkout pages, but I thought I would mention it since this patch has the potential to affect that other issue.)

rszrama’s picture

Status: Reviewed & tested by the community » Needs review

Is there a reason this doesn't just use commerce_order_load() on the order_id?

torgosPizza’s picture

The only difference I can see is that we are setting $reset = TRUE which commerce_order_load does not allow, at least I think?

acrazyanimal’s picture

That is exactly it. We want to make sure we are loading the freshest non-cached copy. commerce_order_load() will end up loading the order with $reset = FALSE.

Its good to note that most of the time commerce_order_is_latest_revision($order) is going to return TRUE unless you do something crazy like what is done in commerce_registration. So most of the time the order will not need to be reloaded unless it happens to be out of sync from the latest revision.

joelpittet’s picture

Status: Needs review » Reviewed & tested by the community

@rszrama, would you like a separate issue to add reset to commerce_order_load()

rszrama’s picture

Category: Bug report » Task
Status: Reviewed & tested by the community » Needs review
FileSize
971 bytes

Wow, that Commerce Rules Extra code is a nightmare. Definitely reinforces my opinion that Rules should never have been shoehorned to work for altering the checkout form. But c'est la vie, that ship has sailed.

We can be a little more direct here, invoking the resetCache() method on the controller with the ID of the order we actually want to remove from the cache. I don't think we need to add this to commerce_order_load() given the simplicity of this pattern.

Anyone wanna test this in a failing scenario?

  • rszrama committed daa0879 on 7.x-1.x
    Issue #2655184 by acrazyanimal, rszrama: Ensure the latest revision of...
rszrama’s picture

Status: Needs review » Fixed

I can't think of any reason not to commit this patch. Makin' it so.

Status: Fixed » Closed (fixed)

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

blasthaus’s picture

looks like the patch committed was incorrect:
Note the FALSE

 function commerce_checkout_router($order, $checkout_page = NULL) {
+  // Ensure this function is routing the latest revision of the given order in
+  // case some hook invoked before this page callback is executed updated some
+  // value on the order that would impact routing.
+  if (FALSE && !commerce_order_is_latest_revision($order)) {
+    entity_get_controller('commerce_order')->resetCache(array($order->order_id));
+    $order = commerce_order_load($order->order_id);
+  }
+
   $checkout_pages = commerce_checkout_pages();
rszrama’s picture

Wow. Thanks for pointing that out. My patch in the queue was right, but I obviously was testing the failing condition and committed that. Ugh. Fixed in -dev, will roll a release ASAP.

jsacksick’s picture

Posting the patch for use in .make files.

joshmiller’s picture

@rszrama Please reopen this ticket :) The fix you talked about never got committed.

nullkernel’s picture

@joshmiller I show it as fixed in branch 7.x-1.x. Commit c733704e on 2017-11-29.

acrazyanimal’s picture

Awesome, thanks for reviewing and committing everyone!