Lesson #3 Class Notes wiki -- NodeAPI

We encourage users to post events happening in the community to the community events group on https://www.drupal.org.
You are viewing a wiki page. You are welcome to join the group and then edit it. Be bold!

[see attached file pastebin.txt, with all the coded versions of dojo.module]

NodeAPI Sunday!

Well, this is "the day after the night before" of Drupal's 6th birthday marking the release of Drupal 5.0 stable release, so... in honor of this the first thing to do is to update your sandbox development lesson playground sites to Drupal 5.0! (Just follow the all-new instructions in UPGRADE.txt).

Where we were at the end of Lesson #1

Please review that whole lesson if you have not already done so, just to remember what behavior we had going with the Dojo "object" (cck+module+theme) at the time.

In terms of configuration and code, we had created the content type Dojo (dojo), then used the CCK to add a field (Label: "Belt Color", Name: field_belt_color"). We had also created a Dojo module:

dojo.info:

; $Id: $
name = Dojo
description = Example
package = Dojo
version = VERSION

dojo.module:

<?php

/**
* Imlementation of hook_form_alter
*
* This will add a validation callback and turn an input format into
* radio buttons.
*/
function dojo_form_alter($form_id, &$form) {
  if (
$form_id == 'dojo_node_form') {
//  print_r($form);
   
$form['#theme'] = 'dojo';
   
$form['#validate']['dojo_validate'] = array();
   
$form['field_belt_color']['key']['#type'] = 'radios';
   
$tree = taxonomy_get_tree(1);
//    print_r($tree);
   
$belts = array();
    foreach(
$tree as $term) {
     
$belts[$term->tid] = $term->name;
    }
   
   
$form['field_belt_color']['key']['#options'] = $belts;
  
  }
}

function
theme_dojo($form) {
//  print_r($form);

 
$output .= drupal_render($form);
  return
$output;
}

function
dojo_validate($form_id, $form_values, $form) {
  if(
$form_values['title'] == 'Foo') {
   
form_set_error('title', t('Foo is not allowed!'));
  }
}

/**
* I'm writing this funtion because I need to understand db_rewrite_sql
*/
function dojo_query() {
 
$sql = 'SELECT * from {node} WHERE nid = %d';
 
$sql = db_rewrite_sql($sql);
 
print_r($sql);
 
$result = db_query($sql, 2);
 
$node = db_fetch_object($result);
 
print_r($node);
}

?>

We also had some theming going on, and we added the node-dojo.tpl.php file to the default garland theme directory, and altered the content div as follows:

  <div class="content">
<!--
     <?php print $content ?>
-->
  <?php
//print $content;
   
echo $node->content['body']['#value'];
    if (
$node->field_belt_color[0]['value'] == 'white') {
      echo
'<h4>White Belt! Welcome to The Dojo!</h4>';
    } elseif (
$node->field_belt_color[0]['value'] == 'black') {
      echo
'<h4>Sensei!</h4>';
    }
 
?>

  </div>

Starting point for the NodeAPI Sunday itself

Josh posted the dojo.info (same as above) and the dojo.module files to get us started. The dojo.module file is the same as above, with the addition of an empty nodeapi hook just to get us started. This is the dojo.module file Josh posted:

<?php

/**
* Implementation of hook_nodeapi
*
* For dojo lesson and great justice!
*
* dojo.module
*/

function dojo_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch (
$op) {
    case
'submit':
      break;
    case
'insert':
      break;
    case
'update':
      break;
    case
'view':
      break;
  }
}

/**
* Imlementation of hook_form_alter
*
* This will add a validation callback and turn an input format into
* radio buttons.
*/
function dojo_form_alter($form_id, &$form) {
  if (
$form_id == 'dojo_node_form') {
//  print_r($form);
   
$form['#theme'] = 'dojo';
   
$form['#validate']['dojo_validate'] = array();
   
$form['field_belt_color']['key']['#type'] = 'radios';
   
$tree = taxonomy_get_tree(1);
//    print_r($tree);
   
$belts = array();
    foreach(
$tree as $term) {
     
$belts[$term->tid] = $term->name;
    }
   
   
$form['field_belt_color']['key']['#options'] = $belts;
  
  }
}

