Drupal

Drupal 7 Tutorial: Porting CCK Modules from Drupal 6

One of the biggest changes when it comes to module development from Drupal 6 to Drupal 7, is the functionality of CCK module in 6 is now in core.


One of the biggest changes when it comes to module development from Drupal 6 to Drupal 7, is the functionality of CCK module in 6 is now in core. For most this a welcome improvement to Drupal core, but due to the number of API changes, the process of porting your old CCK modules might turn out to be more of an undertaking then you originally expected. That is what I found out while working on a Drupal 7 port of the SWFUpload module. This post will walk through some of the key changes and challenges you might face, based on my experience with the SWFUpload module.

Resources

First and foremost, this is too big a topic to fit into one blog post, so I have linked extensively to external resources that I found helpful. Also, this only covers my experience on porting one module, so I undoubtedly missed a few things. Here are a couple resources you will want to have handy: Updating CCK Field Modules from D6 to D7 gives an overview of some of the changes, which I may not cover here. Also the Coder module does a great job with core api changes, but as of this writing, doesn’t cover changes from the CCK API. In most cases, I have included a diff instead of the resulting code to make it easier to see what changed.

References to the imagefield and filefield modules

References to the "imagefield" module need to be changed to "image", and "filefield" need to be changed to "file". Pay particular attention to module_load_include() and module_invoke() calls.

Examples:

module_load_include(‘inc’, ‘imagefield’, ‘imagefield_widget’)

becomes:

module_load_include(‘inc’, ‘image’, ‘image.field’)

and,

module_invoke(‘filefield’, ‘widget_settings’, ‘form’, $widget)

becomes:

module_invoke(‘file’, ‘field_widget_settings_form’, $field, $instance)

Also make sure to update your .info file.

--- a/swfupload.info
+++ b/swfupload.info
@@ -1,9 +1,7 @@
 name = SWFupload Widget
-description = A widget for CCK's Filefield which enables multiple file uploads using the SWFUpload library.
+description = A widget for File fields which enables multiple file uploads using the SWFUpload library.
 package = CCK
 version = VERSION
-dependencies[] = filefield
-dependencies[] = jqp
-core = 6.x
+dependencies[] = file
+dependencies[] = libraries
+core = 7.x

hook_widget is now hook_field_widget_form

Aside from more parameters, you may notice errors about undefined indexes because one of the fields is the element, instead of declaring a new array and returning that, "Field widget form elements should be based on the passed-in $element, which contains the base form element properties derived from the field configuration." In the case of SWFUpload, I used the field types functions to handle the bulk of the work, but then override the attributes that I care about.

 /**
- * Implementation of hook_widget().
+ * Implements hook_field_widget_form().
  */
-function swfupload_widget(&$form, &$form_state, $field, $items, $delta = 0) {
-  $element = array(
-    '#type' => 'swfupload_widget',
-    '#default_value' => $items,
-  );
+function swfupload_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
+  if (module_exists('image') && $field['type'] == 'image') {
+    $element += image_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
+  } else {
+    $element += file_field_widget_form($form, $form_state, $field, $instance, $langcode, $items, $delta, $element);
+  }
+
+  if (!empty($items)) {
+    $default_values = array();
+    foreach ($items as $key => $file) {
+      $default_values[$file['fid']] = $file + array(
+        'thumb' => '/'. swfupload_thumb_path($file),
+      );
+      unset($element[$key]);
+    }
+
+    $element['#value'] = $default_values;
+    unset($element[$key + 1]);
+  } else {
+    // Remove child elements because we don't want their callbacks to fire
+    unset($element[0]);
+  }
+
+  $element['#theme'] = 'swfupload_widget';
+  $element['#after_build'] = array('swfupload_add_js');
+  $element['#element_validate'] = array('swfupload_widget_validate');
+  //$element['#process'] = array('swfupload_widget_process');
+  //$element['#value_callback'] = 'swfupload_widget_value';
+
   return $element;
 }

If your #value_callback is not firing or getting unexpected input

One of the key troubles I ran into was with the fact that SWFUpload allows you to upload multiple files at once, and it stores the resulting file information in a single hidden field. In Drupal 6 it was easy for the module to implement a #value_callback which would turn that string into a proper array. In Drupal 7, the $form_state['values'] and even $form_state['input'] were being overwritten and the value callback was not being fired. This is because in Drupal 7, the form builder calls an internal function _form_builder_handle_input_element which uses the drupal_array_get_nested_value() to check the structure of the input, to make sure all the elements parents exist. If not, it uses drupal_array_set_nested_value() to populate an empty default value. Only after that does it call the #value_callback. A way around this was to use an #element_validate calback instead, which will need to create the array as expected by form builder.

/**
 * An #element_validate callback for the file_field_widget field.
 */
