At the last Oxford Drupal User Group meetup, Mike Harris demonstrated an amazing tracking system for orders, purchases, and accounts payable, built in Drupal just using nodes augmented with Field Collection, Views PDF... and Computed Field.
Oh, Computed Field. Wherever I've worked - first at Torchbox, now as a freelance - I've always had a policy of avoiding storing PHP in the database, for all sorts of reasons: maintainability, robustness, versioning, security.... But Mike's results were so darned impressive. How conflicted I felt, as I saw what he'd built "just work" in a way that my supposedly superior code often didn't.
With that still in my mind, later that same week I wondered to myself: how easy would it be to replicate the functionality Computed Field, in a module structured so that almost anyone can build - practically by example? Obviously you can do anything you like in PHP code, but: how easy would it be to do it - simply, easily, repeatably? That way the same functionality could be put together, but with all the security and maintainability improvements that come from versioning, IDEs, and not permitting PHP execution through Drupal's GUI.
I worked out you could indeed do it, by leveraging a specific Drupal 7 hook. Here's how.
Example content type
Let's look at a simple CF example. A content type where each node stores a number of people and a number of apples, and what's required is an on-the-fly calculation - at the point of node save, which is how CF does it - of how many apples can be shared out per person.
My content type therefore has the following four fields:
- field_num_apples (integer): number of apples available
- field_num_people (integer): number of people to share the apples between
- field_apples_per_person (computed field): see below.
- field_apples_per_person_2 (decimal field): see further below
CF takes over the third field above; but the fourth field will remain empty until I build a dedicated module below.
Firstly, here's the code I put in the computed field configuration, to calculate the result:
$entity_field[0]['value'] = array_pop(array_pop(field_get_items($entity_type, $entity, 'field_num_apples'))) / array_pop(array_pop(field_get_items($entity_type, $entity, 'field_num_people')));
It's a simple division of one field by another, using the array_pop()
syntax actually suggested by CF help. It's fairly simple to work out what's going on, but as we'll see it's just as simple when it's in a module - if you embed it in the right structure.
Module equivalent
Here's we build the equivalent functionality, but in a module. In sites/MYSITE/modules, create a subdirectory called "mycompfield". Then, add the following files within that directory:
Filename: mycompfield.info
name = My Computed Fields description = Computed fields, except using code under version control core = 7.x
Filename: mycompfield.module
<?php /** * Implements hook_field_storage_pre_update(). */ function mycompfield_field_storage_pre_update($entity_type, $entity, &$skip_fields) { // Set the number of apples per person $entity->field_apples_per_person_2[$entity->language][0]['value'] = array_pop(array_pop(field_get_items($entity_type, $entity, 'field_num_apples'))) / array_pop(array_pop(field_get_items($entity_type, $entity, 'field_num_people'))); }
And that's it! For my small example content type, anyway. You can see that I've used hook_field_storage_pre_update()
to change the value, but I'm letting core Drupal carry on and save the content of that field for me.
With this code in place, then if you save the node, the second field should get populated as if it were powered by CF. You should be able to see the clear parallels between the code that went into the CF configuration screen: if you rewrite the $entity_field[0]...
bit of the CF code, you can see it's almost a drop-in replacement.
That works fine, but it's not extensible or robust: for a start, I'd need to add a lot more logic if I were dealing with many entity types and bundles (content types), with fields that might or might not exist. Very quickly it could get very complicated. But here's a quick fix that should help extend the code very easily - and also make moving away from CF a little bit easier.
Improvement: a function for each field
/** * Implements hook_field_storage_pre_update(). */ function mycompfield_field_storage_pre_update($entity_type, $entity, &$skip_fields) { foreach($entity as $key => &$value) { $function = "_mycompfield_compute_{$entity_type}_{$key}"; if (function_exists($function)) { // Drill down into language to be the equivalent of Computed Field $languaged_value =& $entity->{$key}[$entity->language]; $function($entity_type, $entity, $languaged_value); } } } function _mycompfield_compute_node_field_apples_per_person_2($entity_type, $entity, &$entity_field) { $entity_field[0]['value'] = array_pop(array_pop(field_get_items($entity_type, $entity, 'field_num_apples'))) / array_pop(array_pop(field_get_items($entity_type, $entity, 'field_num_people'))); } function _mycompfield_compute_node_ANOTHER_FIELD($entity_type, $entity, &$entity_field) { /* ... Drop in your CF code here */ } function _mycompfield_compute_ANOTHER_ENTITY_TYPE_SOME_FIELD($entity_type, $entity, &$entity_field) { /* ... Drop in your CF code here */ }
... and so on: this pattern is extensible for as many combinations of entity types and field names that you might care to imagine. Also, you can see at the same time that I've solved the $entity_field...
issue: you no longer have to edit your CF configuration before just dropping it in between the curly brackets of the function definition.
Summary
I like what Computed Field does, but not how it does it. These patterns let you very quickly build a pseudo-CF module, which you can then submit to version control, easily roll back or switch off if you need to etc. In that way, it combines the best of both worlds: powerful in-Drupal field calculations, with the stability and reliability of versionable code.