20 APIs in 20 Days: Using CCK to Create New Field Types

Error message

The spam filter installed on this site is currently unavailable. Per site policy, we are unable to accept new submissions until that problem is resolved. Please try resubmitting the form in a couple of minutes.

This post is an entry in our 20 APIs in 20 Days series. Learn more about how best practices lead to sustainable development at www.trellon.com.

CCK is the most important acronym you need to know in relation to content management in Drupal. It stands for Content Construction Kit and is the framework that allows people to enter all kinds of different information in their web sites.

As with any Drupal tool, the beauty of CCK is not in its functionality alone, but in the ability of other modules to extend that functionality. The CCK API allows other modules to define unique field types that integrate seamlessly into the Drupal platform, which has even made its way into Drupal core in version 7.

The most well-known modules that leverage this ability include the Filefield, Imagefield, Emfield, Link and Email modules, but there are almost 300 contributed modules tagged as CCK-related for Drupal 6. Regardless of scope or application, all of these modules have taken advantage of CCK’s API to define unique field types which become available to any node content within Drupal.

As a straight-forward example, we’re going to step through Matthias Hutterer’s Email module. This module leverages the API to provide a unique CCK field type dedicated to properly formatted email addresses. We will identify the key functions that a module must implement to define a custom field type, and will provide explanation of configurable options along the way.

Let’s begin with a list of ingredients (our hooks):

  • Let CCK know you’re there (Installation):
    • content_notify
  • Tell CCK about yourself (Field Settings):
    • hook_field_info
    • hook_field_settings
    • hook_field
  • Tell CCK and Drupal how you’ll behave on node edit forms (Widget Settings):
    • hook_widget_info
    • hook_widget_settings
    • hook_widget
    • hook_elements
  • Tell Drupal how you’ll look when a node is viewed (Theming):
    • hook_theme + theme_MY_ELEMENT
    • custom formatters

That’s a long list, so let’s get cracking.

Installation

First, we’ll need to let CCK know when our module is available for use:

function email_install() {
  drupal_load('module', 'content');
  content_notify('install', 'email');
}

In our module’s *.install file, we let CCK know when our module is installed, enabled, disabled and uninstalled. We do this by calling the appropriate hook (hook_install, hook_enable, etc.) and executing CCK’s ‘content_notify’ function with the appropriate parameter (‘install’, ‘enable’, etc.) inside that hook.

Field Settings

In the email.module file, we see the field take shape:

function email_field_info() {
  return array(
    'email' => array(
      'label' => 'Email',
// function concludes...

Our field now has now been registered with an internal name, ‘email’, and an administrative label, ‘Email’.

function email_field_settings($op, $field) {
  switch ($op) {
    case 'database columns':
      $columns['email'] = array('type' => 'varchar', 'length' => 255, 'not null' => FALSE, 'sortable' => TRUE);
      return $columns;
  }
}

Here, the Email module is telling CCK that it wants CCK to handle its database storage. It wants a special column made in a content type’s database table when that content type uses the Email field. It wants the column to be named ‘field_NAME_OF_FIELD_email’ and to have specific database settings configured. In addition to specifying database columns on the $op ‘database columns’, a custom settings form can be generated on $op ‘form’ (see the CCK ‘text’ module for an example).

Finally, we’re going to perform some custom validation when our field gets a value:

function email_field($op, &$node, $field, &$items, $teaser, $page) {
  switch ($op) {
    case 'validate':
      if (is_array($items)) {
        foreach ($items as $delta => $item) {
          if ($item['email'] != '' && !valid_email_address(trim($item['email']))) {
            form_set_error($field['field_name'],t('"%mail" is not a valid email address',array('%mail' => $item['email'])));
          }
        }
     }
     break;
// function concludes...

If there’s a value, this verifies the validity of the email address by calling Drupal’s valid_email_address function.

Widget Settings

Once we’ve modeled the logical elements of our field type as seen above, we need to define its own custom form widget, so the final HTML form element can be assigned the appropriate attributes.
Just like the hook_field* functions above, the hook_widget* functions have _info and _settings variants:

function email_widget_info() {
  return array(
    'email_textfield' => array(
      'label' => t('Text field'),
      'field types' => array('email'),
      'multiple values' => CONTENT_HANDLE_CORE,
      'callbacks' => array(
        'default value' => CONTENT_CALLBACK_DEFAULT,
      ),
    ),
  );
}

function email_widget_settings($op, $widget) {
  switch ($op) {
    case 'form':
      $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('_email_widget_settings_size_validate'),
        '#required' => TRUE,
      );
      return $form;

    case 'save':
      return array('size');
  }
}

Notice where hook_widget_info mimics hook_field_info (name and label), but also tells CCK which field types this widget applies to (‘field types’ => array(‘email’)).

Just like in hook_field_settings, in hook_widget_settings we get a chance to enhance the settings form (we see the aggregate of both these functions on the settings form at “admin/content/node-type/[NAME OF CONTENT TYPE]/fields/[NAME OF FIELD]”). The Email module performs the same enhancement that the CCK Text module does; it checks to see if a text-field size has been set, and if one hasn’t, it defaults to 60.

To get our form element attached to the form, we call hook_widget:

function email_widget(&$form, &$form_state, $field, $items, $delta = 0) {
  $element = array(
    '#type' => $field['widget']['type'],
    '#default_value' => isset($items[$delta]) ? $items[$delta] : '',
  );
  return $element;
}

So when CCK attaches a form element to a form, we tell it that it should set the appropriate type (we only defined one in our hook_widget_info function, ‘email_textfield’) and it should use a default value we’ve specified if available. If our module employed multiple widgets, we could use conditional statements here to add attributes based on the widget type, but in most cases the above function will suffice (see NodeReference.module for an example of per-widget customization).

Finally, after we’ve updated CCK with everything there is to know about our new field, we have to let Drupal’s Form API (FAPI) know about it as well. To do this, we call hook_elements:

function email_elements() {
  return array(
    'email_textfield' => array(
      '#input' => TRUE,
      '#columns' => array('email'),
      '#delta' => 0,
      '#process' => array('email_textfield_process'),
    ),
  );
}

Notice that we’re not passing in ‘email’ as the element, but ‘email_textfield’; FAPI is interested in our new widget, which we’ve logically connected to our field back in hook_widget_info. Further, we tell FAPI where to go to process this new element: ‘#process’ => array(‘email_textfield_process’).

All of the functions up to this point have dealt with configuring our new field – defining its settings and describing our new widget. None of them have dealt with how Drupal will actually render our widget as a standard HTML form element on a node edit form. This is accomplished in the function assigned to the #process attribute we just saw in hook_elements:

function email_textfield_process($element, $edit, $form_state, $form) {
  $field = $form['#field_info'][$element['#field_name']];
  $field_key = $element['#columns'][0];
  $delta = $element['#delta'];
  $element[$field_key] = array(
    '#type' => 'textfield',
    '#title' => $element['#title'],
    '#description' => content_filter_xss($field['widget']['description']),
    '#required' => $element['#required'],
    '#maxlength' => 255,
    '#size' => !empty($field['widget']['size']) ? $field['widget']['size'] : 60,
    '#attributes' => array('class' => 'text', 'dir' => 'ltr'),
    '#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL,
  );
  return $element;
}

For those accustomed to the Form API, this should be much more familiar. Our field is going to be a ‘textfield’, but an overhauled one with all the custom field and widget settings we defined above (where you see [‘widget’]) as well as some core CCK values ($element[‘#title’] is set by CCK when you assign a “Label” value to a new field on the Manage Fields page).

Now we’ve told CCK about our field element and its widget, we’ve told FAPI how to generate that element on a node edit page, but how does the data we collect get rendered when the node it's in is viewed? Finally, we attack the theme layer.

Theming

When we call hook_elements, Drupal automatically expects theme functions named after the elements we declare. So for Email, we had ‘email_textfield’ as the one element we declared, so we need to include a theme_email_textfield function to take care of the default example. Even though Drupal makes the connection between the form element and the theme function automatically, we still need to declare our theme-able function in hook_theme per usual:

function email_theme() {
  return array(
    'email_textfield' => array(
      'arguments' => array('element' => NULL),
    ),
// more theme functions declared here…
  );
}

function theme_email_textfield($element) {
  return $element['#children'];
}

The theme_email_textfield function just returns the default HTML value generated in Drupal’s includes/form.inc . However, there are more values in $element[‘field_name’] and $element[‘delta’], (the name and position of the element respectively) that can be used for custom theming.

As a final flourish to your CCK theming tasks, you could provide further options to the end-user by creating custom formatters, which are made available on the "Display" settings page for every content type(admin/content/node-type/[NAME OF CONTENT TYPE]/display). The Email module calls hook_field_formatter_info to register its custom formatters, declares them in hook_theme, then creates the theme_CUSTOM_FORMATTER functions to define the final HTML output. To use one of these formatters, users can navigate to the Display settings page and pick one from a drop down.

----------------------------

We’ve only scratched the surface here, despite the length of this post. For more details about the functions used in this post, take a look at the modules that come packaged with CCK like ‘text’ and ‘nodereference’, which contain detailed code comments for each function. And be sure to catch all of Trellon’s API blog posts as we explore the fundamental tools for extending Drupal’s functionality.