function
theme_dojo($form) {
//  print_r($form);

 
$output .= drupal_render($form);
  return
$output;
}

function
dojo_validate($form_id, $form_values, $form) {
  if(
$form_values['title'] == 'Foo') {
   
form_set_error('title', t('Foo is not allowed!'));
  }
}

/**
* I'm writing this funtion because I need to understand db_rewrite_sql
*/
function dojo_query() {
 
$sql = 'SELECT * from {node} WHERE nid = %d';
 
$sql = db_rewrite_sql($sql);
 
print_r($sql);
 
$result = db_query($sql, 2);
 
$node = db_fetch_object($result);
 
print_r($node);
}
?>

Introductory remarks

So the idea here is that we are a community that is about self-help and development, we want to build skills, we want to get better at Drupal and we want to help other people starting to work and to get better at Drupal as well.

So the spirit of this is helping the platform grow, helping the marketplace grow, building out on the goodness of Drupal.

With Drupal 5.0 coming out, there will be a big spike of interest in the platform, so it is to be anticipated that this group will go through another round of growth. So all those now participating must pitch in to help when the "newbies" start showing up, to be helpful to them as well.

We record the lesson and leave behind documentation so that people six months from now can hopefully get a lot out of this, and we are building a real community around this as well.

Josh hopes that others will step up and give lessons also. It would be great if the good theme people could start giving lessons on how to make Drupal look really beautiful, this group is really open to anyone like that stepping up and giving a lesson also.

63 on IRC and 37 on Skype, that's a pretty large number of participants!

What's an API?

Before we get started with the NodeAPI, we realize it is not obvious what an API is, or does, or is good for. We consult the Wikipedia entry Application programming interface

There's a big leap from working on a project on your own, where everything pretty much lives in your head and you control every aspect of what's going on, on the one hand, to a project being worked on by a group of people and/or is meant to interface to other applications. The latter is becoming more and more the norm.

In the case of Drupal core, thousands of people contribute code, with thirty to forty people doing real major engineering on that. In the last lesson we spoke about coding standards, and utility functions, and how that matters a lot in participating in a community development project.

The API is a way of saying that no developer works alone, whenever code is produced, other people are going to want to mess with it, the API is a way of saying, I recognize that my application is not the be-all and end-all, and so I want to make specific, safe and structured ways for other applications and other modules to affect what is going on in my application.

An API is a way of no longer just hacking code, but of making that code more scalable, more stable, and more secure.

NodeAPI overview

The core of Drupal is the node system.
See http://api.drupal.org/api/HEAD/function/hook_nodeapi
The node is the core of all Drupal content. The most utilitarian content is the node.
And the node API is the real thorough, ruggedly tested, widely accessible, cool way to interface with Drupal's node system.

It used to be that there was no node api, and you had to write a module that defined a node and specified its features. Let's suppose (an example from the email list) story wasn't enough and you wanted to have an emotional story. So you would want, apart from the normal Title and Body, you would want an Emotion field, used to describe the emotional character of the story. In the old days, you would have to write emotionalstory.module with its own SQL table to store the extra "emotion" data, available to emotion type nodes.

Well, NodeAPI, which really started in 4.6, has matured in 4.7 and is now super-awesome in 5.0. Rather than say, this is my new node type with its specified functionality, it lets you say, I am going to add functionality which can be applied to any node type! For example, in the case of the file upload module (remember to enable in your brand new sandbox, to see this example), useful for posting images and other content, If you go to administer > content management and then edit any content type, if you scroll down the configuration form, you can see the section "Attachments", which can be enabled or disabled on any node type, once the upload module is enabled. That is because the module uses the NodeAPI to define its functionality, rather than saying there is a special node type that has attachments, it defines this property, this behavior of being able to have attachments, for all node types.

Implementing functionality with the NodeAPI and making it available as something that works for any node in the system, is a lot more powerful than building that behavior for just a single module.

The other thing that NodeAPI is really good for, is that in the same way that FormAPI lets you really tweak the behavior of a form, NodeAPI also lets you add functionality. It's a great way to piggy-back on existing Drupal functionality and teach it some cool trick it wasn't doing before. That really brings your site to where it needs to be.

