If I try to wrap a node whose product reference field references a deleted product, the entity metadata wrapper fails when trying to load the product, i.e. wrapper->product->value(). This seems like a bug to me, b/c technically the value if expressed as the return value of commerce_product_load() would be FALSE. This shouldn't fail at an exception, ->value() should just return FALSE and my code should know how to accommodate failed loads. The alternative would be for the wrapper to just ignore invalid values on these reference fields.

In the meantime, we do need to fix it with hook_commerce_product_delete() on our end to remove the product ID from any existing product references so the problem never happens.

CommentFileSizeAuthor
#19 1030140.patch1.11 KBgambaweb
#11 entity_references.patch3.09 KBfago
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

fago’s picture

Hm, having FALSE as a valid value for an entity doesn't really sound right to me. If the info says there is an entity property which is an entity, I'd not say FALSE should be a valid value.

Anyway, I see the three options to handle this case:
1) Throw an exception, as it is now. Fine for BC, and as said makes sense to me. If there is entity reference with a stale id, e.g. you would still be able to get the id and the type of the wrapper. Then if you want to make use of the entity and it is not there, I'd expect an exception - not?
2) Return NULL, what as to the docs means the property is not set at all (and is valid). Sounds right, but it is not really practicable as it would mean we would have to manually filter list-of-referenced-entities to not contain invalid entity ids in order to be consistent - ouch, much unnecessary entity-loading....
3) Return FALSE, as suggested. This would mean we would have to additionally check for FALSE in some cases of the code, as well as any module using the wrapper would have to follow that change - e.g. rules.

In the end, option 1) and 3) are possible ways to go. As 3) would be an API change now and I think 1) makes sense, I'd tend to stay with 1). Any good reasons / use-cases why 3) should be preferred?

Maybe, a little helper would help also? E.g. we have already validate, so we could add something like

public function hasValidValue() {
  try {
    $value = $this->value();
    return isset($value) && $this->validate($value);
  }
  catch (EntityMetadataWrapperException $e) {
    return FALSE;
  }
}
rszrama’s picture

Title: Wrapping entities with stale references fails » Do not throw exceptions when wrapping an entity with a stale reference

Ahh, didn't realize I'd already opened an issue for this, but this continues to be a major problem for us in Commerce. We have several entities with various property and field based references between them, as you know. There seem to be innumerable ways in which people delete entities without the associated references being taken care of, no matter how much code I try to bake into deletion routines.

One that I have that is unsolvable based on browser behavior is that if someone clicks the "Remove" button on the Cart form for two separate items on the same page request (i.e. before the first remove has finished), then the first item gets deleted but the reference to it on the cart order is not wiped properly. Even more, people directly using either their own update / delete queries or (even worse) the API I've created for them to manipulate entities continue to disrupt these associations.

Continuing to throw exceptions like you do now means every time someone gets to a state where they have a shopping cart order whose line item reference field has a stale line item ID in it, the site is dead for them. They see endless exceptions because we try to load the order to display a cart block. It means further that administrators can't even go delete those orders, because we load and wrap the order during deletion to clear out the line items referenced by the order.

Ultimately what this means is that we have based our handling and manipulation of entities on an entity_metadata_wrapper() that makes Commerce to brittle to work with. It's proving very frustrating for people trying to use / develop with Commerce, but our goal of course is to use and promote the wrapper as a tool for simplification. Our strategy of cleaning up references on deletion is simply not working, and even if it were, it wouldn't exactly scale (we're not batching these deletions, for example). We could try to start cleaning up references on load prior to any sort of wrapping, but that's going to be fraught with race / cache conditions. Short of a workaround on our end, option 3 above or some variation of it seem to be the only way to make this usable for our contributors.

I think I might even revisit your idea of NULL. If a line item's order_id property is set to a non-existent order ID, then making that NULL would work just fine - there's no order to reference, and therefore the developer should be free to re-set the order_id or have it removed entirely on save (so that after the first load / save the invalid association is no longer present). That may be too data intrusive, who knows?

I'll see if Damien has any other thoughts toward a solution in here. Also, I don't suppose #1236736: ensure exceptions are only thrown when calling value() is related to this issue at all?

rszrama’s picture

Here's a search of my issue queue for EntityMetadataWrapperException for reference: http://drupal.org/project/issues/commerce?text=EntityMetadataWrapperExce...

fago’s picture

I agree that we need to solve this, it keeps popping up in Rules itself too.

Re-visiting options 1) to 3), I don't think they are mutually exclusive. 1) would apply anyway if the entity-wrapper is considered empty, i.e. have value NULL.

Also, recently a patch went in that made the wrappers interpret FALSE entity values returned from getter callbacks as "not set", i.e. the wrapper returns NULL. That makes option 3) more unattractive as assigning another meaning to FALSE when returned from the getter callback or returned from the wrapper is certainly confusing.

So what about a changed option 2)
*) Don't throw an exception when accessing $entityWrapper->value() but return NULL if it cannot be loaded. Thus, that way the property would appear as "is not set".
*) Keep throwing exceptions when accessing $entity->property->value() as now if the entity is not loadable.
*) Don't filter list of entity-ids for being valid, just as a single entity wrappers containing an id. Document that there is no warranty the ids are valid for getIdentifier() and value() when returning ids.

