From 8d71033797c85b6837c164a01732ff3df104db40 Mon Sep 17 00:00:00 2001
From: Earl Miles <merlin@logrus.com>
Date: Thu, 23 Apr 2009 23:58:55 +0000
Subject: [PATCH] Add the ability to automatically do .inc files for default
 objects like Views does as well as more powerful version controls.

---
 delegator/delegator.install      |  12 +++
 delegator/plugins/tasks/page.inc |   1 +
 includes/export.inc              | 132 ++++++++++++++++++++-----------
 includes/plugins.inc             |  74 ++++++++++++++---
 4 files changed, 159 insertions(+), 60 deletions(-)

diff --git a/delegator/delegator.install b/delegator/delegator.install
index e2619acb..d650bfca 100644
--- a/delegator/delegator.install
+++ b/delegator/delegator.install
@@ -24,6 +24,12 @@ function delegator_schema_1() {
   $schema['delegator_handlers'] = array(
     'export' => array(
       'identifier' => 'handler',
+      'api' => array(
+        'owner' => 'delegator',
+        'api' => 'delegator_default',
+        'minimum_version' => 1,
+        'current_version' => 1,
+      ),
     ),
     'fields' => array(
       'did' => array(
@@ -97,6 +103,12 @@ function delegator_schema_1() {
     'description' => 'Contains page subtasks for implementing pages with arbitrary tasks.',
     'export' => array(
       'identifier' => 'page',
+      'api' => array(
+        'owner' => 'delegator',
+        'api' => 'delegator_default',
+        'minimum_version' => 1,
+        'current_version' => 1,
+      ),
     ),
     'fields' => array(
       'pid' => array(
diff --git a/delegator/plugins/tasks/page.inc b/delegator/plugins/tasks/page.inc
index e89e9421..158d469e 100644
--- a/delegator/plugins/tasks/page.inc
+++ b/delegator/plugins/tasks/page.inc
@@ -377,6 +377,7 @@ function delegator_page_load($name) {
  */
 function delegator_page_load_all($task = NULL) {
   ctools_include('export');
+
   if (empty($task)) {
     return ctools_export_load_object('delegator_pages');
   }
diff --git a/includes/export.inc b/includes/export.inc
index 01c6c0c9..355825f2 100644
--- a/includes/export.inc
+++ b/includes/export.inc
@@ -47,8 +47,7 @@ define('EXPORT_IN_CODE', 0x02);
  */
 function ctools_export_load_object($table, $type = 'all', $args = array()) {
   static $cache = array();
-  static $cached_defaults = FALSE;
-  static $cached_database = FALSE;
+  static $cached_database = array();
 
   $schema = ctools_export_get_schema($table);
   $export = $schema['export'];
@@ -57,6 +56,11 @@ function ctools_export_load_object($table, $type = 'all', $args = array()) {
     $cache[$table] = array();
   }
 
+  // If fetching all and cached all, we've done so and we are finished.
+  if ($type == 'all' && !empty($cached_database[$table])) {
+    return $cache[$table];
+  }
+
   $return = array();
 
   // Don't load anything we've already cached.
@@ -79,50 +83,42 @@ function ctools_export_load_object($table, $type = 'all', $args = array()) {
   $conditions = array();
   $query_args = array();
 
-  // We do not have to load anything if we have already cached everything.
-  if ($type != 'all' || !$cached_database) {
-    // If they passed in names, add them to the query.
-    if ($type == 'names') {
-      $conditions[] = "$export[key] IN (" . db_placeholders($args, $schema['fields'][$export['key']]['type']) . ")";
-      $query_args = $args;
-    }
-    else if ($type == 'conditions') {
-      foreach ($args as $key => $value) {
-        if (isset($schema['fields'][$key])) {
-          $conditions[] = "$key = " . db_type_placeholder($schema['fields'][$key]['type']);
-          $query_args[] = $value;
-        }
+  // If they passed in names, add them to the query.
+  if ($type == 'names') {
+    $conditions[] = "$export[key] IN (" . db_placeholders($args, $schema['fields'][$export['key']]['type']) . ")";
+    $query_args = $args;
+  }
+  else if ($type == 'conditions') {
+    foreach ($args as $key => $value) {
+      if (isset($schema['fields'][$key])) {
+        $conditions[] = "$key = " . db_type_placeholder($schema['fields'][$key]['type']);
+        $query_args[] = $value;
       }
     }
+  }
 
-    // Make a string out of the conditions.
-    if ($conditions) {
-      $query .= " WHERE " . implode(' AND ', $conditions);
-    }
+  // Make a string out of the conditions.
+  if ($conditions) {
+    $query .= " WHERE " . implode(' AND ', $conditions);
+  }
 
-    $result = db_query($query, $query_args);
+  $result = db_query($query, $query_args);
 
-    // Unpack the results of the query onto objects and cache them.
-    while ($data = db_fetch_object($result)) {
-      $object = _ctools_export_unpack_object($schema, $data, $export['object']);
-      $object->type = t('Normal');
-      $object->export_type = EXPORT_IN_DATABASE;
+  // Unpack the results of the query onto objects and cache them.
+  while ($data = db_fetch_object($result)) {
+    $object = _ctools_export_unpack_object($schema, $data, $export['object']);
+    $object->type = t('Normal');
+    $object->export_type = EXPORT_IN_DATABASE;
 
-      $cache[$table][$object->{$export['key']}] = $object;
-      if ($type == 'conditions') {
-        $return[$object->{$export['key']}] = $object;
-      }
+    $cache[$table][$object->{$export['key']}] = $object;
+    if ($type == 'conditions') {
+      $return[$object->{$export['key']}] = $object;
     }
   }
 
-  if ($type == 'all') {
-    $cached_database = TRUE;
-  }
   // @todo Load subrecords.
 
-  if ($export['default hook'] && !$cached_defaults) {
-    // @todo add a method to load .inc files for this.
-    $defaults = module_invoke_all($export['default hook']);
+  if ($defaults = _ctools_export_get_defaults($table, $export)) {
     $status = variable_get($export['status'], array());
 
     foreach ($defaults as $object) {
@@ -162,22 +158,12 @@ function ctools_export_load_object($table, $type = 'all', $args = array()) {
           $return[$object->name] = $object;
         }
       }
-
-    }
-
-    // We only actually force this when retrieving all, because we may not
-    // have retrieved an object from the database and could thus incorrectly
-    // identify one as being a 'default' object when it is actually
-    // overridden. So we settle for a potential minor performance decrease
-    // in order to get this correct.
-    if ($type == 'all') {
-      // Make sure we don't run that again later on.
-      $cached_defaults = TRUE;
     }
   }
 
   // If fetching all, we've done so and we are finished.
   if ($type == 'all') {
+    $cached_database[$table] = TRUE;
     return $cache[$table];
   }
 
@@ -211,7 +197,7 @@ function ctools_get_default_object($table, $name) {
   }
 
   // @todo add a method to load .inc files for this.
-  $defaults = module_invoke_all($export['default hook']);
+  $defaults = _ctools_export_get_defaults($table, $export);
   $status = variable_get($export['status'], array());
 
   if (!isset($defaults[$name])) {
@@ -232,6 +218,53 @@ function ctools_get_default_object($table, $name) {
   return $object;
 }
 
+/**
+ * Call the hook to get all default objects of the given type from the
+ * export. If configured properly, this could include loading up an API
+ * to get default objects.
+ */
+function _ctools_export_get_defaults($table, $export) {
+  static $cache = array();
+
+  if (!isset($cache[$table])) {
+    $cache[$table] = array();
+
+    if ($export['default hook']) {
+      if (!empty($export['api'])) {
+        $info = ctools_plugin_api_include($export['api']['owner'], $export['api']['api'],
+          $export['api']['minimum_version'], $export['api']['current_version']);
+        $modules = array_keys($info);
+      }
+      else {
+        $modules = module_implements($export['default hook']);
+      }
+
+      foreach ($modules as $module) {
+        $function = $module . '_' . $export['default hook'];
+        if (function_exists($function)) {
+          if (empty($export['api'])) {
+            $cache[$table] += (array) $function($export);
+          }
+          else {
+            foreach ((array) $function($export) as $name => $object) {
+              // If version checking is enabled, ensure that the object can be used.
+              if (isset($object->api_version) &&
+                $object->api_version >= $export['api']['minimum_version'] &&
+                $object->api_version <= $export['api']['current_version']) {
+                $cache[$table][$name] = $object;
+              }
+            }
+          }
+        }
+      }
+
+      drupal_alter($export['default hook'], $cache[$table]);
+    }
+  }
+
+  return $cache[$table];
+}
+
 /**
  * Unpack data loaded from the database onto an object.
  *
@@ -321,7 +354,10 @@ function ctools_export_object($table, $object, $indent = '', $identifier = NULL,
   $output = $indent . '$' . $identifier . ' = new ' . get_class($object) . ";\n";
 
   if ($schema['export']['can disable']) {
-    $output .= $indent . '$' . $identifier . '->disabled' . ' = FALSE; /* Edit this to true to make a default ' . $identifier . ' disabled initially */' . "\n";
+    $output .= $indent . '$' . $identifier . '->disabled = FALSE; /* Edit this to true to make a default ' . $identifier . ' disabled initially */' . "\n";
+  }
+  if (!empty($schema['export']['api']['current_version'])) {
+    $output .= $indent . '$' . $identifier . '->api_version = ' . $schema['export']['api']['current_version'] . ";\n";
   }
 
   // Put top additions here:
diff --git a/includes/plugins.inc b/includes/plugins.inc
index 18a3d277..feb25743 100644
--- a/includes/plugins.inc
+++ b/includes/plugins.inc
@@ -11,16 +11,23 @@
  */
 
 /**
- * Load a group of API files.
+ * Get an array of information about modules that support an API.
  *
  * This will ask each module if they support the given API, and if they do
- * it will load the specified file name. The API and the file name
- * coincide by design.
+ * it will return an array of information about the modules that do.
  *
  * This function invokes hook_ctools_api. This invokation is statically
  * cached, so feel free to call it as often per page run as you like, it
  * will cost very little.
  *
+ * This function can be used as an alternative to module_implements and can
+ * thus be used to find a precise list of modules that not only support
+ * a given hook (aka 'api') but also restrict to only modules that use
+ * the given version. This will allow multiple modules moving at different
+ * paces to still be able to work together and, in the event of a mismatch,
+ * either fall back to older behaviors or simply cease loading, which is
+ * still better than a crash.
+ *
  * @param $owner
  *   The name of the module that controls the API.
  * @param $api
@@ -36,9 +43,19 @@
  *   during operation.
  *
  * @return
- *   The API information, in case you need it.
+ *   An array of API information, keyed by module. Each module's information will
+ *   contain:
+ *   - 'version': The version of the API required by the module. The module
+ *     should use the lowest number it can support so that the widest range
+ *     of supported versions can be used.
+ *   - 'path': If not provided, this will be the module's path. This is
+ *     where the module will store any subsidiary files. This differs from
+ *     plugin paths which are figured separately.
+ *
+ *   APIs can request any other information to be placed here that they might
+ *   need. This should be in the documentation for that particular API.
  */
-function ctools_plugin_api_include($owner, $api, $minimum_version, $current_version) {
+function ctools_plugin_api_info($owner, $api, $minimum_version, $current_version) {
   static $cache = array();
   if (!isset($cache[$owner][$api])) {
     $cache[$owner][$api] = array();
@@ -54,23 +71,56 @@ function ctools_plugin_api_include($owner, $api, $minimum_version, $current_vers
         if (!isset($info['path'])) {
           $info['path'] = drupal_get_path('module', $module);
         }
-        if (!isset($info['file'])) {
-          $info['file'] = "$module.$api.inc";
-        }
         $cache[$owner][$api][$module] = $info;
       }
     }
+  }
+
+  return $cache[$owner][$api];
+}
+
+/**
+ * Load a group of API files.
+ *
+ * This will ask each module if they support the given API, and if they do
+ * it will load the specified file name. The API and the file name
+ * coincide by design.
+ *
+ * @param $owner
+ *   The name of the module that controls the API.
+ * @param $api
+ *   The name of the api. The api name forms the file name:
+ *   $module.$api.inc, though this can be overridden by the module's response.
+ * @param $minimum_version
+ *   The lowest version API that is compatible with this one. If a module
+ *   reports its API as older than this, its files will not be loaded. This
+ *   should never change during operation.
+ * @param $current_version
+ *   The current version of the api. If a module reports its minimum API as
+ *   higher than this, its files will not be loaded. This should never change
+ *   during operation.
+ *
+ * @return
+ *   The API information, in case you need it.
+ */
+function ctools_plugin_api_include($owner, $api, $minimum_version, $current_version) {
+  static $already_done = array();
 
-    // Now that we have a list, do our includes.
-    foreach ($cache[$owner][$api] as $module => $info) {
+  $info = ctools_plugin_api_info($owner, $api, $minimum_version, $current_version);
+  if (!isset($already_done[$owner][$api])) {
+    foreach ($info as $module => $info) {
+      if (!isset($info['file'])) {
+        $info['file'] = "$module.$api.inc";
+      }
       if (file_exists("./$info[path]/$info[file]")) {
-        $cache[$owner][$api][$module]['included'] = TRUE;
+        $info[$module]['included'] = TRUE;
         require_once "./$info[path]/$info[file]";
       }
     }
+    $already_done[$owner][$api] = TRUE;
   }
 
-  return $cache[$owner][$api];
+  return $info;
 }
 
 /**
-- 
GitLab