Index: ctools.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ctools/ctools.module,v
retrieving revision 1.27.2.50
diff -u -p -r1.27.2.50 ctools.module
--- ctools.module	15 Oct 2010 22:11:09 -0000	1.27.2.50
+++ ctools.module	23 Oct 2010 17:08:51 -0000
@@ -335,6 +335,38 @@ function ctools_set_callback_token($toke
   return $string;
 }
 
+/**
+ * Merges any number of arrays / parameters recursively, replacing entries with
+ * string keys with values from latter arrays. If the entry or the next value to
+ * be assigned is an array, then it automagically treats both arguments as an
+ * array. Numeric entries are appended, not replaced, but only if they are
+ * unique.
+ */
+function ctools_array_merge_recursive_distinct() {
+  $arrays = func_get_args();
+  $base = array_shift($arrays);
+  foreach($arrays as $append) {
+    foreach ($append as $key => $value) {
+      if (!array_key_exists($key, $base) && !is_numeric($key)) {
+        $base[$key] = $append[$key];
+        continue;
+      }
+      if (is_array($value) || is_array($base[$key])) {
+        $base[$key] = ctools_array_merge_recursive_distinct($base[$key], $append[$key]);
+      }
+      elseif (is_numeric($key)) {
+        if (!in_array($value, $base)) {
+          $base[] = $value;
+        }
+      }
+      else {
+        $base[$key] = $value;
+      }
+    }
+  }
+  return $base;
+}
+
 // -----------------------------------------------------------------------
 // Drupal core hooks