Thoughts?

>I'll see if Damien has any other thoughts toward a solution in here.
Yep, more input on this issue would be very valuable!

rszrama’s picture

I like the approach - it will be much easier for me to ensure a non-NULL value than to not be able to wrap with invalid references. Will it be feasible for the list wrapper to ignore NULL values? For example, can I safely do this even if one of the line items referenced no longer exists:

$order = commerce_order_load(1);
$wrapper = entity_metadata_wrapper('commerce_order', $order);

foreach ($wrapper->commerce_line_items as $delta => $line_item_wrapper) {
  $price = $line_item_wrapper->unit_price->value();
  // Do something with the price data.
}

This would work fine as long as $line_item_wrapper wasn't NULL, otherwise I suppose I'd have to check !is_null() in any case like this.

fago’s picture

Good question.

Currently, it would fetch the wrappern, and then load each entity on its own. Thus, we would have to return the NULL value for the entity.

We could add a $wrapper->commerce_line_items->value() call before looping over the wrapper, what would trigger the multiple-loading. Thus we could filter invalid values at this step? Afterwards, the single-load would benefit from the static cache.

I'm not sure about the filtering though, as it would be a bit inconsistent. Before the load is triggered you'd get more values than afterwards. For that reason I tend to think it would better stay with the empty wrapper.

We should probably introduce a helper to ease checking it though, e.g.
$wrapper->isEmpty()
? Or is that to easily confused with PHP's empty() ? $wrapper->isset() or $wrapper->hasValue()?

rszrama’s picture

Hmm, it's tough. I like the consistency of every value in the foreach() for these reference fields being a wrapper of the expected type. It shouldn't be that difficult to check !is_null() before performing any operations on it, but what if we could just bypass NULL values in the loop while leaving the value itself alone? That probably breaks the spirit of the iterator interface, so maybe it's not a good idea.

As for helper names, I don't think any are more or less confusing. They probably aren't even necessary, though, since the wrapper wouldn't be a wrapper at all but NULL, right?

fago’s picture

No, there would be a wrapper but the $wrapper->value() would return NULL - generally this is the case for wrappers of a property that is not set.
Accessing $wrapper->value() is fine, but returns NULL. However once you'd do $wrapper->property->value() you'd get an exception ("unable to fetch data as the parent data structure is not set" or so).

rszrama’s picture

Sounds good enough to me. I think I understand why you don't want to filter NULL values out of the iterator and can accommodate them in our foreach() loops. If you were going to add a helper function, I'd go with isNull(), but I'm not sure it's necessary. It's not that much more work to check if ($wrapper->value() === NULL). : )

amitaibu’s picture

Coming from a duplicate issue #1253140: Don't throw exception if can't load entity -- subscribe

fago’s picture

Status: Active » Needs review
FileSize
3.09 KB

*) Don't throw an exception when accessing $entityWrapper->value() but return NULL if it cannot be loaded. Thus, that way the property would appear as "is not set".
*) Keep throwing exceptions when accessing $entity->property->value() as now if the entity is not loadable.
*) Don't filter list of entity-ids for being valid, just as a single entity wrappers containing an id. Document that there is no warranty the ids are valid for getIdentifier() and value() when returning ids.

I think that's the way to go. Attached patch implements it. Please test.

amitaibu’s picture

Status: Needs review » Needs work
+++ b/includes/entity.wrapper.inc
@@ -947,10 +946,19 @@ class EntityListWrapper extends EntityMetadataWrapper implements IteratorAggrega
+   * still accessible, however the entity object value would be NULL. That way,
+   * there may be NULL values in lists of entity objects due to stale entity
+   * references.

Instead of getting NULLs, maybe we should array_filter() first?

fago’s picture

Status: Needs work » Needs review

That would make it rather inconsistent, because a foreach over the list would get you a wrapper for each item. The same way just getting the identifiers does contain stale references by design (it's not loaded yet).
But directly using value() on the list would remain different results, what's inconsistent and might trigger odd bugs if code relies upon the same things to be there.

Consider

foreach ($list_wrapper as $wrapper) {
 $v[] = $wrapper->value();
}

and

foreach ($list_wrapper->value() as $entity) {
 $v[] = $entity;
}

Pretty much the same - so should be the results.

amitaibu’s picture

> That would make it rather inconsistent, because a foreach over the list would get you a wrapper for each item.

Why would array_filter() result in getting the wrapper -- It's just to remove the NULLs form the list, or maybe I missed what you meant in #13 ;)

fago’s picture

It would result in the two code-snippets from #13 getting different results in $v. Once with NULL values, once without.

rfay’s picture

Subscribe.

fago’s picture

Committed #11.

fago’s picture

Status: Needs review » Fixed
gambaweb’s picture

FileSize
1.11 KB

if I could still offer a different solution.
make sure that the we can load the entity as per
http://api.drupal.org/api/drupal/includes--common.inc/function/entity_lo...

Status: Fixed » Closed (fixed)
Issue tags: -D7 stable release blocker

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