by Karen StevensonJuly 15, 2009

Creating Custom CCK Fields

You can create custom CCK fields, widgets, and formatters for any situation, but it can be hard to see how to do it. I finally found time to create an 'Example' module that creates a simple field, formatter, and widget, with lots of embedded documentation about what belongs where. You need to create three files, an .info file, an .install file, and the module itself. The code below creates a very simple textfield, but it can be used as a starting point for any custom module. I'm also attaching a .zip file with the contents of this custom module.

The .info File

  
; $Id$
name = Example field
description = Defines an example field type.
dependencies[] = content
package = CCK
core = 6.x
  

The .install File

  
// $Id$
// Notify CCK when this module is enabled, disabled, installed, 
// and uninstalled so CCK can do any necessary preparation or cleanup.
/**
 * @file
 * Implementation of hook_install().
 */
function example_install() {
  drupal_load('module', 'content');
  content_notify('install', 'example');
}

/**
 * Implementation of hook_uninstall().
 */
function example_uninstall() {
  drupal_load('module', 'content');
  content_notify('uninstall', 'example');
}

/**
 * Implementation of hook_enable().
 *
 * Notify content module when this module is enabled.
 */
function example_enable() {
  drupal_load('module', 'content');
  content_notify('enable', 'example');
}

/**
 * Implementation of hook_disable().
 *
 * Notify content module when this module is disabled.
 */
function example_disable() {
  drupal_load('module', 'content');
  content_notify('disable', 'example');
}
  

The Module

  
// $Id$

/**
 * @file
 * An example to define a simple field, widget, and formatter.
 * A module could define only a field, only a widget, only a 
 * formatter, or any combination. Widgets and formatters must
 * declare what kind of field they work with, which can be any
 * existing field as well as any new field the module creates.
 */

//==========================================//
// DEFINING A FIELD
//==========================================//

/**
 * Implementation of hook_field_info().
 */
function example_field_info() {
  return array(
    // The machine name of the field, 
    // no more than 32 characters.
    'example' => array(
      // The human-readable label of the field that will be 
      // seen in the Manage fields screen.
      'label' => t('Example field'),
      // A description of what type of data the field stores.
      'description' => t('Store text data in the database.'),
      // An icon to use in Panels.
      'content_icon' => 'icon_content_text.png',
    ),
  );
}

/**
 * Implementation of hook_field_settings().
 */
function example_field_settings($op, $field) {
  switch ($op) {
    // Create the form element to be used on the field 
    // settings form. Field settings will be the same for 
    // all shared instances of the same field and should 
    // define the way the value will be stored 
    // in the database.
    case 'form':
      $form = array();
      $form['max_length'] = array(
        '#type' => 'textfield',
        '#title' => t('Maximum length'),
        '#default_value' => is_numeric($field['max_length']) ? $field['max_length'] : 255,
        '#required' => FALSE,
        
        // Use #element_validate to validate the settings.
        '#element_validate' => array('_example_length_validate'),
        '#description' => t('The maximum length of the field in characters. Must be a number between 1 and 255'),
      );
      return $form;
      
    // Return an array of the names of the field settings 
    // defined by this module. These are the items that 
    // CCK will store in the field definition
    // and they will be available in the $field array.
    // This should match the items defined in 'form' above.
    case 'save':
      return array('max_length');

    // Define the database storage for this field using 
    // the same construct used by schema API. Most fields
    // have only one column, but there can be any number 
    // of different columns. After the schema API values, 
    // add two optional values to each column, 
    //  'views', to define a Views field 
    //  'sortable', to add a Views sort field
    case 'database columns':
      $columns['value'] = array(
        'type' => 'varchar', 
        'length' => is_numeric($field['max_length']) ? $field['max_length'] : 255, 
        'not null' => FALSE, 
        'sortable' => TRUE, 
        'views' => TRUE,
      );
      return $columns;

    // Optional: Make changes to the default $data array 
    // created for Views. Omit this if no changes are 
    // needed, use it to add a custom handler or make 
    // other changes.
    case 'views data':
      // Start with the $data created by CCK
      // and alter it as needed. The following
      // code illustrates how you would retrieve
      // the necessary data.
      $data = content_views_field_views_data($field);
      $db_info = content_database_info($field);
      $table_alias = content_views_tablename($field);
      $field_data = $data[$table_alias][$field['field_name'] .'_value'];
      // Make changes to $data as needed here.
      return $data;
  }
}

/**
 * Custom validation of settings values.
 * 
 * Create callbacks like this to do settings validation.
 */
