Writing an intelligent hook_nodeapi function in Drupal

Posted on

For Drupal module developers, the hook_nodeapi function affords a lot of flexibility for interacting with nodes at various operation. If you just start pasting or writing code, you'll quickly end up with a giant, messy switch statement. But there is a simpel way to keep your code nicely organized. First, lets take a look at what operations we can affect:

  • "alter": the \$node->content array has been rendered, so the node body or teaser is filtered and now contains HTML. This op should only be used when text substitution, filtering, or other raw text operations are necessary.
  • "delete": The node is being deleted.
  • "delete revision": The revision of the node is deleted. You can delete data associated with that revision.
  • "insert": The node is being created (inserted in the database).
  • "load": The node is about to be loaded from the database. This hook can be used to load additional data at this time.
  • "prepare": The node is about to be shown on the add/edit form.
  • "prepare translation": The node is being cloned for translation. Load additional data or copy values from \$node->translation_source.
  • "print": Prepare a node view for printing. Used for printer-friendly view in book_module
  • "rss item": An RSS feed is generated. The module can return properties to be added to the RSS item generated for this node. See comment_nodeapi() and upload_nodeapi() for examples. The \$node passed can also be modified to add or remove contents to the feed item.
  • "search result": The node is displayed as a search result. If you want to display extra information with the result, return it.
  • "presave": The node passed validation and is about to be saved. Modules may use this to make changes to the node before it is saved to the database.
  • "update": The node is being updated.
  • "update index": The node is being indexed. If you want additional information to be indexed which is not already visible through nodeapi "view", then you should return it here.
  • "validate": The user has just finished editing the node and is trying to preview or submit it. This hook can be used to check the node data. Errors should be set with form_set_error().
  • "view": The node content is being assembled before rendering. The module may add elements \$node->content prior to rendering. This hook will be called after hook_view(). The format of \$node->content is the same as used by Forms API.

That's 15 operations, multiplied by the number of content types on your site, that's potentially 15N cases we'll have to account for. If you want your code to run regardless of the content type, thats another 15 cases. How much do you like spaghetti?

Our own function naming convention to the rescue

By using a simple naming convention, we can make sure that a) we can drop in code to run for any operation and/or content type b) encapsulate the code within its own function. To do this, your actual hook_nodeapi implementation will simply delagate execution to these other functions.  First, if a function named module_nodeapi_operation exists, we'll call it. Then if the function module_nodeapi_operation_content-type exists, execute it. Let's assume you have a module foo, the hook_nodeapi function looks like:  

  1. \
[/\*\*]{.co4}
  1. [ \* Implementation of hook_nodeapi that delegates operations to other functions]{.co4}