Just like all the other hooks, NodeAPI is a hook. Every time practically anything happens to a node, Drupal says, is there a NodeAPI function that pertains to this, is there any module installed that wants to get involved in the node processing at this point in the node assembly line.

When you write a nodeapi function (like dojo_nodeapi() in our dojo module), the first parameter you are being passed is the node itself by reference. You are able to operate on the node as it is being passed through the system, which is why we think of it all as an assembly line.

For a wonderful overview diagram of this assembly line, see http://www-128.ibm.com/developerworks/ibm/library/i-osource5/sidefile1.html which appears in Part 5 - Getting started with Drupal of the fabulous IBM series of twelve articles related to Drupal.
Here is the bare link in case something gets lost in the markup: http://www-128.ibm.com/developerworks/ibm/osource/implement.html.

So, in the course of data being pulled out of the database, the node being assembled, then passed through a theme function and finally being displayed on the screen, this lets you work on it as it is being moved through the assembly line.

Iteration 1

As explained above, in the first class we had defined a Dojo content type, added a Belt Color field to it, implemented a dojo module and theme template, all this as a starting point, together with the skeleton dojo_nodeapi function placed in dojo.module:

<?php
/**
* Implementation of hook_nodeapi
<em>
</em> For dojo lesson and great justice!
<em>
</em> dojo.module
*/

function dojo_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch (
$op) {
    case
'submit':
      break;
    case
'insert':
      break;
    case
'update':
      break;
    case
'view':
      break;
  }
}
?>

Note: to show the power of NodeAPI in this lesson, the only thing you need to start off with is a Dojo content type created at administer > content types > Add content type, with a Title and a Body.

You get the node passed by reference, so if you really wanted to be disruptive, you could do "unset($node)" just before the switch block, and that would totally break Drupal, because you would essentially be taking every node as it comes off the assembly line and throwing it into the trash. So we are not going to do that, but you could. That is the power of NodeAPI.

The second parameter is $op. There are many ops apart from the cases present in this example (see API reference page). The operations refer to "what stage of the assembly line" we are actually in. They are all points at which we can say "Hey I want to do something to the node, at that point, in my module".

For example, in insert, update, delete, there you can insert code to synchronize with module specific database tables at the same time the core tables are being written to.

In view, the content is being assembled before rendering, so this is the place to affect the content (add something to the body, for example).

With NodeAPI, you can grab the node and do something with it.

Depending on the $op you then may have one or two additional arguments.

Let's do something very simple, in the case of submit:

<?php

/**
* Implementation of hook_nodeapi
*
* For dojo lesson and great justice!
*
* dojo.module
*/

function dojo_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch (
$op) {
    case
'submit':
      if (
$node->type == 'dojo') {
       
drupal_set_message('DOJO NODE!');
       }
      break;
    case
'insert':
      break;
    case
'update':
      break;
    case
'view':
      break;
  }
}
?>

So now, when we submit a new dojo type node, we see the message "DOJO NODE!" alongside "Your Dojo has been created".

An example of all this, is in the upload module itself, where it adds its attachment options as part of the content type configuration form:

