Bypassing Drupal Commerce customer profile duplication
In Drupal Commerce, we deal in entities, fields, and field-based relationships between entities (i.e. references). The extent to which we implemented our data model on these systems from the earliest days of Drupal 7 is what grants Drupal Commerce developers a level of flexibility previously unavailable to eCommerce developers in general. Cool, right?
Unfortunately, the Drupal entity trade can be dangerous when dealing with historical data that isn't supposed to change - when a reference should do more than merely "refer" but also express constraints.
Such is the case with customer profile references on orders.
Once customer information (e.g. a billing or shipping address) is entered for an order, we don't want the order to lose that information at a later date. This means we have to prevent not just the deletion of the referenced customer profiles but also changes to them - the latter because our references are to entitiies, not revisions of entities.
Side note: honestly, I'm fine with that - I can't imagine a reasonable UI or update strategy for entity revision references. It's hard enough to keep straight as is.
The most common place users encounter customer profile duplication is when they go to update a customer profile through the UI. If any field values are changed, the Order module will prevent the update if it detects the customer profile is referenced by a non-shopping cart order. It empties the profile IDs in a presave hook, forcing the save to create a new customer profile instead of updating the existing one.
This same process affects not just customer profiles being edited through the UI but also customer profiles being updated through code. However, if you combine the fact that customer profiles have revisions with the absence of the duplication related messages you see when performing the operation in the UI, it's easy to see how this functionality might appear to developers as a case of revisions gone awry.
There are a couple of functions developers can refer to to read comments describing the process and see the implementation itself:
- commerce_order_commerce_customer_profile_presave()
- commerce_order_commerce_customer_profile_can_delete()
It's worth noting that we've coded this functionality to be paranoid - if there's a slight chance something substantive may have changed in the field data, we force duplication instead of updating the original. Better to duplicate than to lose vital data.
Still, the keen observer will note that we actually do permit customer profiles to be updated through the UI on one condition. If an administrator edits a customer profile through the edit form of the sole order referencing the profile, we permit the update.
You may have a need in custom code to perform an update to a customer profile field that you know does not affect the historical record in a substantive way. Maybe it's simply a matter of changing some internal field that has no bearing on the fulfillment of orders. In such cases, to bypass customer profile duplication, you can imitate the process the order edit form uses to identify an order as safe to delete.
To do this, add a temporary "entity_context" property to the customer profile object. This is the property the Order module looks for in the presave hook to determine if the customer profile is being edited in the context of its sole referencing order. If you properly identify this order in the entity_context, the Order module will permit the update to occur without duplication:
<span style="color: #000000"><span style="color: #0000BB"><?php<br>$profile </span><span style="color: #007700">= </span><span style="color: #0000BB">commerce_customer_profile_load</span><span style="color: #007700">(</span><span style="color: #0000BB">1</span><span style="color: #007700">);<br></span><span style="color: #0000BB">$wrapper </span><span style="color: #007700">= </span><span style="color: #0000BB">entity_metadata_wrapper</span><span style="color: #007700">(</span><span style="color: #DD0000">'commerce_customer_profile'</span><span style="color: #007700">, </span><span style="color: #0000BB">$profile</span><span style="color: #007700">);<br></span><span style="color: #0000BB">$wrapper</span><span style="color: #007700">-></span><span style="color: #0000BB">field_referral_source </span><span style="color: #007700">= </span><span style="color: #DD0000">'MySpace'</span><span style="color: #007700">;<br></span><span style="color: #0000BB">$profile</span><span style="color: #007700">-></span><span style="color: #0000BB">entity_context </span><span style="color: #007700">= array(<br> </span><span style="color: #DD0000">'entity_type' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'commerce_order'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'entity_id' </span><span style="color: #007700">=> </span><span style="color: #0000BB">1</span><span style="color: #007700">,<br>);<br></span><span style="color: #0000BB">commerce_customer_profile_save</span><span style="color: #007700">(</span><span style="color: #0000BB">$profile</span><span style="color: #007700">);<br></span><span style="color: #0000BB">?></span></span>
Obviously, this is obtuse.
In Commerce 2.x, we'll do well to improve the developer experience here. It gets even worse if you want to make such an update on a site using Commerce Addressbook where you do want to update a customer profile referenced by multiple orders without enacting duplication. In fact, until I wrote this post, there would have been no way to achieve this short of a miraculous hook_query_alter().
This whole post came out of a quick e-mail exchange with Forest Mars, whom I'm looking forward to meeting at next week's Florida DrupalCamp (you going?). Since I've had a chance to think about the issue in a longer form post, I'm going to go ahead and add a query tag so it's at least possible to target the "can delete" query without committing developer sins.
And that is how e-mail and blogging improve open source projects, my friends!