D7: How not to use hook_entity_*

Roman Zimmermann's picture
Roman Zimmermann

Recently I came in a situation where I wanted to extend all entities of a specific type (payment) to reliably provide (and store) an additional property. At first glance this seems like a no brainer: simply use hook_entity_* to save/update/load the property to or from the database and that should do it. Turns out it isn't … and there is a lot to learn about how entities work in D7.

Lets take a look at a specific example.

Example: store a payment context object with payments

For payment_context I wanted to have a context object as a property of payments. The objects ids are put into $payment->context_data (which is automatically serialized by the payment module) and the class/type is stored in $payment->context as a string.

First attempt: use $payment->context


// For brevity the "Implements"-comments and the ifs
// that limit the functionality to the payment entity-type are left out.

function example_entity_presave($entity, $entity_type) {
  $payment->context_data = $payment->context->ids();
  $payment->context = $payment->context->type();
  // The payment module takes care of storing and serializing / unserializing those.
}

function example_entity_load($entites, $entity_type) {
  foreach ($entities as $entity) {
    $entity->context = PaymentContext::create($entity->context, $enity->context_data);
  }
}

That looks good. Lets try it out:


$p = new Payment();
$p->context = new SomeContext();
// example_entity_presave() will be called before saving
// replacing $payment->context with $payment->context->type().
entity_save('payment', $p);

// Fatal error: Call to a member function doSomething() on a non-object.
$p->context->doSomething();

EDIT: If the entity is manipulated in hook_entity_presave() this essentially creates two states of the object: One from creation/loading until saving and another after saving (until the next load). Any code that uses the entity then needs to handle both cases. This makes it cumbersome to work with the entity.

Lesson learned: Never use hook_entity_presave() to manipulate entities!

EDIT2: The same holds for hook_entity_insert/update() and hook_field_presave/insert/update() for the very same reason.

Second attempt: Put the context object somewhere else.


function example_entity_presave($entity, $entity_type) {
  $payment->context_data = $payment->contextObj->ids();
  $payment->context = $payment->contextObj->type();
}

function example_entity_load($entites, $entity_type) {
  foreach ($entities as $entity) {
    $entity->contextObj = PaymentContext::create($entity->context, $enity->context_data);
  }
}

… and try it out …


$p = new Payment();
$p->contextObj = new SomeContext();
entity_save('payment', $p);

// yeeeha!
$p->contextObj->doSomething();

Sadly it's not that easy. If we deal with $payment objects created by other modules (that don't know anything about our context object) we still can't rely on $payment->contextObj to be set. So we have to check for it's existence every time we wan't to use it - or risk a fatal error … except if we somehow had a way of adding it to every newly created payment - but there isn't.

Lesson learned: There is no hook_entity_object_prepare()! - like there is for nodes.

Turns out the only way to achieve such an addition reliably is by overriding the entity class or the controller class.

BTW: You run into similar issues with the field API

Share this!

Comments

You don't explain why your first attempt didn't work. Was the data not saved? Did your load function not retrieve the data? Did it fail to create an object?

Roman Zimmermann's picture

entity_save() triggers a call to hook_entity_presave(). In the implementation of the hook above the value of $payment->context is replaced by a string. So any code that uses $payment->context after the payment has been saved won't be able to access the object. Any code that can't tell if the payment will be saved before it is called, needs to work with both the string and the object.

I've added an explanation to the blog post. Thanks for your input!

Thanks for the clarification! So the more nuanced lesson is perhaps more like: don't rely on hook_entity_presave for data that must be transformed for storage?

Roman Zimmermann's picture

Yes, you shouldn't use hook_entity_presave() for transforming data for storage - but the same holds for: hook_entity_insert(), hook_entity_update(), hook_field_presave(), hook_field_insert(), hook_field_update() -- for the same reason.

If you need to transform an entity for storage there is simply no hook that you can use safely.

Pages