function _example_length_validate($element, &$form_state) {
  $value = $form_state['values']['max_length'];
  if ($value && !is_numeric($value)|| $value < 1 || $value > 255) {
    form_set_error('max_length', t('"Max length" must be a number between 1 and 255.'));
  }
}

/**
 * Implementation of hook_field().
 */
function example_field($op, &$node, $field, &$items, $teaser, $page) {
  switch ($op) {
    // Do validation on the field values here. The widget 
    // will do its own validation and you cannot make any 
    // assumptions about what kind of widget has been used, 
    // so don't validate widget values, only field values.
    case 'validate':
      if (is_array($items)) {
        foreach ($items as $delta => $item) {
          // The error_element is needed so that CCK can 
          // set an error on the right sub-element when 
          // fields are deeply nested in the form.
          $error_element = isset($item['_error_element']) ? $item['_error_element'] : '';
          if (is_array($item) && isset($item['_error_element'])) unset($item['_error_element']);
          if (!empty($item['value'])) {
            if (!empty($field['max_length']) && drupal_strlen($item['value']) > $field['max_length']) {
              form_set_error($error_element, t('%name: the value may not be longer than %max characters.', array('%name' => $field['widget']['label'], '%max' => $field['max_length'])));
            }
          }
        }
      }
      return $items;

    // This is where you make sure that user-provided 
    // data is sanitized before being displayed.
    case 'sanitize':
      foreach ($items as $delta => $item) {
        $example = check_plain($item['value']);
        $items[$delta]['safe'] = $example;
      }
  }
}

/**
 * Implementation of hook_content_is_empty().
 * 
 * CCK has no way to know if something like a zero is
 * an empty value or a valid value, so return
 * TRUE or FALSE to a populated field $item array.
 * CCK uses this to remove empty multi-value elements
 * from forms.
 */
function example_content_is_empty($item, $field) {
  if (empty($item['value'])) {
    return TRUE;
  }
  return FALSE;
}

/**
 * Implementation of hook content_generate().
 * 
 * Optional, provide dummy value for nodes created
 * by the Devel Generate module.
 */
function example_content_generate($node, $field) {
  $node_field = array();
  // Generate a value that respects max_length.
  if (empty($field['max_length'])) {
    $field['max_length'] = 12;
  }
  $node_field['value'] = user_password($field['max_length']);
  return $node_field;
}

/**
 * Implementation of hook_token_list() 
 * and hook_token_values().
 * 
 * Optional, provide token values for this field.
 */
function example_token_list($type = 'all') {
  if ($type == 'field' || $type == 'all') {
    $tokens = array();

    $tokens['example']['raw']       = t('Raw, unfiltered text');
    $tokens['example']['formatted'] = t('Formatted and filtered text');

    return $tokens;
  }
}

function example_token_values($type, $object = NULL) {
  if ($type == 'field') {
    $item = $object[0];

    $tokens['raw']  = $item['value'];
    $tokens['formatted'] = isset($item['view']) ? $item['view'] : '';
    return $tokens;
  }
}

//==========================================//
// DEFINING A FORMATTER
//==========================================//

/**
 * Implementation of hook_theme().
 */
function example_theme() {
  return array(
    // Themes for the formatters.
    'example_formatter_default' => array(
      'arguments' => array('element' => NULL),
    ),
    'example_formatter_plain' => array(
      'arguments' => array('element' => NULL),
    ),
  );
}

/**
 * Implementation of hook_field_formatter_info().
 * 
 * All fields should have a 'default' formatter.
 * Any number of other formatters can be defined as well.
 * It's nice for there always to be a 'plain' option
 * for the raw value, but that is not required.
 * 
 */
function example_field_formatter_info() {
  return array(
    // The machine name of the formatter.
    'default' => array(
      // The human-readable label shown on the Display 
      // fields screen.
      'label' => t('Default'),
      // An array of the field types this formatter 
      // can be used on.
      'field types' => array('example'),
      // CONTENT_HANDLE_CORE:   CCK will pass the formatter
      // a single value.
      // CONTENT_HANDLE_MODULE: CCK will pass the formatter
      // an array of all the values. None of CCK's core 
      // formatters use multiple values, that is an option
      // available to other modules that want it.
      'multiple values' => CONTENT_HANDLE_CORE,
    ),
    'plain' => array(
      'label' => t('Plain text'),
      'field types' => array('example'),
      'multiple values' => CONTENT_HANDLE_CORE,
    ),
  );
}