Index: help/export.html
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ctools/help/export.html,v
retrieving revision 1.1.2.7
diff -u -p -r1.1.2.7 export.html
--- help/export.html	22 Jul 2010 22:41:44 -0000	1.1.2.7
+++ help/export.html	23 Oct 2010 17:02:18 -0000
@@ -57,6 +57,10 @@ function mymodule_schema() {
         'table' => 'my_join_table',
         'left_key' => 'format',
         'right_key' => 'id',
+        // One-to-many relationships must define 'array_key'. Can be either
+        // EXPORT_NUMERIC_ARRAY or a field name from the joined table.
+        'array_key' => EXPORT_NUMERIC_ARRAY,
+
         // Optionally you can define extra clauses to add to the INNER JOIN
         'extras' => "AND extra_clauses",
 
Index: includes/export.inc
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/ctools/includes/export.inc,v
retrieving revision 1.19.2.23
diff -u -p -r1.19.2.23 export.inc
--- includes/export.inc	12 Aug 2010 20:48:31 -0000	1.19.2.23
+++ includes/export.inc	23 Oct 2010 17:02:19 -0000
@@ -19,6 +19,12 @@ define('EXPORT_IN_DATABASE', 0x01);
 define('EXPORT_IN_CODE', 0x02);
 
 /**
+ * A bit flag used to tell that exported child objects shall be structures as
+ * an numeric array.
+ */
+define('EXPORT_NUMERIC_ARRAY', 0x01);
+
+/**
  * @defgroup export_crud CRUD functions for export.
  * @{
  * export.inc supports a small number of CRUD functions that should always
@@ -380,6 +386,8 @@ function ctools_export_load_object($tabl
 
   $status = variable_get($export['status'], array());
   // Unpack the results of the query onto objects and cache them.
+  // Since objects can live across multiple tables with one-to-many
+  // relationships we have to merge each row object.
   while ($data = db_fetch_object($result)) {
     $object = _ctools_export_unpack_object($schema, $data, $export['object']);
     $object->table = $table;
@@ -390,9 +398,9 @@ function ctools_export_load_object($tabl
       $object->disabled = $status[$object->{$export['key']}];
     }
 
-    $cache[$table][$object->{$export['key']}] = $object;
+    $cache[$table][$object->{$export['key']}] = (object) ctools_array_merge_recursive_distinct((array) $cache[$table][$object->{$export['key']}], (array) $object);
     if ($type == 'conditions') {
-      $return[$object->{$export['key']}] = $object;
+      $return[$object->{$export['key']}] = (object) ctools_array_merge_recursive_distinct((array) $return[$object->{$export['key']}], (array) $object);
     }
   }
 
@@ -602,14 +610,28 @@ function _ctools_export_unpack_object($s
   }
 
   if (isset($schema['join'])) {
+    $i = 0;
     foreach ($schema['join'] as $join_key => $join) {
       $join_schema = ctools_export_get_schema($join['table']);
+      $array_key = isset($join['array_key']) ? $join['array_key'] : FALSE;
       if (!empty($join['load'])) {
         foreach ($join['load'] as $field) {
           $info = $join_schema['fields'][$field];
-          $object->$field = empty($info['serialize']) ? $data->$field : unserialize(db_decode_blob($data->$field));
+          // Fetch the field value from the $data object.
+          $field_value = empty($info['serialize']) ? $data->$field : unserialize(db_decode_blob($data->$field));
+          // Find out how we want to structure this joined field.
+          if ($array_key == EXPORT_NUMERIC_ARRAY) {
+            $object->{$join_key}[$i]->$field = $field_value;
+          }
+          elseif (is_string($array_key)) {
+            $object->{$join_key}[$data->$array_key]->$field = $field_value;
+          }
+          else {
+            $object->$field = $field_value;
+          }
         }
       }
+      $i++;
     }
   }
 
@@ -692,41 +714,29 @@ function ctools_export_object($table, $o
 
   $fields = $schema['fields'];
   if (!empty($schema['join'])) {
-    foreach ($schema['join'] as $join) {
+    foreach ($schema['join'] as $join_key => $join) {
+      $join_schema = drupal_get_schema($join['table']);
       if (!empty($join['load'])) {
         foreach ($join['load'] as $join_field) {
-          $fields[$join_field] = $join['fields'][$join_field];
+          $fields[$join_field] = $join_schema['fields'][$join_field];
+          // Add the join table's join key to each field, so we know how to
+          // export this field later on.
+          if (!empty($join['array_key'])) {
+            $fields[$join_field]['join_key'] = $join_key;
+          }
         }
       }
     }
   }
 
   // Go through our schema and joined tables and build correlations.
-  foreach ($fields as $field => $info) {
-    if (!empty($info['no export'])) {
+  foreach ($fields as $field => &$info) {
+    // Check that the field shall be exported and if it recursively already
+    // have been processed.
+    if (isset($info['no export']) || isset($info['_processed'])) {
       continue;
     }
-    if (!isset($object->$field)) {
-      if (isset($info['default'])) {
-        $object->$field = $info['default'];
-      }
-      else {
-        $object->$field = '';
-      }
-    }
-
-    // Note: This is the *field* export callback, not the table one!
-    if (!empty($info['export callback']) && function_exists($info['export callback'])) {
-      $output .= $indent . '$' . $identifier . '->' . $field . ' = ' . $info['export callback']($object, $field, $value, $indent) . ";\n";
-    }
-    else {
-      $value = $object->$field;
-      if ($info['type'] == 'int') {
-        $value = (isset($info['size']) && $info['size'] == 'tiny') ? (bool) $value : (int) $value;
-      }
-
-      $output .= $indent . '$' . $identifier . '->' . $field . ' = ' . ctools_var_export($value, $indent) . ";\n";
-    }
+    $output .= _ctools_export_field($object, $identifier, $field, $fields, $indent);
   }
 
   // And bottom additions here
@@ -738,6 +748,47 @@ function ctools_export_object($table, $o
 }
 
 /**
+ * Helper function that exports a field on an object into code.
+ */
+function _ctools_export_field($object, $identifier, $field, &$fields, $indent = '') {
+  $info = $fields[$field];
+  // Looks like the field we try to export exists in a child object.
+  if (!isset($object->$field) && isset($info['join_key'])) {
+    $output = '';
+    foreach ($object->{$info['join_key']} as $key => $child_object) {
+      $output .= '  ' . $indent . '$' . $info['join_key'] . ' = new ' . get_class($child_object) . ";\n";
+      // Currently the export functionality doesn't support chained joins.
+      // So we can expect the child object to only contain fields.
+      foreach ($child_object as $child_field => $child_field_value) {
+        $output .= _ctools_export_field($child_object, $info['join_key'], $child_field, $fields, $indent . '  ');
+      }
+      $array_key = is_numeric($key) ? '' : "'$key'";
+      $output .= $indent . '$' . $identifier . '->' . $info['join_key'] . '[' . $array_key . '] = $' . $info['join_key'] . ";\n";
+    }
+    return $output;
+  }
+  elseif (!isset($object->$field)) {
+    $value = isset($info['default']) ? $info['default'] : '';
+  }
+  else {
+    $value = $object->$field;
+  }
+
+  // Note: This is the *field* export callback, not the table one!
+  if (isset($info['export callback']) && function_exists($info['export callback'])) {
+    $output = $indent . '$' . $identifier . '->' . $field . ' = ' . $info['export callback']($object, $field, $value, $indent) . ";\n";
+  }
+  else {
+    if ($info['type'] == 'int') {
+      $value = (isset($info['size']) && $info['size'] == 'tiny') ? (bool) $value : (int) $value;
+    }
+    $output = $indent . '$' . $identifier . '->' . $field . ' = ' . ctools_var_export($value, $indent) . ";\n";
+  }
+  $fields[$field]['_processed'] = TRUE;
+  return $output;
+}
+
+/**
  * Get the schema for a given table.
  *
  * This looks for data the export subsystem needs and applies defaults so