[ \* \@see http://api.drupal.org/api/function/hook_nodeapi/6]{.co4}
  1. [ \* \@author Oscar Merida ]{.co4}
[ \*/]{.co4}
  1. [function]{.kw2} foo_nodeapi[(]{.br0}[&]{.sy0}[\$node]{.re0}[,]{.sy0} [\$op]{.re0}[,]{.sy0}[\$a3]{.re0} [=]{.sy0} [NULL]{.kw2}[,]{.sy0} [\$a4]{.re0} [=]{.sy0} [NULL]{.kw2}[)]{.br0}
[{]{.br0}
  1. [ // our own all too clever function api]{.co1}
[ // first call foo\_\$op if it exists]{.co1}
  1. [ // then call foo\_\$op\_\$node-\>type if it exists ]{.co1}
[ \$f_base]{.re0} [=]{.sy0} ['foo_nodeapi\_']{.st_h} [.]{.sy0} [\$op]{.re0}[;]{.sy0}
  1. [ if]{.kw1} [(]{.br0}[[function_exists]{.kw3}](http://www.php.net/function_exists)[(]{.br0}[\$f_base]{.re0}[)]{.br0}[)]{.br0}
[ {]{.br0}
  1. [ \$f_base]{.re0}[(]{.br0}[&]{.sy0}[\$node]{.re0}[,]{.sy0} [\$a3]{.re0}[,]{.sy0} [\$a4]{.re0}[)]{.br0}[;]{.sy0}
[ }]{.br0}
[ \$f_content]{.re0} [=]{.sy0} [\$f_base]{.re0} [.]{.sy0} ['\_']{.st_h} [.]{.sy0} [\$node]{.re0}[-\>]{.sy0}[type]{.me1}[;]{.sy0}
  1. [ if]{.kw1} [(]{.br0}[[function_exists]{.kw3}](http://www.php.net/function_exists)[(]{.br0}[\$f_content]{.re0}[)]{.br0}[)]{.br0}
[ {]{.br0}
  1. [ \$f_content]{.re0}[(]{.br0}[&]{.sy0}[\$node]{.re0}[,]{.sy0} [\$a3]{.re0}[,]{.sy0} [\$a4]{.re0}[)]{.br0}[;]{.sy0}
[ }]{.br0}
  1. [}]{.br0}

If you need to alter something about all nodes before they are saved, you would create a function named foo_nodeapi_presave.

  1. \
[function]{.kw2} foo_nodeapi_presave[(]{.br0}[&]{.sy0}[\$node]{.re0}[,]{.sy0} [\$op]{.re0}[,]{.sy0} [\$a3]{.re0}[,]{.sy0} [\$a4]{.re0}[)]{.br0}
  1. [{]{.br0}
[ // do something to all nodes before they are saved]{.co1}
  1. [}]{.br0}

Likewise, to affect what is loaded with a story node, we need a function named foo_nodeapi_load_story:

  1. \
[function]{.kw2} foo_nodeapi_load_story[(]{.br0}[&]{.sy0}[\$node]{.re0}[,]{.sy0} [\$op]{.re0}[,]{.sy0} [\$a3]{.re0}[,]{.sy0} [\$a4]{.re0}[)]{.br0}
  1. [{]{.br0}
[ // do something to story nodes when they are loaded]{.co1}
  1. [}]{.br0}

Caveats

Keep in mind that both functions are called, but the content type specific one is called after the more general one so you can undo/override the latter. As a side note, I was trying to use the presave operation to set the value of a CCK Text field if it was empty. Since the order of hook calls depend on the weight of the module in the system table, I had to make sure the content module had a higher weight than my own. If you're trying to set the value of a CCK field but it's not being saved, you'll have to do the same.

Tags: Drupal

─── ✧ ─── ✦ ─── ✧ ───

jQuery 1.3 released - I heart jquery.

Posted on

Just when you thought jquery couldn't get better, the 1.3 release shows drastic performance improvements across a number of benchmarks. That, and the new live event framework, which will make your life easier if you've ever dealth with binding and rebinding events (F1 Timetracker & Workload I'm looking at you), are valuable improvements to the core. Has it only been out for three years? How did I ever do any javascripting without it? Release:jQuery 1.3 - jQuery JavaScript Library

\ \

One thing that became very obvious during the development of the new engine: We wanted to be able to collaborate on it with other libraries and developers. We saw an opportunity for some solid collaboration with some of the best JavaScript developers - the result of which will help users of all libraries. For this reason we made sure that Sizzle was able to work completely standalone (no dependencies).

Tags: Javascript, jQuery

─── ✧ ─── ✦ ─── ✧ ───

HOWTO: Redirect a site to a single hostname with Apache

Posted on

There are a number of reasons for making sure that your website is served from a single hostname, that is the address users see in their browser's location bar. This post will show you how to do that once with two rewrite rules and never worry againg. A single hostname helps with SEO by not penalizing you for having you content crawled at multiple urls (www.oscarm.org vs oscarm.org for example). Also, SSL certificates are issued to a single domain, if your certificate is for www.oscarm.org and a user goes to https://oscarm.org/, the user would see a warning about the certicate not matching. You could also be using hostnames for marketing short urls - party.oscarm.org, for example. In all these cases you want visitors to end up at a single domain, www.oscarm.org. If you're using Apache to serve your website, you can do this using two simple mod_rewrite directives.  You'll have to adjust them if you put them in your VirtualHost block or in an .htaccess file.

RewriteCond %{HTTP_HOST} !\^oscarm.org\ RewriteCond /?(.\*) https://oscarm.org/\$1 \[R=permanent,L\]

Why is this technique preferable? Most snippets you'll see online will show you how to redirect one host name to another. By using the exclamation point to speciy a non-matching pattern, we can redirect any host name pointed at this virtual host to our desired host name with just these two lines. Make sure to include the R=permanent flag as well, so that search engines only index pages using the desired host name.

─── ✧ ─── ✦ ─── ✧ ───