If I read the current code correctly, the "plugins" tool will unconditionally load all plugins at least once (on initialization of the cache, or when listing the plugins without using the cache). In case my module sports a set of pluggable stuff that is implemented as classes, that would in some situations lead to PHP errors if ctools doesn't consider the correct include order that is required by the class hierarchy, like the thing that triggered the late Views 2 API change.

It would be neat if that use case was possible with ctools too, so that I can make use of that code instead of reimplementing it in my own module. (In case I missed something and it works already, please consider this a support request asking for a hint for implementing something like this with ctools.)

Comments

merlinofchaos’s picture

In order for the plugins to work, the .inc file must be loaded to give information about the dependencies of the plugin. For a class dependency system to work, the classes should thus be kept in a file separate from the .inc -- this is how Views does its class dependencies (the classes are registered via the normal .views.inc)

Note that currently the .inc files are categories completely by directory name, but I have a TODO note in my own list to make it possible to categorize by filename, so that it could be possible to get files named modulename.views.inc or pluginname.views.inc and handle Views-like issues as well. The main .inc file would then need to include a class registry system.

It's entirely possible that this registry system could be a separate tool which can be easily invoked for plugins that want to use it. The Views code is currently a bit specialized toward Views but it might be possible to distill this down. That part makes sense to leave open as a feature request.

merlinofchaos’s picture

Upon reflection, I suppose that two separate plugin types could be combined to do this.

Plugin A is module specific (i.e, modulename.views.inc) and it registers the plugins, and then plugins have a separate .inc file. In that case, the plugin code would need some way to know that plugin .inc files must be registered before they are loaded, and this registration must contain an inheritance tree. This in itself is not too difficult, I think, since there is a hook to give the plugin system this information, but plugins would need to understand dependencies. Also, they wouldn't discover the files used in the normal manner, (i.e, looking for .inc files) but instead all files would be known by the registration system. That makes it a slightly different kind of plugin and much of plugin.inc isn't suitable for that since it is about 80% concerned with auto-discovering plugins.

jpetso’s picture

Version: » 6.x-1.x-dev
Status: Active » Needs review
StatusFileSize
new2.88 KB

I notice that you solved part of this issue by adding a loader for $module.$api.inc files. The attached patch should make it possible to let modules support multiple API versions in different files - I'm not sure if you consider that a worthwhile idea, but it doesn't really come with additional costs so the only issue would probably be best practices in file naming.

merlinofchaos’s picture

Ok, that's interesting! I'm very intrigued. I need to think about this a little bit more but I really like this.

jpetso’s picture

Going back to the original issue topic, this is how the current code of my class-dependency aware extension loader looks like. I thought maybe you're interested in real-world usage outside of your own killer modules :) If some of that code could be replaced by stuff in ctools, that would be neat.

Generally, the code below first retrieves the [module].transformations.inc files, then gathers the plugin files and dependencies by calling a hook (which is supposed to reside in [module].transformation.inc), and then gathers additional information about the separate plugins from each plugin file - that way, plugin info and the corresponding class can live in the same place.

/**
 * Retrieve the list of available extensions and their properties.
 *
 * @param $extensionType
 *   Either 'operations' or 'data_wrappers', depending on which extension type
 *   you want to retrieve info about.
 */
function _transformations_extension_info($extensionType) {
  static $extensions;

  if (isset($extensions)) {
    return $extensions[$extensionType];
  }
  $cache = cache_get('transformations:extensions');

  if (!empty($cache->data)) {
    $extensions = $cache->data;
    return $extensions[$extensionType];
  }
  ctools_include('plugins');

  // Call hook_ctools_plugin_api() to include the various
  // [module].transformations.inc files, taking care of API compatibility.
  $files = ctools_plugin_api_include('transformations', 'transformations',
    TRANSFORMATIONS_MINIMUM_VERSION, TRANSFORMATIONS_VERSION
  );
  $types = array(
    'operations' => 'operation',
    'data_wrappers' => 'data_wrapper',
  );

  // Call hook_transformations_[type]_info() in [module].transformations.inc,
  // in order to retrieve the file and dependency info. The ctools function
  // behaves nearly like module_invoke(), but also adds default values for
  // 'module', 'name' (= class name), 'path' and 'file' if the hook does not
  // provide those.
  foreach ($types as $plural => $singular) {
    $extensions[$plural] = array();
    $apiInfo = array('hook' => 'transformations_' . $singular . '_info');

    foreach ($files as $module => $file) {
      $result = ctools_plugin_process($apiInfo, $module, $module, $file['path']);
      if (is_array($result)) {
        $extensions[$plural] += $result;
      }
    }
    drupal_alter($apiInfo['hook'], $extensions[$plural]);
  }

  // Call [module]_operation_[className]() in the .inc file of the
  // respective plugin, for retrieving detail info without object instantiation.
  foreach (array('operations' => 'operation') as $plural => $singular) {
    foreach ($extensions[$plural] as $className => $info) {
      _transformations_include_extension($className, $extensions[$plural]);

      $function = $info['module'] . '_' . $singular . '_' . $className;
      $details = $function();
      if (is_array($details)) {
        $extensions[$plural][$className] += $details + array(
          'usesTypeOptions' => FALSE,
          'label' => $className,
        );
      }
      else {
        unset($extensions[$plural][$className]);
      }
    }
  }

  cache_set('transformations:extensions', $extensions);
  return $extensions[$extensionType];
}

