Drupal create custom compound field types

Note Feb 20, 2015: Here's another post on creating a custom compound field: http://blog.openlucius.com/en/blog/coding-custom-compound-drupal-fields

Note Mar 8, 2016: Here's a post about creating a compound field for Drupal 8: http://www.ixis.co.uk/blog/drupal-8-creating-field-types-multiple-values

This post is mostly copied from http://getlevelten.com/blog/ian-whitcomb/defining-custom-field-types-drupal just in case that page ever goes away.

Have you ever been in a situation where you needed to collect a certain type of data on a node form and display it in a certain way that wasn't supported by the core Drupal field types? Rather than using a text field to display something like a phone number or email address, you may consider using the phone or email modules instead, both of which are perfect examples of why you may need to define your own custom field types.

For the sake of this example we will define a field type for inputting a vehicle license plate number and state (I have no idea why you would need this). First, we'll need to define the types of data stored in these fields in the .install file usinghook_schema.

<?php
/**
 * Implements hook_field_schema().
 */
function license_plate_field_schema($field) {
  return array(
    'columns' => array(
      'plate_number' => array(
        'type' => 'varchar',
        'length' => 8,
        'not null' => TRUE,
      ),
      'state' => array(
        'type' => 'varchar',
        'length' => 2,
        'not null' => FALSE,
      ),
    ),
  );
}

Next, tell the fields module about your new field type using hook_field_info in your .module file. The widget is the input form and the formatter is the field display.

<?php
/**
 * Implements hook_field_info().
 */
function license_plate_field_info() {
  return array(
    'license_plate' => array(
      'label' => t('License Plate'),
      'description' => t('This field stores a license plate number in the database.'),
      'default_widget' => 'license_plate_field',
      'default_formatter' => 'license_plate_default',
    ),
  );
}

Create your field widget type(s) using hook_field_widget_info. You'll select the widget when adding the field to a content type, which would allow you to specify multiple input formats for this particular field. In most cases, you'll probably only need one of these (or none if you're using one of the core widgets). In this case, since we're collecting the plate number and state as separate pieces of data, we'll need to create a widget with those fields.

/**
 * Implements hook_field_widget_info().
 */
function license_plate_field_widget_info() {
  return array(
    'license_plate_field' => array(
      'label' => t('Text field'),
      'field types' => array('license_plate'),
    ),
  );
}

You can define the field settings form using hook_field_settings_form. This will show up in the Global Settings sections of the field configuration page upon adding it to the node and will apply to this field across all instances of it.

/**
 * Implements hook_field_settings_form().
 */
function license_plate_field_settings_form($field, $instance, $has_data) {
  $settings = $field['settings'];
  // Add your global settings fields here
  $form = array();
  return $form;
}

You can also use hook_field_instance_settings_form to define settings that are specific to the entity type which you are adding the field to. This would enable you to use the same field across different content types and change these settings.

/**
 * Implements hook_field_instance_settings_form().
 */
function license_plate_field_instance_settings_form($field, $instance) {
  $settings = $instance['settings'];
  // Add your instance settings fields here.
  $form = array();
  return $form;
}

The input form (ie: widget) can be built using hook_field_widget_form. The switch case is necessary if you're defining multiple widgets.

/**
 * Implements hook_field_widget_form().
 */
function license_plate_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
  switch ($instance['widget']['type']) {
    case 'license_plate_field' :
      $element['license_plate'] = array(
        '#type' => 'fieldset',
        '#title' => $element['#title'],
        '#tree' => TRUE,
      );
      $element['license_plate']['plate_number'] = array(
        '#type' => 'textfield',
        '#title' => t('Plate number'),
        '#default_value' => isset($items[$delta]['plate_number']) ? $items[$delta]['plate_number'] : '',
        '#required' => $element['#required'],
        '#size' => 8,
        '#attributes' => array('maxlength' => 8),
      );
      $element['license_plate']['state'] = array(
        '#type' => 'select',
        '#title' => t('State'),
        '#default_value' => isset($items[$delta]['state']) ? $items[$delta]['state'] : '',
        '#options' => array(
          'AL' => 'Alabama',
          'AK' => 'Alaska',
          'AZ' => 'Arizona',
        ),
        '#required' => $element['#required'],
      );
      break;
  }
  return $element;
}

hook_field_presave can be used to prepare the data from the widget to be inserted into the database. Since our widget uses multiple fields nested inside a fieldset, we will need to map the field values to the correct place in the data array to be nested in the database.

/**
 * Implements hook_field_presave().
 */
function license_plate_field_presave($entity_type, $entity, $field, $instance, $langcode, &$items) {
  foreach ($items as $delta => $item) {
    if (isset($item['license_plate']['plate_number'])) {
      $items[$delta]['plate_number'] = $item['license_plate']['plate_number'];
      $items[$delta]['state'] = $item['license_plate']['state'];
    }
  }
}

Use hook_field_is_empty to tell Drupal how to check if the field is empty and handle it accordingly. This is necessary if you mark the field as required.

/**
 * Implements hook_field_is_empty().
 */
function license_plate_field_is_empty($item, $field) {
  if (empty($item['license_plate']['plate_number'])) {
    return TRUE;
  }
  return FALSE;
}

Now setup the field validation callback with hook_field_validate. In this case we really don't need any validation, but in case you do, here's how to do it.

/**
 * Implements hook_field_validate().
 */
function license_plate_field_validate($entity_type, $entity, $field, $instance, $langcode, $items, &$errors) {
  // Loop through field items in the case of multiple values.
  foreach ($items as $delta => $item) {
    if (isset($item['plate_number']) && $item['plate_number'] != '') {
   if (!valid_license_plate($item['plate_number'])) {
        $errors[$field['field_name']][$langcode][$delta][] = array(
          'error' => t('Invalid license plate number.'),
        );
      }
    }
  }
}

Now you'll need to define your formatter(s) and specify the field output using hook_field_formatter_info andhook_field_formatter_view.

/**
 * Implements hook_field_formatter_info().
 */
function license_plate_field_formatter_info() {
  return array(
    'license_plate_default' => array(
      'label' => t('Default'),
      'field types' => array('license_plate'),
    ),
  );
}
/**
 * Implements hook_field_formatter_view().
 */
function license_plate_field_formatter_view($entity_type, $entity, $field, $instance, $langcode, $items, $display) {
  $element = array();
  switch ($display['type']) {
    case 'license_plate_default' :
      foreach ($items as $delta => $item) {
        if (isset($item['plate_number'])) {
          $element[$delta]['#markup'] = $item['state'] . ' ' . $item['plate_number'];
        }
      }
      break;
  }
  return $element;
}

And voila! a shiny new license plate field type for use anywhere on your entities, nodes, and users.

Tags

External References

Article Type

General