Creating Google Gadgets within Drupal

Submitted by Frederic Marand on Thu, 2007-07-19 15:43

2009-05-24: UPDATE: now ported to D6

screendump showing the Drupal API google gadget Having recently discovered the Google Gadgets API, and being on vacation this week proved too much of a temptation: I had to build one using Drupal. Here is a way to do it, as a module creating a Google Gadget to directly search http://api.drupal.com/ from the iGoogle home page. User preferences allow searching the various versions of the API and default to Drupal 5.

What's in a gadget module, anyway ?

Google gadgets are (typically) small XML documents, which can be either envelopes (metadata) pointing to externally defined "modules" (not to be confused with drupal modules), or fat objects containing the "module" as a CDATA section within the envelope itself.

For this smallish example, I chose the second option, which tends to make things more compact. For a more general gadget creation API within drupal and a set of gadgets such as a popular site might wish to create, it would probably be better to serve envelopes and data separately, using the <content type="url"></content> mechanism in Google Gadgets.

Design choices: PHP5 + DOM

With Drupal plowing full speed ahead to PHP5.2 only, it obviously only made sense to use PHP5 features, but the steps to take to downgrade to PHP4 are still fairly obvious.

In this case this means that we'll be defining a GoogleGadget class to hold our actual working methods, and only use PHP4 traditional functions to hook into Drupal.

Code, code, code

I don't think this needs commenting, but it had to be there.

Our class will be holding the data in Drupal-traditional fashion: arrays within arrays, and '#' prefixed for child elements. So what does such a class need to hold ?

  • a content type definition: gadgets are in HTMl by default, but can contain other content types, so this has to be declared
  • content: what good would it be without it ?
  • module-level preferences: for global preferences which gadget users won't be allowed to change
  • user-level preferences: for those which the iGoogle UI will allow users to change

contentType * @var string */ public $content = ''; /** * Hold elements (#-prefixed) and attributes below ModulePrefs * * @var array */ public $modulePrefs; /** * Hold attributes for UserPref elements * * @var array */ public $userPrefs; ?>

In the class constructor, we just set up the defaults and initialize arrays. This avoids warnings under E_STRICT | E_ALL when adding elements to them later on.

contentType = $type; $this->content = $body; $this->modulePrefs = array(); $this->userPrefs = array(); } ?>

Then we define our FAPI-like array for module preferences.

modulePrefs["#$section"][$name] = $value; } ?>

And the vector of user preferences.

userPrefs[$name] = $attributes; $this->userPrefs[$name]['name'] = $name; $this->userPrefs[$name]['display_name'] = $displayName; } ?>

<Rant> why did they use two different structures ? module preferences are stored as elements under child elements of Moduleprefs while user preferences are stored as such child elements themselves.</Rant>

Actual rendering

Since we're using a module and not a theme, we must return a full page, which means echoing to output and using die(), instead of returning the result of the module rendering.

This function is the main point of change if one wants to use separate envelopes and content: as such, it outputs both in one pass.

createElement('Module'); $module = $doc->appendChild($module); ?>

Yay, XML DOM. Much cleaner and actually simpler than hand-generating the XML by appending/prepending strings here and there. So here we create the module preferences part:

