Updating CCK Modules from 5.x to 6.x

There are significant changes in CCK field modules in 6.x compared to 5.x. This document identifies the changes that developers will need to make in their modules.

Contents

Summary

A number of new hooks and functions are available in the D6 version, read down for more details:

New hooks:

  • hook_content_is_empty()
  • hook_content_fieldapi()

New functions:

  • content_notify()
  • content_module_delete()
  • content_field_form()
  • content_field_default_values()
  • content_instance_default_values()
  • content_field_instance_create()
  • content_field_instance_read()
  • content_field_instance_update()
  • content_field_instance_delete()

Miscellaneous

  • The content module can handle multiple values, and provides options to either select a specific number of multiple values to provide, or allow for unlimited multiple values using the AHAH 'add more' button processing.
  • To properly handle multiple values, CCK needs a reliable method to tell if a field is to be considered empty. To do this, each field module is expected to add a hook_content_is_empty() that will be passed an individual delta value and the field array. That hook should return TRUE if the item should be considered empty and FALSE otherwise. This system makes it possible for fields that have multiple columns to declare an element is empty only if a specific column is missing a value.
    <?php
    /**
    * Implementation of hook_content_is_empty().
    */
    function userreference_content_is_empty($item, $field) {
      if (empty(
    $item['uid'])) {
        return
    TRUE;
      }
      return
    FALSE;
    }
    ?>
  • New functions are available to create 'default' field and widget arrays -- content_field_default_values() and content_instance_default_values(). These can be used to help expose field structure to external modules or as a starting point to creating field and widget arrays.
  • Drupal 6 uses E_ALL testing, which creates errors anytime a field array is missing any indexes. Field arrays are merged with the default value arrays to be sure all parts of the field and widget have indexes set so they never create undefined index errors. This means all field values will always return true to isset(), and module authors should not use isset() to test if values exist for form elements on the field settings form. Instead use is_array(), is_numeric() or similar tests.
  • Update or add install(), uninstall(), enable() and disable() hooks for your module so each of those hooks include the function content_notify(). This will notify the content module that a module is being added or removed so it can remove fields, clean up views, and take other necessary action. Without this hook, the content module has no way to know what is going on with its field modules. The recommended place for all four of these hooks is in the .install file. An example:
    <?php
    /**
    * Implementation of hook_install().
    */
    function text_install() {
     
    content_notify('install', 'text');
    }

    /**
    * Implementation of hook_uninstall().
    */
    function text_uninstall() {
     
    content_notify('uninstall', 'text');
    }

    /**
    * Implementation of hook_enable().
    */
    function text_enable() {
     
    content_notify('enable', 'text');
    }

    /**
    * Implementation of hook_disable().
    */
    function text_disable() {
     
    content_notify('disable', 'text');
    }
    ?>

Fields

  • hook_field_settings($op = 'view') is finally deprecated and is not used.
  • hook_field_settings($op = 'database columns') now uses Schema API syntax.
  • Field db columns are now forced to have 'not null' => FALSE so that empty fields are always NULL. This is needed to provide Views handlers that search for empty/not empty, and to make it possible for the content module to handle empty multiple values in a consistant manner. Fields should be defined that way in hook_field($op = 'columns'), but even if they are not the content module will override their schema to enforce this. When using 'not null' => FALSE, there is no need to provide a default value, so leave it out completely.
  • There is some expanded information provided in hook_field_info(). See the text module for an example. If your module will provide its own Views tables or arguments, change CONTENT_CALLBACK_DEFAULT to CONTENT_CALLBACK_CUSTOM.
    <?php
    /**
    * Implementation of hook_field_info().
    *
    * Here we indicate that the content module will use its default
    * handling for the view of this field.
    *
    * 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 text_field_info() {
      return array(
       
    'text' => array(
         
    'label' => 'Text',
         
    'callbacks' => array(
           
    'tables' => CONTENT_CALLBACK_DEFAULT,
           
    'arguments' => CONTENT_CALLBACK_DEFAULT,
            ),
          ),
        );
    }
    ?>

    Additional information is also provided now in hook_widget_info(). Again, you can use the text module as an example.

Widgets

  • In the previous code, all CCK widgets were added to the form at once using content_widget_invoke(), and there was no easy way to get the form element for an individual field. In D6 there is a function that can be used to retrieve a single field's form element -- content_form_field(&$form, &$form_state, $field) will retrieve an entire form element for the requested field. An optional fourth argument, $get_delta, will retrieve only a specific delta value of that field's form element.
  • Widgets that include a file upload element need to add $form['#attributes']['enctype'] = "multipart/form-data"; to the $form array they receive as a parameter in hook_widget.
  • The content module now handles multiple values and default values for all fields unless they opt out in hook_widget_info() by changing 'multiple values' from CONTENT_HANDLE_CORE to CONTENT_HANDLE_MODULE and 'default value' from CONTENT_CALLBACK_DEFAULT to CONTENT_CALLBACK_CUSTOM or CONTENT_CALLBACK_NONE.
    <?php
    /**
    * 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 text_widget_info() {
      return array(
       
    'text_textfield' => array(
         
    'label' => 'Text Field',
         
    'field types' => array('text'),
         
    'multiple values' => CONTENT_HANDLE_CORE,
         
    'callbacks' => array(
           
    'default value' => CONTENT_CALLBACK_DEFAULT,
            ),
        ),
       
    'text_textarea' => array(
         
    'label' => 'Text Area',
         
    'field types' => array('text'),
         
    'multiple values' => CONTENT_HANDLE_CORE,
         
    'callbacks' => array(
           
    'default value' => CONTENT_CALLBACK_DEFAULT,
            ),
        ),
      );
    }
    ?>

There are significant changes in the way that widgets work in the D6 version. We now expect each widget to handle its own processing using FAPI. In older versions, hook_widget() was called multiple times with different ops to do pre processing, create a form, do post processing, do validation, etc. The widget module was responsible for creating multiple values where needed, and deciding when to keep or throw away empty values. Plus the old version never passed the $form array to the widget, so there was no way for the widget to directly manipulate the $form.

Now hook_widget() has no operations, it is only used to place the element into the form. If the widget is leaving the handling of multiple values up to the content module, it will only return a single delta item each time it is called. The widget is passed a reference to the $form and to $form_state that it can use in any way it wants, and it also receives a $delta value that it can use to tell which delta the content module is processing.

The widget could still build out a complete form element in this stage, but the core elements now only create a placeholder at this point, and are built out more completely using #process and are validated using #element_validate. They also use hook_elements() to make themselves available as FAPI element types.

This construct makes it possible to nest and reuse elements. For instance, the nodereference select field uses the optionwidgets select element and the nodereference autocomplete field uses the text element. There are plans to build on this capability to create 'combo' widgets that allow you to combine various elements together into a single widget. Contributed CCK modules can follow this example if they want their widgets to be reusable.

Look at any of the core field modules for examples of how they use hook_elements(), and note that if you use hook_elements() you must also provide a theme for each element.

<?php
/**
* Implementation of hook_widget().
*
* Attach a single form element to the form. It will be built out and
* validated in the callback(s) listed in hook_elements. We build it
* out in the callbacks rather than here in hook_widget so it can be
* plugged into any module that can provide it with valid
* $field information.
*
* Content module will set the weight, field name and delta values
* for each form element. This is a change from earlier CCK versions
* where the widget managed its own multiple values.
*
* If there are multiple values for this field, 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'] holds the form values.
* @param $field
*   the field array
* @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 number_widget(&$form, &$form_state, $field, $items, $delta = 0) {
 
$element = array(
   
'#type' => $field['widget']['type'],
   
'#default_value' => isset($items[$delta]) ? $items[$delta] : NULL,
  );
  return
$element;
}
?>

One other thing to point out is that we now use #columns to identify the column names, rather than hard-coding them into the elements. This is done to make it easier to share and reuse widgets. In the old code, you would have seen $element['value'] = array(...), now you see $element[$field_key] = array(...), where $field_key is a value taken from #columns. This is what makes it possible for both nodereference, which has a column named 'nid', and text, which uses a column named 'value', to use optionwidgets select and checkboxes.

The content module adds the #columns value to the element, so they are available in all #process and #element_validate functions.

The content module also adds #delta to each element, so you can tell which is which in processes and themes.

The field name is available in $element['#field_name'] and the complete field array is available in $form['#field_info'][$element['#field_name']].

<?php
/**
* Process an individual element.
*
* Build the form element. When creating a form using FAPI #process,
* note that $element['#value'] is already set.
*
*/
function text_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' => t($field['widget']['label']),
   
