4.6.x taxonomy.module taxonomy_node_save($nid, $terms)
4.7.x taxonomy.module taxonomy_node_save($nid, $terms)
5.x taxonomy.module taxonomy_node_save($nid, $terms)
6.x taxonomy.module taxonomy_node_save(&$node, $terms)

Save term associations for a given node.


modules/taxonomy/taxonomy.module, line 671
Enables the organization of content into categories.


function taxonomy_node_save(&$node, $terms) {

  // Free tagging vocabularies do not send their tids in the form,
  // so we'll detect them here and process them independently.
  if (isset($terms['tags'])) {
    $typed_input = $terms['tags'];
    foreach ($typed_input as $vid => $vid_value) {
      $typed_terms = drupal_explode_tags($vid_value);
      $inserted = array();
      foreach ($typed_terms as $typed_term) {

        // See if the term exists in the chosen vocabulary
        // and return the tid; otherwise, add a new record.
        $possibilities = taxonomy_get_term_by_name($typed_term);
        $typed_term_tid = NULL;

        // tid match, if any.
        foreach ($possibilities as $possibility) {
          if ($possibility->vid == $vid) {
            $typed_term_tid = $possibility->tid;
        if (!$typed_term_tid) {
          $edit = array(
            'vid' => $vid,
            'name' => $typed_term,
          $status = taxonomy_save_term($edit);
          $typed_term_tid = $edit['tid'];

        // Defend against duplicate, differently cased tags
        if (!isset($inserted[$typed_term_tid])) {
          db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $typed_term_tid);
          $inserted[$typed_term_tid] = TRUE;
  if (is_array($terms)) {
    foreach ($terms as $term) {
      if (is_array($term)) {
        foreach ($term as $tid) {
          if ($tid) {
            db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $tid);
      else {
        if (is_object($term)) {
          db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term->tid);
        else {
          if ($term) {
            db_query('INSERT INTO {term_node} (nid, vid, tid) VALUES (%d, %d, %d)', $node->nid, $node->vid, $term);

  // Flush the term "cache" for this node
  $node->taxonomy = taxonomy_node_get_terms($node, 'tid', TRUE);


rimian’s picture

Passing $node by reference, adding what's returned by taxonomy_node_get_terms($node) would be handy here.

Of course you can get around it easy enough by setting the weight of your module to be greater than the taxonomy module and calling the above function.

avibrazil@gmail.com’s picture

If you are programmatically importing and/or updating nodes with drush or a module, you'll have to test if each of your $node's taxonomy contain objects (because $node was setup with a node_load() call) or a plain array (because you are creating it from the first time).

I had to add the following function to my drush scripts so I can generically set terms to nodes, regardeless if they were node_load()ed or just created:

function node_add_term(&$node,$vid,$tid) {

        # Check if $node->taxonomy is an array of objects or simple values


        if ($objectMode) {
                if (!isset($node->taxonomy[$tid])) {
                        $node->taxonomy[$tid]=new stdClass();
        } else {
dolu’s picture

That's exactly the problem im going through now.

Some times $node looks like this :

$node->taxonomy[$vid] = $tid;

and sometimes like this

$node->taxonomy[$tid] = (object)$term;

That's very annoying, why is it handled two different ways!?

Dianna L’s picture

No idea why it is like this, but thanks so much for showing the two options! Helped me solve an issue without tearing all my hair out.