modulePrefs) > 0) { $prefsNode = $doc->createElement('ModulePrefs'); $prefsNode = $module->appendChild($prefsNode); $sections = array(); foreach ($this->modulePrefs as $prefName => $prefValue) { if ($prefName[0] == '#') { $prefName = substr($prefName, 1); if (!in_array($prefName, $sections)) // must create it { ${$prefName} = $doc->createElement($prefName); ${$prefName} = $prefsNode->appendChild(${$prefName}); $sections[] = $prefName; } foreach ($prefValue as $attrName => $attrValue) { ${$prefName}->setAttribute($attrName, $attrValue); } } else { $prefsNode->setAttribute($prefName, $prefValue); } } } // module preferences ?>

Then the user preferences part:

userPrefs > 0)) { foreach ($this->userPrefs as $prefArray) { $prefNode = $doc->createElement('UserPref'); $prefNode = $module->appendChild($prefNode); foreach ($prefArray as $prefName => $prefValue) { $prefNode->setAttribute($prefName, $prefValue); } } } ?>

At this point, the envelope ends, and we add the content, since we are doing the fat format with content within the envelope.

createElement('Content'); $content = $module->appendChild($content); $content->setAttribute('type', $this->contentType); $data = $doc->createCDATASection($this->content); $data = $content->appendChild($data); ?>

Noticed the CDATA section ? Ain't it nicer than hand-inputting the actual CDATA syntax ?

And now, of course, we output our DOM as an XML string.

saveXML(); die(); } } // end of class ?>

Hooking with drupal

Not much to see here: just a minimal hook_menu implementation. The menu item is only there since this is a demo. You'd probably want to use a MENU_CALLBACK instead of the default MENU_NORMAL_ITEM.

'gadget', 'title' => t('Google Gadget sample'), 'access' => TRUE, 'callback' => 'gadget_main', ); } return $items; } ?>

And, of course, any menu callback needs to be defined, so here is the gadget_main callback we just hook_menu'ed.

 

EOT ); ?>

heredoc syntax is convenient for such an example, but a production-leve example would obviously use something else here to pass to the GoogleGadget constructor.

Google APIs

Now, look at the contents of the CDATA section above: the __UP_ string is substituted on the fly by Google with the user preferences; and the final _IG_AdjustIFrameHeight(); call will allow the iGoogle page to use a Javascript resizing to fit the gadget's height to its contents.

Note that this depends on having the dynamic-height being set up as a module preference, which is done in the "Finishing up" section below.

Finishing up

Now we set up a few properties:

contentType = 'html'; // default value, just added here to show it exists $g->modulePrefs['title'] = 'Drupal API reference'; $g->modulePrefs['author'] = 'Frederic G. MARAND'; $g->modulePrefs['description'] = 'A simple module to demonstrate how to build google gadgets within Drupal'; $g->modulePrefs['author_email'] = 'http://blog.riff.org/contact'; $g->modulePrefs['author_link'] = 'http://blog.riff.org/'; $g->setModulePref('Require', 'feature', 'dynamic-height'); $g->addUserPref('apiversion', 'Drupal API version', array ( 'default_value' => '5' )); $g->render(); // includes die() to avoid drupal themeing } ?>

...and finish by invoking the object's render method. That's all there is to it !

Discussion

For production-level situations using such gadgets, I think it would be better to combine several strategies:

  • separate envelope and content: envelopes can typically be static, even when the content is dynamic, hence totally cached, with no load on drupal itself, of served
  • use a similar module to handle the attribute and content preparation and rendering, but...
  • actually render using a theme: this is cleaner within the drupal architecture, where every output is supposed to come from a theme, and actually allows pages to end normally, with all necessary closure actions, which is not the case when terminating with a die() as is done here.

As an exercice left to the reader: make the standard drupal autocomplete work in the gadgets.

Hi Xamox, thanks for the reference. I had already seen mentions of that module but hadn't checked what it did.

Reading the description of mysite.module, though, it seems it is more designed as a portlet container like the iGoogle or netvibes pages than as a module to generated Google Gadgets for use with iGoogle itself, as in the example is described.

[I'm the MySite maintener]

Correct. MySite can _use_ Google Gadgets, but it does not create them.

However, we could merge this code approach into the module and allow MySite content elements to be _exported_ to Google Gadgets.

I'm toying with ways to make MySite's content elements accessible as JavaScript, so that users could add content to their sites as well.

The Google Gadget method would be similar. See a related discussion at http://drupal.org/node/140151.

As to this: "use a similar module to handle the attribute and content preparation and rendering, but..." MySite can handle the content preparation, I just need to write two functions for that. Then we can pass the content to the new presentation layer that you've written.