Setting the "Allowed Values Function" Property on Text Fields

Sean MacCath-Moran
#Development | Posted

Have you ever needed to populate a list field in Drupal (be it a radio, select, or checkboxes) on an entity, but needed it to use a set of values calculated elsewhere in the system? For example, imagine you're maintaining a list of valid sports which are only used as values and passed to an external data feed. In this scenario, the admin users can see the list and select items, and you're using that same list when processing the external data later -- but that's it; other than these two needs, there's nothing else that will access this list of sports values. Given its limited scope of use, it doesn't really make sense to maintain this list on the site as a taxonomy. It also doesn't feel right to create the array of values in code for use in the processing step and in the allowed values field on the field settings screen since maintaining the lists in two locations makes updating them annoying (at best) and prone to failure.

Introducing the Allowed Values Function setting.

NOTE: For the sake of the solutions explored in this article, suppose also that the site-code being worked on is maintained in a repository and that Features are being used to transmit changes to entities.

Wherez It From & Howz It Work?!

The good news is that the 'allowed_values_function' property is stored with the field settings of a basic Drupal installation. When a value is set for this property in the code, Drupal will hide the usual "Allowed values list" control on the field's admin screen:

Field sport value

The function that the 'allowed_values_function' is set to, should return an array of value/label pairs. In the case of the example being used in this article, we'll want a list of sports; e.g.:

  1. /**
  2. * Fetch list of sports names
  3. */
  4. function my_module_default_sports_list() {
  5. return array(
  6. 'baseball' => t('Baseball'),
  7. 'bowling' => t('Bowling'),
  8. 'crosscountry' => t('Cross Country'),
  9. . . .
  10. 'volleyball' => t('Volleyball'),
  11. 'waterpolo' => t('Water Polo'),
  12. 'wresting' => t('Wrestling'),
  13. );
  14. }

There's a wee fly in our ointment though: there's no ready way to set this value in an out-of-the-box Drupal install, so now we have a different set of problems to consider: i.e. what's the "right way" to get access to the field?

One might be initially tempted to write an install hook which manually updates the database, and there's some merit to this thinking. After all, it would be easy enough to do; the setting is stored in the "data" field of the "field_config" table as a serialized array of values. However, part of the problem with updating the database directly is a philosophical issue: this "data" field is created and maintained by Drupal core with the clear implication that it's meant to be "private", which is to say that the code is written with the expectation that nothing else will be modifying this field value but itself. Changing this value via a database update violates the intended use of the field. This brings us to the other part of the problem with direct modifications: when a paradigm exists that has the implicit expectation of private control, violating that expectation has the potential to cause all manner of unforeseen problems since that code's maintainers aren't seeking to be accommodating to anyone else's needs but their own.

Fortunately, there are a couple of other ways to get at setting this value, each with its own pros and cons.

Do It With: Hook Update

My preferred method of enacting this update is via code. This method leaves a more-or-less clear trail for future coders to follow when they're attempting to decipher what has happened with the field settings over time, and I rather like the unambiguous nature of this approach. Further, there's little chance for unintended side effects when using this method. The down side is that it requires a bit more effort up front to set it up.

Keep in mind that the goal in this scenario is to get the field settings adjusted and to then re-export the Feature which contains the entity with this field attached to it. There are just a few lines of code to run a single time in order to set this value on the field, and there are a number of places this can be made to happen. For example, one could place it in a hook_init() function on a dev environment, run it, and then delete the code. However, while this would be a quick way to "get 'er done", it leaves a bit of a mystery for the next coder.

Instead, if you're writing the code anyway, why not save it for posterity? To this end, I advise placing this code in an update hook. As an added bonus, I'm including here a check to determine if the field presently exists before attempting to modify it -- this is useful in situations where an existing Feature's entity is having a field added to it at the same time as the allowed_values_function setting on that field is being modified. In such a case, the Feature will need to be reverted (in order to create the field) before updates can run, and this is what the following table check accomplishes. A nice benefit to this appraoch is that it works out correctly even if the Feature is updated with the new allowed_values_function in the dev space and exported since a one-time rerun of this the code has no ill effect. Withal, creating an update hook provides the maximum visibility and flexibility for this operation.

  1. /**
  2. * Set the my_module_default_sports_list() function as the source of the 'default_sports' field
  3. */
  4. function my_module_update_7099() {
  5. if (!db_table_exists('field_data_field_default_sports')) {
  6. $out = array(
  7. '#abort' => array(
  8. 'success' => FALSE,
  9. 'query' => 'The field_data_field_default_sports table doesn't exist yet, which likely means that the My Module Feature hasn't been reverted yet (which needs to happen before this update can run).'
  10. )
  11. );
  12. return $out;
  13. }
  15. $default_sports_field = field_info_field('field_default_sports'); // machine name of field
  16. unset($default_sports_field['settings']['allowed_values']); // having this set interferes with the allowed_values_function value
  17. $default_sports_field['settings']['allowed_values_function'] = 'my_module_default_sports_list'; // function name that provides array of values
  18. field_update_field($default_sports_field);
  19. }

After running this, the Feature can be exported in order to preserve the change and communicate it to other environments.

Do It With: Features Edit

Of course, reading through the previous section begs the question of why we don't just update the Feature code manually and skip all this other coding nonsense. For me, the problem with doing so is the same philosophical issue as discussed regarding a direct update to the database: I view the Feature code as being something maintained by a system which expects to have full control over it. But the truth is that this consideration doesn't generally apply to Features as strongly as it does with the database field we'd looked at. The Features module makes some efforts to gracefully accommodate unmanaged changes made to its code.

For those that choose this path, I'll leave the vagaries of how to increment the Feature version and revert the changes as a problem for you to tackle. First, we need the field to modify, so if it's being newly added then do so in the content type and re-export the feature. From there, adjusting the resulting code to include a new allowed_values_function is fairly trivial. Building on the example above, one would look in the file and track down the field_instances definition for node-my_module_contest_forms-field_default_sports.

If there isn't an array on a "settings" key defined at this location already, it's (fairly) safe to add it. This will then need an "allowed_values" key added with an empty array assigned to it, and an "allowed_values_function" key set to the name of the function providing a value array -- which is "my_module_default_sports_list" in this example:

  1. $field_bases['field_default_sports'] = array(
  2. 'active' => 1,
  3. 'cardinality' => -1,
  4. . . .
  5. <strong>'settings' => array(
  6. 'allowed_values' => array(),
  7. 'allowed_values_function' => 'my_module_default_sports_list',
  8. ),</strong>
  9. . . .
  10. );

What You Get For Your Efforts

Regardless of the method used to populate the allowed_values_function property, the result is as you would expect. The field instances are populated by pulling data from the provided function; e.g.:
Happy coding! If you are in the mood to learn more about Features, check out Hunter Fox's blog post "How to Update Several Feature Exports All At Once"

Sean MacCath-Moran