/**
 * Theme function for 'default' example field formatter.
 * 
 * $element['#item']: the sanitized $delta value for the item,
 * $element['#field_name']: the field name,
 * $element['#type_name']: the $node->type,
 * $element['#formatter']: the $formatter_name,
 * $element'#node']: the $node,
 * $element['#delta']: the delta of this item, like '0',
 * 
 */
function theme_example_formatter_default($element) {
  return $element['#item']['safe'];
}

/**
 * Theme function for 'plain' example field formatter.
 */
function theme_example_formatter_plain($element) {
  return strip_tags($element['#item']['safe']);
}

//==========================================//
// DEFINING A WIDGET
//==========================================//

/**
 * Implementation of hook_widget_info().
 *
 * Here we indicate that the content module will handle
 * the default value and multiple values for these widgets.
 *
 * Callbacks can be omitted if default handing is used.
 * They're included here just so this module can be used
 * as an example for custom modules that might do things
 * differently.
 */
function example_widget_info() {
  return array(
    // The machine name of the widget, no more than 32 
    // characters.
    'example_widget' => array(
      // The human-readable label of the field that will be 
      // seen in the Manage fields screen.
      'label' => t('Example widget'),
      // An array of the field types this widget can be 
      // used with.
      'field types' => array('example'),
      // Who will handle multiple values, default is core.
      // 'CONTENT_HANDLE_MODULE' means the module does it.
      // See optionwidgets for an example of a module that 
      // handles its own multiple values.
      'multiple values' => CONTENT_HANDLE_CORE,
      'callbacks' => array(
        // Who will create the default value, default is core.
        // 'CONTENT_CALLBACK_CUSTOM' means the module does it.
        // 'CONTENT_CALLBACK_NONE' means this widget has 
        // no default value.
        'default value' => CONTENT_CALLBACK_DEFAULT,
      ),
    ),
  );
}

/**
 * Implementation of hook_widget_settings().
 */
function example_widget_settings($op, $widget) {
  switch ($op) {
    // Create the form element to be used on the widget 
    // settings form. Widget settings can be different 
    // for each shared instance of the same field and 
    // should define the way the value is displayed to 
    // the user in the edit form for that content type.
    case 'form':
      $form = array();
      $size = (isset($widget['size']) && is_numeric($widget['size'])) ? $widget['size'] : 60;
      $form['size'] = array(
        '#type' => 'textfield',
        '#title' => t('Size of textfield'),
        '#default_value' => $size,
        '#element_validate' => array('_element_validate_integer_positive'),
        '#required' => TRUE,
      );
      return $form;

    // Return an array of the names of the widget settings 
    // defined by this module. These are the items that 
    // CCK will store in the widget definition and they 
    // will be available in the $field['widget'] array.
    // This should match the items defined in 'form' above.
    case 'save':
      return array('size');
  }
}

/**
 * Implementation of hook_widget().
 *
 * Attach a single form element to the form. 
 * 
 * CCK core fields only add a stub element and builds 
 * the complete item in #process so reusable elements 
 * created by hook_elements can be plugged into any 
 * module that provides valid $field information.
 * 
 * Custom widgets that don't care about using hook_elements
 * can be built out completely at this time.
 *
 * If there are multiple values for this field and CCK is 
 * handling multiple values, the content module will call 
 * this function as many times as needed.
 *
 * @param $form
 *   the entire form array, 
 *   $form['#node'] holds node information
 * @param $form_state
 *   the form_state, 
 *   $form_state['values'][$field['field_name']]
 *   holds the field's form values.
 * @param $field
 *   the field array
 * @param $items
 *   array of default values for this field
 * @param $delta
 *   the order of this item in the array of 
 *   subelements (0, 1, 2, etc)
 *
 * @return
 *   the form item for a single element for this field
 */
function example_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  
  $element['value'] = array(
    '#type' => 'textfield',
    '#default_value' => isset($items[$delta]['value']) ? $items[$delta]['value'] : NULL,
    '#autocomplete_path' => $element['#autocomplete_path'],
    '#size' => !empty($field['widget']['size']) ? $field['widget']['size'] : 60,
    '#attributes' => array('class' => 'example'),
    '#maxlength' => !empty($field['max_length']) ? $field['max_length'] : NULL,
  );
  
  // Used so that hook_field('validate') knows where to 
  // flag an error in deeply nested forms.
  if (empty($form['#parents'])) {
    $form['#parents'] = array();
  }
  $element['_error_element'] = array(
    '#type' => 'value',
    '#value' => implode('][', array_merge($form['#parents'], array('value'))),
  );
  
  return $element;
}