function upload_form_alter($form_id, &$form) {
  if ($form_id == 'node_type_form' && isset($form['identity']['type'])) {
    $form['workflow']['upload'] = array(
      '#type' => 'radios',
      '#title' => t('Attachments'),
      '#default_value' => variable_get('upload_'. $form['#node_type']->type, 1),
      '#options' => array(t('Disabled'), t('Enabled')),
    );
  }
...

And then, in its implementation of the nodeapi hook, it can continually checks on this setting:

function upload_nodeapi(&$node, $op, $teaser) {
  switch ($op) {

    case 'load':
      $output = '';
      if (variable_get("upload_$node->type", 1) == 1) {
        $output['files'] = upload_load($node);
        return $output;
      }
      break;
...

Iteration 2

We can also affect the view operation:

    case 'view':
      If($node->type == 'dojo') {
        $node->content['body']['#value'] .= '<h1>Foo!</h1>';
      }
      break;

To eliminate conflicts with what we did we the theming part in Lesson #1 with the dojo module, we change the "content" div of the file nodo-dojo.tpl.php to read as follows:

  <div class="content">
  <?php
/*
    echo $node->content['body']['#value'];
    if ($node->field_belt_color[0]['value'] == 'white') {
      echo '<h4>White Belt! Welcome to The Dojo!</h4>';
    } elseif ($node->field_belt_color[0]['value'] == 'black') {
      echo '<h4>Sensei!</h4>';
    }
*/
 
print $content;

 
?>


  </div>

Iteration 3

Validate and Access control!

The idea here is for users to be able to post dojo content types without creating a new revision, only if they are assigned to a role for which the 'post dojo without revision' is set. Otherwise they should be shown an error message reminding them that it is obligatory to create a new revision upon posting a dojo.

The perm, nodeapi and form_alter hook implementations come into play. The complete dojo.module for this iteration looks like this:

<?php

/**
* Implementation of hook_perm
*
* Set some example permissions to check for dojo.module
*/

function dojo_perm() {
  return array(
'post dojo without revision');
}

/**
* Implementation of hook_nodeapi
*
* For dojo lesson and great justice!
*/

function dojo_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch (
$op) {
    case
'submit':
      If(
$node->type == 'dojo') {
       
drupal_set_message('DOJO NODE!');
      }
      break;
    case
'insert':
      break;
    case
'validate':
     
// what we want is for this to check the perms if the revision bit is not set.
     
if ($node->revision != 1 && user_access('post dojo without revision') == FALSE) {
       
form_set_error('revision', t('Hey man, you have to make a revision'));
      }
      break;
    case
'update':
      break;
    case
'view':
      If(
$node->type == 'dojo') {
       
$node->content['body']['#value'] .= '<h1>Foo!</h1>';
      }
      break;
  }
}

/**
* Imlementation of hook_form_alter
*
* This will add a validation callback and turn an input format into
* radio buttons.
*/
function dojo_form_alter($form_id, &$form) {
  if (
$form_id == 'dojo_node_form') {
//  print_r($form);
   
$form['#theme'] = 'dojo';
   
$form['#validate']['dojo_validate'] = array();
//    $form['field_belt_color']['key']['#type'] = 'radios';
   
$tree = taxonomy_get_tree(1);
//    print_r($tree);
   
$belts = array();
    foreach(
$tree as $term) {
//     $belts[$term->tid] = $term->name;
   
}
   
//    $form['field_belt_color']['key']['#options'] = $belts;
   
unset($form['options']['revision']);
   
$form['revision'] =  array(
                   
'#type' => 'checkbox',
                   
'#title' => 'Create new revision',
                   
'#default_value' => 1,
                   
'#prefix' => '<div style="font-size: 3em;">',
                   
'#suffix' => '</div>',
                   
'#weight' => -100,
                );
  }
}

function
theme_dojo($form) {
//  print_r($form);

 
$output .= drupal_render($form);
  return
$output;
}

function
dojo_validate($form_id, $form_values, $form) {
  if(
$form_values['title'] == 'Foo') {
   
form_set_error('title', t('Foo is not allowed!'));
  }
}

/**
* I'm writing this funtion because I need to understand db_rewrite_sql
*/
function dojo_query() {
 
$sql = 'SELECT * from {node} WHERE nid = %d';
 
$sql = db_rewrite_sql($sql);
 
print_r($sql);
 
$result = db_query($sql, 2);
 
$node = db_fetch_object($result);
 
print_r($node);
}
?>
<== remember, this is just for formatting purposes, we always leave it out in module files to avoid whitespace problems

To see this working, first of all you need to add a role in administer > user management > roles, for which the 'post dojo without revision' permission can be checked and unchecked. And then you need to attempt to post a dojo with and without that permission checked (and with and without checking the create new revision option.

The revision checkbox is now in huge letters at the top of the altered form -- see $form['revision'] array replacing usual revision array and given a weight of -100. See also creative use of form api in the dojo_form_alter() function: #weight (it's set to -100!), #prefix (how to put html as a label preceding widget), etc.

The order of the options elements makes no difference, of course, as long as they are present in the options array (check devel render tab (using devel module) on form to see the whole array).

We could have handled the altering of the form on a theme level, of course, in the corresponding template.php file.

So, to contrast the ways ("there's more than one way to do it"): using the NodeAPI we are interested in affecting the node itself as it moves down the node rendering assembly line; while we use the FormAPI when we are more concerned with the form presentation layer itself, how it is displayed to the user.

Questions on scalability

Fielding a general question on scalability, wanting to use Drupal for a really big project, with lots and lots of users.

You have to weight the business costs of writing a web application from the ground up in something like C++, versus using Drupal with somewhat less efficiency than an application started from scratch, but still completely scalable, having been used for applications with a huge number of user logins.

There are certainly pitfalls to avoid, there is a high performance group on groups.drupal.org, drupal.org itself is a pretty high performance website, especially considering the big spike in traffic for Drupal 5.0 without the site crashing.

In general CCK is fairly scalable. If you have a lot of fields you might want to write your own normalized, properly keyed and indexed and otherwise optimized database table to make it more efficient to be searched through.

When you use fields that are used in multiple nodes, then CCK is not as efficient, since multiple JOINS are being performed. There is a trade off involved, but it is now much more efficient than it used to be, and is certainly more effective than its predecessor flexinode, which is a little bit less friendly and less scalable in Josh's experience.

CCK ( + NodeAPI + FormAPI) is great for prototyping and hacking together a bunch of functionality, and may be more than adequate given the requirements at hand. But at the end of the day one has to ask if it is worth it to replace the CCK nodes with node (type) modules. Everything depends on the scalability requirements.

Locale certainly adds an additional load to the website because the t() function is firing a lot to replace strings, but it essentially adds memory overhead, as opposed to database overhead, and in Josh's experience, almost always the first thing to become a problem in a Drupal scalability scenario is your database: the amount of queries and slowing down of the system produces a noticeable slowing down from the users' point of view.

Often on some shared web hosting sites, the database server is physically a different machine than the web server, and if that is not a very well thought out architecture, it can occasion slowdowns. But if you're scaling you probably shouldn't be on shared hosting anyway.

But locale basically cache's all this stuff, so there is a hit in memory for loading all that stuff, but memory is pretty cheap, so while there is a performance overhead to having locale functionality, but Josh doesn't think it is all that high. There are a lot of international sites and a lot of them are actually pretty big.

Question: Is CCK hard to theme?

No, we did this in the first lesson ( http://groups.drupal.org/node/2284 ), when we saw that each node type gets its own node-yourccktype.tpl.php files.

The module contemplate makes it even easier to do, giving you a web-based interface to do it, it should certainly be checked out.

Iteration 4

NodeAPI and searching.
One operation in hook_nodeapi is 'update index'
The way that this works is that the search index calls the view function so it invokes node_view on the node and it uses that to figure out the key words and indexes the text, so if you have some data you want to put into the search index that's not coming up in node_view, which means essentially that it is not present in the body, then you need to use the update_index op to include it.

Search reference section in the Drupal API: http://api.drupal.org/api/HEAD/group/search

So we turn on the search module in our spanking new Drupal 5.x Dojo Sandbox installation, and make sure cron is either running or else we run cron manually from the administration pages. Now, purple, Foo and other strings we are adding in programmatically should be indexed also. Purple shows up (taxonomy term). Foo! is showing up also.

So we add the following case to the basic switch in dojo_nodapi():

    case 'update index':
      echo node_view($node);
      break;
    case 'view':
      If($node->type == 'dojo') {
        $node->content['body']['#value'] .= '<h1>dojotastic!</h1>';
      }
      break;

So now Foo! stops showing up, and we reindex via the administration options (search settings), then we run cron so that update_index is called.

And we search and dojotastic shows up and is searchable.

Iteration 5

But Josh is still frustrated because he doesn't want people to see 'dojotastic' rendered, rather he just wants it to go into the search index.
Now let's take out 'dojotastic' from the view op, and insert some content via the update_index op and do the following (only works with return!!! - discovered after a lot of trial and error, with help from the docs (which do prescribe use of return after all :) ), and with a 'little' help from old SamTressler working feverishly on his sandbox all the while):

    case 'update index':
      return 'dojotastic';
      break;
    case 'view':
      break;

or... better still...

    case 'update index':
      If($node->type == 'dojo') {
        return 'dojotastic';
      }
      break;
    case 'view':
      If($node->type == 'dojo') {
        $node->content['body']['#value'] .= '<h1>This was added in nodeapi for dojo nodes onle!</h1>';
      }

Iteration 6

How can we show users who are known puppy haters, for example, on searches?

We can do that with 'search result' (remembering that search stuff only works with return):

    case 'search result':
      if ($node->uid == 1) { // use the uid of your puppy-hater here
        $output = '<b>THIS USER HATES PUPPIES!</b>';
        return $output;
      }
      break;

Iteration 7

Premium puppy searches! (well, something roughly along those lines anyway):

See function dojo_form_alter(). We are using FormAPI to register an additional validate hook called dojo_search_validate. That function (see code below) is then saying, if this is not a privileged user AND their search key includes the word 'dojo', let's trigger an error and not let them do the search.

User #1 can do it. But the unprivileged user is not allowed to search on 'dojo'.

Our code finally as follows:

<?php

/**
* Implementation of hook_perm
*
* Set some example permissions to check for dojo.module
*/

function dojo_perm() {
  return array(
'post dojo without revision');
}

/**
* Implementation of hook_nodeapi
*
* For dojo lesson and great justice!
*/

function dojo_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  switch (
$op) {
    case
'submit':
      If(
$node->type == 'dojo') {
       
drupal_set_message('DOJO NODE SUBMITTED!');
      }
      break;
    case
'search result':
      if (
$node->uid == 1) { // use the uid of your puppy-hater here
       
$output = '<b>THIS USER HATES PUPPIES!</b>';
        return
$output;
      }
      break;
    case
'validate':
     
// what we want is for this to check the perms if the revision bit is not set.
     
if ($node->revision != 1 && user_access('post dojo without revision') == FALSE) {
       
form_set_error('revision', t('Hey man, you have to make a revision'));
      }
      break;
    case
'update index':
      If(
$node->type == 'dojo') {
        return
'dojotastic';
      }
      break;
    case
'view':
      If(
$node->type == 'dojo') {
       
$node->content['body']['#value'] .= '<h1>This was added in nodeapi for dojo nodes onle!</h1>';
      }
      break;
  }
}

/**
* Imlementation of hook_form_alter
*
* This will add a validation callback and turn an input format into
* radio buttons.
*/
function dojo_form_alter($form_id, &$form) {
  if (
$form_id == 'dojo_node_form') {
//  print_r($form);
   
$form['#theme'] = 'dojo';
   
$form['#validate']['dojo_validate'] = array();
//    $form['field_belt_color']['key']['#type'] = 'radios';
   
$tree = taxonomy_get_tree(1);
//    print_r($tree);
   
$belts = array();
    foreach(
$tree as $term) {
//     $belts[$term->tid] = $term->name;
   
}
   
//    $form['field_belt_color']['key']['#options'] = $belts;
   
unset($form['options']['revision']);
   
$form['revision'] =  array(
                   
'#type' => 'checkbox',
                   
'#title' => 'Create new revision',
                   
'#default_value' => 1,
                   
'#prefix' => '<div style="font-size: 3em;">',
                   
'#suffix' => '</div>',
                   
'#weight' => -100,
                );
  }
  if (
$form_id == 'search_form') {
   
// restrict search terms w/custom validation callback
   
$form['#validate']['dojo_search_validate'] = array();
  }
}

function
theme_dojo($form) {
//  print_r($form);

 
$output .= drupal_render($form);
  return
$output;
}

function
dojo_validate($form_id, $form_values, $form) {
  if(
$form_values['title'] == 'Foo') {
   
form_set_error('title', t('Foo is not allowed!'));
  }
}

function
dojo_search_validate($form_id, $form_values, $form) {
 
// if the user doesn't have access, stop them from searching for the dojo
 
if(user_access('post dojo without revision') == FALSE && strstr($form_values['keys'], 'dojo')) {
   
form_set_error('keys', t('Yoy cannot search for the Dojo!'));
  }
}

/**
* I'm writing this funtion because I need to understand db_rewrite_sql
*/
function dojo_query() {
 
$sql = 'SELECT * from {node} WHERE nid = %d';
 
$sql = db_rewrite_sql($sql);
 
print_r($sql);
 
$result = db_query($sql, 2);
 
$node = db_fetch_object($result);
 
print_r($node);
}
?>
AttachmentSize
pastebins.txt18.78 KB