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.
- Commerce (all modules)
- Entity Registration
- Commerce Registraton
- Commerce Rules Extra
Steps to reproduce:
- Create a registration enabled product (a product with a registration field attached).
- Ensure that the registration settings for the product has the "Enabled" checkbox unchecked so that essentially registration is not available.
- Create a new order through the order admin UI.
- Add a line item for the disabled registration product.
- Set the order status to "Checkout: Registration" and optionally choose an owner user for the order.
- As the user that owns the order go to /checkout/
- 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
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 |
Comment | File | Size | Author |
---|---|---|---|
#17 | commerce_checkout-remove-debug-code-2655184-17.patch | 1.08 KB | jsacksick |
#11 | 2655184-11.checkout_router_latest_revision.patch | 971 bytes | rszrama |
| |||
#3 | commerce-check_latest_revision_before_checkout_routing-2655184-0.patch | 1.18 KB | acrazyanimal |
|
Comments
Comment #2
acrazyanimal CreditAttribution: acrazyanimal commentedComment #3
acrazyanimal CreditAttribution: acrazyanimal commentedHere 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.
Comment #4
acrazyanimal CreditAttribution: acrazyanimal commentedComment #5
jbabiak CreditAttribution: jbabiak at OpenConcept Consulting Inc. commentedConfirmed 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
Comment #6
torgosPizzaThis 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.)
Comment #7
rszrama CreditAttribution: rszrama at Centarro commentedIs there a reason this doesn't just use commerce_order_load() on the order_id?
Comment #8
torgosPizzaThe only difference I can see is that we are setting $reset = TRUE which commerce_order_load does not allow, at least I think?
Comment #9
acrazyanimal CreditAttribution: acrazyanimal commentedThat 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.
Comment #10
joelpittet@rszrama, would you like a separate issue to add reset to
commerce_order_load()
Comment #11
rszrama CreditAttribution: rszrama at Centarro commentedWow, 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?
Comment #13
rszrama CreditAttribution: rszrama at Centarro commentedI can't think of any reason not to commit this patch. Makin' it so.
Comment #15
blasthaus CreditAttribution: blasthaus commentedlooks like the patch committed was incorrect:
Note the FALSE
Comment #16
rszrama CreditAttribution: rszrama at Centarro commentedWow. 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.
Comment #17
jsacksick CreditAttribution: jsacksick at Centarro commentedPosting the patch for use in .make files.
Comment #18
joshmiller@rszrama Please reopen this ticket :) The fix you talked about never got committed.
Comment #19
nullkernel CreditAttribution: nullkernel commented@joshmiller I show it as fixed in branch 7.x-1.x. Commit c733704e on 2017-11-29.
Comment #20
acrazyanimal CreditAttribution: acrazyanimal commentedAwesome, thanks for reviewing and committing everyone!