function swfupload_widget_validate(&$element, &$form_state) {
  $raw_value = $form_state['input'][$element['#field_name']][$element['#language']][0]['raw_value'];
  $files = json_decode($raw_value);
  $values = array();
  foreach ($files as $file) {
    $values[] = array(
      'fid' => $file->fid,
      'filename' => $file->filename,
      'display' => (empty($file->display)) ? 0 : 1,
      'description' => '',
    );
  }
 
  $form_state['input'][$element['#field_name']][$element['#language']] = $values;
  $form_state['values'][$element['#field_name']][$element['#language']] = $values;
}

content_fields() is now field_info_fields()

If you are passing parameters to content_fields then you probably want field_info_field() instead.

hook_widget_info() has changed to hook_field_widget_info()

The Updating CCK Field Modules from D6 to D7 article does a good job of showing the structure change, but doesn’t map the changes between the constants. See the CCK constants section towards the bottom of the page, compared to the FIELD_BEHAVIOR constants in Drupal 7.

 /**
- * Implementation of hook_widget_info().
+ * Implements hook_field_widget_info().
  */
-function swfupload_widget_info() {
+function swfupload_field_widget_info() {
   return array(
     'swfupload_widget' => array(
       'label' => t('SWFUpload'),
-      'field types' => array('filefield'),
-      'multiple values' => CONTENT_HANDLE_MODULE,
-      'callbacks' => array(
-        'default value' => CONTENT_CALLBACK_CUSTOM,
+      'field types' => array('file', 'image'),
+      'settings' => array(
+        'progress_indicator' => 'throbber',
+        'preview_image_style' => 'thumbnail',
+      ),
+      'behaviors' => array(
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
+        'default value' => FIELD_BEHAVIOR_NONE,
       ),
     ),
   );
 }

hook_widget_settings() is now hook_field_widget_settings_form()

And the $op parameter no longer exists, so instead of implementing a switch statement for $op = ‘form’, $op = ‘validate’ and $op = ‘save’, use your $op = 'form' and use #validate, #element_validate and #submit to add any custom validation. Also instead of being passed the widget, you are passed two parameters, the $field structure and the $instance structure.

 /**
- * Implementation of CCK's hook_widget_settings($op = 'form').
+ * Implements hook_field_widget_settings_form().
  */
-function swfupload_widget_settings_form($widget) {
-  if (module_exists('imagefield')) {
-    module_load_include('inc', 'imagefield', 'imagefield_widget');
-    $form = imagefield_widget_settings_form($widget);
+function swfupload_field_widget_settings_form($field, $instance) {
+  if (module_exists('image') && $field['type'] == 'image') {
+    module_load_include('inc', 'image', 'image.field');
+    $form = image_field_widget_settings_form($field, $instance);
   }
   else {
-    $form = module_invoke('filefield', 'widget_settings', 'form', $widget);
+    $form = module_invoke('file', 'field_widget_settings_form', $field, $instance);
   }
-  return $form;
-}
 
-/**
- * Implementation of CCK's hook_widget_settings($op = 'validate').
- */
-function swfupload_widget_settings_validate($widget) {
-  // Check that set resolutions are valid.
-  foreach (array('min_resolution', 'max_resolution') as $resolution) {
-    if (!empty($widget[$resolution]) && !preg_match('/^[0-9]+x[0-9]+$/', $widget[$resolution])) {
-      form_set_error($resolution, t('Please specify a resolution in the format WIDTHxHEIGHT (e.g. 640x480).'));
-    }
-  }
-}
+  $form['#submit'][] = 'swfupload_field_widget_settings_submit';
 
-/**
- * Implementation of CCK's hook_widget_settings($op = 'save').
- */
-function swfupload_widget_settings_save($widget) {
-  $filefield_settings = module_invoke('filefield', 'widget_settings', 'save', $widget);
-  return array_merge($filefield_settings, array('max_resolution', 'min_resolution', 'alt',  'custom_alt', 'title', 'custom_title', 'title_type', 'default_image', 'use_default_image'));
+  return $form;
 }

Handling file uploads with file_save_upload()

You will need to make sure the destination is a public:// or private:// path. In Drupal 6 file_save_upload() used to take a full file path. So instead of calling file_directory_path(), file fields now have a uri_scheme setting: $field['settings']['uri_scheme'] and paths are relative to the configured public and private directory. Also, if you were using the filepath property of the return object, you’ll want to replace that with destination as filepath has been removed from the return object.

Summary

I hope that even though I glossed over a lot of the details, the real code examples along with the external resources help make porting your module a little easier. Have you had issues with other API changes? I'd love to hear your experiences.

Similar posts

Get notified on new marketing insights

Be the first to know about new B2B SaaS Marketing insights to build or refine your marketing function with the tools and knowledge of today’s industry.