'#description' => t($field['widget']['description']),
   
'#required' => $element['#required'],
   
'#default_value' => isset($element['#value'][$field_key]) ? $element['#value'][$field_key] : NULL,
   
'#autocomplete_path' => $element['#autocomplete_path'],
  );
  return
$element;
}
?>

Example field code is available in the /examples/ folder of the module. The examples include an example similar to the core text module, that creates a re-usable form element that uses hook_elements. A simpler example is also available. The simple example creates a complete form element in hook_widget(), rather than using #process, similar to the way the older code works. This simple example doesn't use hook_elements(), and thus does not create re-usable widgets as the core field modules do.

API

The beginnings of an API is available in content_crud.inc, along with a hook_content_fiedapi() that modules can use to intervene when fields are created, read, updated, or deleted. The API is evolving and subject to change at any time, but currently:

The api functions include:

- content_field_instance_create()
- content_field_instance_read()
- content_field_instance_update()
- content_field_instance_delete()
- content_module_delete()

The hook_content_fieldapi() ops are:

- create instance
- read instance
- delete instance
- update instance

Formatters

Formatters are now just wrappers around regular Drupal theme functions. They receive an $element array of values and return a formatted display from those values.

The default behavior of formatters is that they will create a theme for a single field value, as has been done in previous versions of CCK. The D6 version now adds the possibility of creating multiple value formatters, as well. Multiple value formatters will receive all the values of a field so you can, for instance, plot all the values on a map or in a graph.

The 'view' operation (handled by the Content module) constructs the $node in a way that you can use drupal_render() to display the formatted output for an individual field.

i.e. print drupal_render($node->field_foo);

The node array will now look like:

  'Single value' formatter :
   $node->content['field_foo'] = array(
     '#type' => 'content_field_view',
     '#title' => 'label'
     '#field_name' => 'field_name',
     '#node' => $node,
     'items' =>
       0 => array(
         '#theme' => $theme,
         '#field_name' => 'field_name',
         '#type_name' => $node->type,
         '#formatter' => $formatter_name,
         '#item' => $items[0],
       ),
       1 => array(
         '#theme' => $theme,
         '#field_name' => 'field_name',
         '#type_name' => $node->type,
         '#formatter' => $formatter_name,
         '#item' => $items[1],
       ),
     ),
   );
  'Multiple value' formatter :
   $node->content['field_foo'] = array(
     '#type' => 'content_field_view',
     '#title' => 'label'
     '#field_name' => 'field_name',
     '#node' => $node,
     'items' => array(
       '#theme' => $theme,
       '#field_name' => 'field_name',
       '#type_name' => $node->type,
       '#formatter' => $formatter_name,
       0 => array(
         '#item' => $items[0],
       ),
       1 => array(
         '#item' => $items[1],
       ),
     ),
   );

 
 

Drupal is a registered trademark of Dries Buytaert.