/**
 * Include the file containing an operation or data wrapper.
 */
function _transformations_include_extension($className, $extensionInfo) {
  static $included = array();

  while (isset($className)) {
    if (empty($extensionInfo[$className])) {
      return;
    }
    $info = $extensionInfo[$className];

    if (!empty($info['file'])) {
      $path = $info['path'] . '/' . $info['file'];
      if (empty($included[$path])) {
        include_once($path);
        $included[$path] = TRUE;
      }
    }
    $className = isset($info['parent']) ? $info['parent'] : NULL;
  }
}
merlinofchaos’s picture

The difficulty with this is that it's largely a module_invoke replacement, which is always a bit difficult if you might need references. Still, there's definitely a few things that we could probably abstract out of that in a satisfactory manner. We could assume, for example, that it's data collection only and that usually doesn't need references, for example. I'll ponder this, though it's not on my priority list just yet.

jpetso’s picture

Ok. The current system works just fine for me, so whatever happens (or not) is ok with me :)

merlinofchaos’s picture

Would having a hook that looks something like this work well for you?

function mymodule_ctools_api_info() {
  return array(
    'api_name' => array(
      'version' => current version #,
      'minimum version' => minimum version #,
      'hooks' => array(
        'name' => array(
          'cache' => true/false, // if true, results of the hook will be cached. Maybe more data is needed. like cache table.
        ),
      ),
   );
}

With that, we might be able to reduce a lot of that code to:

  // This is just a basic module_invoke hook.
  $result = ctools_plugin_api_invoke('module', 'api', 'hook', ...);

And

  // This one is known to be a hook that compiles data, so it will automatically
  // do the kinds of things that ctools_process would do. i.e, add file, path, module
  // data automatically if not already set.
  $result = ctools_plugin_api_invoke_compile('module', 'api', 'hook', ...);
  $result = ctools_plugin_api_alter('module', 'api', 'hook', $alterable_object, ...);

Now, one difference between my general philosophy and what you have is this:

  // Call [module]_operation_[className]() in the .inc file of the
  // respective plugin, for retrieving detail info without object instantiation.

I generally have that info as part of a registration function, so you get all the information via the _compile type hook, above, which would get a full list of all classes in the system. You could then use:

  ctools_plugin_api_include_hierarchy($data, 'object_name');

That would walk up the parent chain and include all the files. You'd still be responsible for invoking your own objects. This all mirrors how Views does things, of course, which is part of why I'm not completely sure if it'll work in your world, but I wanted to run this by you and see.

(Also, if you're interested in expanding this, I'll happily accept contributions. This could be quite useful to people).

merlinofchaos’s picture

Also with these changes it might be necessary to separate the API stuff into its own .inc file; it isn't *really* related to the plugins and doesn't need the code, and additionally might be confusing since we're creating two very similar concepts and separating them might improve DX.

merlinofchaos’s picture

Oh and I'm not sold on _compile as a good name. Maybe _collect.

neclimdul’s picture

I'm looking for exactly this functionality as well. I went over some stuff with merlin today and ended up with something fairly simple that did this using the and a trivial patch to ctools. That clouds the issue a bit but it means that we might look at what can be share between plugins and this api model.

Very interested in where this goes.

merlinofchaos’s picture

And one more comment: I'm still dithering on how separate the API ideal should be from the plugin ideal.

jpetso’s picture

re #8: That should work great for me.

Now, one difference between my general philosophy and what you have is this:

  // Call [module]_operation_[className]() in the .inc file of the
  // respective plugin, for retrieving detail info without object instantiation.

I generally have that info as part of a registration function, so you get all the information via the _compile type hook, above, which would get a full list of all classes in the system.

I understand the intention behind that, but in my case I wanted to move the information as near to the plugin itself as possible. The data that I'm querying from [module]_operation_[className]() is really stuff that would have belonged into a static $myoperation::info() method, but that's only usable from PHP 5.3 upwards. It's just label, description, "is abstract" (can I find this out programmatically) and an operation-specific property called "type options". I want to keep this local to the plugin, in order to ensure it's properly synchronized by the plugin author when the class changes.

The full list of classes is still contained in the [module].transformations.inc file, so the _compile hook actually does compile all the classes. I could add my custom stuff via ctools_plugin_api_alter(), so that should be fine.

neclimdul’s picture

StatusFileSize
new2.89 KB

Caffeine frenzy; feedapi_eparser hacking; and a generous amount of leading from merlinofchaos lead to the following patch. I have only loosely tested it so there may be edge bugs, copy paste errors in docs etc. Serious review welcome!

neclimdul’s picture

StatusFileSize
new2.94 KB

minor fixes from merlin's review on irc.

neclimdul’s picture

StatusFileSize
new3.06 KB

more awesome feed back. now with fewer notices and better docs!

merlinofchaos’s picture

Status: Needs review » Fixed

Nice to have this one in! Thanks!

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.