You are here

menu_node.module in Menu Node API 7

Same filename and directory in other branches
  1. 6 menu_node.module

Menu Node API Manages relationships between the {node} and {menu_links} table.

File

menu_node.module
View source
<?php

// $Id: menu_node.module,v 1.15 2011/01/13 22:36:05 agentken Exp $

/**
 * @file
 * Menu Node API
 * Manages relationships between the {node} and {menu_links} table.
 */

/**
 * Implements hook_menu_link_insert().
 */
function menu_node_menu_link_insert($link) {
  if ($link['router_path'] != 'node/%' || !isset($link['mlid'])) {
    return;
  }
  $nid = str_replace('node/', '', $link['link_path']);
  if ($node = node_load($nid)) {
    menu_node_record_save($node, $link);
  }
}

/**
 * Implements hook_menu_link_update().
 */
function menu_node_menu_link_update($link) {
  if ($link['router_path'] != 'node/%') {

    // new link is NOT attached to a node
    if (menu_node_get_node($link['mlid'])) {

      // old link WAS attached to a node: menu_node record need to be deleted:
      menu_node_record_delete_by_link($link);
    }
  }
  else {

    // new link IS attached to a node
    $nid = str_replace('node/', '', $link['link_path']);
    if ($node = node_load($nid)) {
      menu_node_record_save($node, $link);
    }
  }
}

/**
 * Implements hook_menu_link_delete().
 */
function menu_node_menu_link_delete($link) {
  menu_node_record_delete_by_link($link);
}

/**
 * Implements hook_node_load().
 */
function menu_node_node_load($nodes, $types) {
  if (!is_array($nodes)) {
    return;
  }

  // TODO: Optimize for multiple nodes.
  foreach ($nodes as $node) {

    // Ensure the menu object is loaded.
    $node->menu_node_links = menu_node_get_links($node->nid);
  }
}

/**
 * Implements hook_node_update().
 *
 * It is possible that a node save does not trigger a menu change,
 * and other module may wish to be informed that the node data
 * has been updated. So do so here.
 */
function menu_node_node_update($node) {
  if (!empty($node->menu_node_links)) {
    foreach ($node->menu_node_links as $link) {
      _menu_node_invoke($node, $link, 'update');
    }
  }
}

/**
 * Needless, menu_node_delete() delete each menu items.
 * Implements hook_node_delete().
 */

/*
function menu_node_node_delete($node) {
 menu_node_record_delete($node);
}
*/

/**
 * Get the relevant node object for a menu link.
 *
 * This lookup function should be run for update and delete operations.
 *
 * @param int $mlid
 *   The menu link id.
 * @param bool $load
 *   TRUE (default) to return the full node object; FALSE to return only the
 *   node id.
 * @param bool $reset
 *   TRUE to flush this node from the entity load cache before loading the
 *   node object; FALSE (default) to load from the cache.
 * @return
 *   A node id, a complete node object or FALSE on failure.
 */
function menu_node_get_node($mlid, $load = TRUE, $reset = FALSE) {
  $nid = db_query("SELECT n.nid FROM {node} n INNER JOIN {menu_node} mn ON n.nid = mn.nid WHERE mn.mlid = :mlid", array(
    ':mlid' => $mlid,
  ))
    ->fetchField();
  if (empty($nid)) {
    return FALSE;
  }
  if ($load) {
    if ($reset) {
      return entity_load_unchanged('node', $nid);
    }
    else {
      return node_load($nid);
    }
  }
  return $nid;
}

/**
 * Get the relevant menu links for a node.
 *
 * @param $nid
 *   The node id.
 * @param $router
 *   Boolean flag indicating whether to attach the menu router link to the $item object.
 *   If set to TRUE, the router will be set as $item->menu_router.
 * @return
 *   An array of complete menu_link objects or an empy array on failure.
 */
function menu_node_get_links($nid, $router = FALSE) {
  $result = db_query("SELECT * FROM {menu_links} WHERE link_path = :link_path", array(
    ':link_path' => 'node/' . $nid,
  ));
  $items = array();
  foreach ($result as $data) {
    if ($router) {
      $data->menu_router = menu_get_item('node/' . $nid);
    }
    $items[$data->mlid] = $data;
  }
  return $items;
}

/**
 * Get all menu links assigned to a specific menu.
 *
 * @param $menu_name
 *   The machine name of the menu, e.g. 'navigation'.
 * @return
 *   A simple array of menu link ids.
 */
function menu_node_get_links_by_menu($menu_name) {
  $links = array();
  $result = db_query("SELECT mlid FROM {menu_links} WHERE menu_name = :menu_name", array(
    ':menu_name' => $menu_name,
  ));
  foreach ($result as $data) {
    $links[] = $data->mlid;
  }
  return $links;
}

/**
 * Get all nodes assigned to a specific menu.
 *
 * @param $menu_name
 *   The machine name of the menu, e.g. 'navigation'.
 * @param $load
 *   Boolean flag that indicates whether to load the node object or not.
 *   NOTE: This can be resource intensive!
 * @return
 *   A simple array of node ids.
 */
function menu_node_get_nodes_by_menu($menu_name, $load = FALSE) {
  $links = array();
  $result = db_query("SELECT mn.nid FROM {menu_node} mn INNER JOIN {menu_links} ml ON mn.mlid = ml.mlid WHERE ml.menu_name = :menu_name", array(
    ':menu_name' => $menu_name,
  ));
  foreach ($result as $data) {
    if ($load) {
      $nodes[$data->nid] = node_load($data->nid);
    }
    else {
      $nodes[] = $data->nid;
    }
  }
  return $nodes;
}

/**
 * Public function for generating a tree representation of nodes in a menu.
 *
 * This function is useful for showing the relationship between nodes within
 * a given menu tree. It can be used to build options lists for forms and other
 * user interface elements.
 *
 * @param $tree
 *   The parent menu tree, generated by menu_tree_all_data().
 * @param $menu
 *   The name of the menu for which to return data.
 * @param $filter
 *   An array of menu links ids that indicate the only children to return.
 *   That is, if this array is populated, only its members and their children will
 *   be returned by this function.
 * @param $options
 *   An array of processing options. The valid options are 'marker' and 'spacer'.
 *   -- 'marker' indicates a text mark to indicate menu depth for a menu link.
 *   -- 'spacer' indicates the text string to insert betwen a marker and its link title.
 * @return
 *   A nested array of menu data.
 */
function menu_node_tree($tree, $menu = NULL, $filter = array(), $options = array()) {
  $options += array(
    'marker' => '-',
    'spacer' => ' ',
  );
  $data = array();
  if (!empty($menu)) {
    _menu_node_tree($data, $menu, $value, $filter, $options['marker'], $options['spacer']);
    return $data[$menu];
  }
  else {
    foreach ($tree as $key => $value) {
      _menu_node_tree($data, $key, $value, $filter, $options['marker'], $options['spacer']);
    }
  }
  return $data;
}

/**
 * A private recursive sort function.
 *
 * Given a menu tree, return its child node links.
 *
 * @param $tree
 *   The recursive tree data.
 * @param $menu
 *   The menu that this data belongs to.
 * @param $data
 *   The tree data for this menu element.
 * @param $parents
 *   An array of menu link ids indicating the tree elements to return.
 * @param $marker
 *   A string (or NULL) to prepend to the menu link title, indicating relative depth.
 * @param $spacer
 *   A string (or NULL) to place between the marker and the title.
 * @return
 *   No return. Modify $tree by reference.
 */
function _menu_node_tree(&$tree, $menu, $data, $parents, $marker = NULL, $spacer = NULL) {
  if (empty($tree)) {
    $tree = array();
  }
  if (empty($parents)) {
    $parents = array();
  }
  if (in_array($data['link']['mlid'], $parents)) {
    $parent = menu_node_get_parent($data['link']);
    $tree[$parent][$data['link']['mlid']] = str_repeat($marker, $data['link']['depth']) . $spacer . $data['link']['title'] . ' ';
  }
  if (!empty($data['below'])) {

    // Recursive processing joy!
    foreach ($data['below'] as $value) {
      _menu_node_tree($tree, $menu, $value, $parents, $marker, $spacer);
    }
  }
}

/**
 * Return the parent link of a menu element.
 *
 * @param $item
 *   The menu item.
 * @param $return
 *   Indicates the value to return, options are:
 *   -- 'title' returns the name of the parent menu item.
 *   -- 'item' returns the entire parent, as loaded by menu_get_item().
 * @return
 *   A string, representing the parent name or a menu object.
 */
function menu_node_get_parent($item, $return = 'title') {
  if ($return == 'title') {
    return db_query("SELECT link_title FROM {menu_links} WHERE mlid = :mlid", array(
      ':mlid' => $item['p1'],
    ))
      ->fetchField();
  }
  $path = db_query("SELECT link_path FROM {menu_links} WHERE mlid = :mlid", array(
    ':mlid' => $item['p1'],
  ))
    ->fetchField();
  return menu_get_item($path);
}

/**
 * Save records to the {menu_node} table.
 *
 * After saving, we fire the appropriate menu_node hook,
 * either 'insert' or 'update'.
 *
 * @param $node
 *   The node being saved.
 * @param $link
 *   The menu link being saved.
 */
function menu_node_record_save($node, $link, $hook = 'update') {

  // Our internal API uses $link as an object, not an array.
  $link = (object) $link;
  $new = menu_node_exists($link->mlid);
  $record = array(
    'nid' => $node->nid,
    'mlid' => $link->mlid,
  );

  // Save if the record does not exist, otherwise update the existing link
  if (empty($new)) {
    drupal_write_record('menu_node', $record);
    $hook = 'insert';
  }
  else {
    drupal_write_record('menu_node', $record, 'mlid');
  }
  _menu_node_invoke($node, $link, $hook);
}

/**
 * Wrapper function for module hooks.
 *
 * @param $node
 *   The node being saved.
 * @param $link
 *   The menu link being saved.
 * @param $hook
 *   The hook to invoke ('insert', 'update', or 'delete').
 */
function _menu_node_invoke($node, $link, $hook) {

  // It is possible that this function will try to fire for both the node and menu
  // hooks. So we static cache the nid and only fire once per link per node
  // operation. Note that mlid is the primary key of our table.
  static $ids;
  if (isset($ids[$hook][$node->nid][$link->mlid])) {
    return;
  }
  $ids[$hook][$node->nid][$link->mlid] = TRUE;
  module_invoke_all('menu_node_invoke_' . $hook, $link, $node);
}

/**
 * Check to see if a specific mlid exists.
 *
 * @param $mlid
 *   The menu link id.
 * @return
 *   The count of matches (which should be 1 or 0).
 */
function menu_node_exists($mlid) {
  return (bool) db_query("SELECT COUNT(mlid) FROM {menu_node} WHERE mlid = :mlid", array(
    ':mlid' => $mlid,
  ))
    ->fetchField();
}

/**
 * (Deprecated)
 * Delete a record from {menu_node} and run hook_menu_node_record_delete().
 *
 * We deliberately run the hook before the delete, in case any module
 * wishes to run a JOIN on the {menu_node} table.
 *
 * @param $node
 *   The node being deleted.
 * @return
 *   No return. hook_menu_node_record_delete() is invoked.
 */
function menu_node_record_delete($node) {
  menu_node_record_delete_by_node($node);
}

/**
 * Delete a record from {menu_node} by nid and run hook_menu_node_record_delete().
 *
 * We deliberately run the hook before the delete, in case any module
 * wishes to run a JOIN on the {menu_node} table.
 *
 * @param $node
 *   The node being deleted.
 * @return
 *   No return. hook_menu_node_record_delete() is invoked.
 */
function menu_node_record_delete_by_node($node) {
  if (!empty($node->menu_node_links)) {
    foreach ($node->menu_node_links as $link) {
      _menu_node_invoke($node, $link, 'delete');
    }
  }

  // Now delete the record.
  db_delete('menu_node')
    ->condition('nid', $node->nid)
    ->execute();
}

/**
 * Delete a record from {menu_node} by mlid and run hook_menu_node_record_delete().
 *
 * We deliberately run the hook before the delete, in case any module
 * wishes to run a JOIN on the {menu_node} table.
 *
 * @param $link
 *   The menu link being deleted.
 * @return
 *   No return. hook_menu_node_record_delete() is invoked.
 */
function menu_node_record_delete_by_link($link) {
  $link = (object) $link;
  if ($node = menu_node_get_node($link->mlid)) {

    // TODO: If http://drupal.org/node/1012768 doesn't get in,
    // then we must attach the $link to the node and format as an
    // object, which is what our API expects.
    if (empty($node->menu_node_links)) {
      $node->menu_node_links[$link->mlid] = $link;
    }
    _menu_node_invoke($node, $link, 'delete');
  }

  // Now delete the record.
  db_delete('menu_node')
    ->condition('mlid', $link->mlid)
    ->execute();
}

Functions

Namesort descending Description
menu_node_exists Check to see if a specific mlid exists.
menu_node_get_links Get the relevant menu links for a node.
menu_node_get_links_by_menu Get all menu links assigned to a specific menu.
menu_node_get_node Get the relevant node object for a menu link.
menu_node_get_nodes_by_menu Get all nodes assigned to a specific menu.
menu_node_get_parent Return the parent link of a menu element.
menu_node_menu_link_delete Implements hook_menu_link_delete().
menu_node_menu_link_insert Implements hook_menu_link_insert().
menu_node_menu_link_update Implements hook_menu_link_update().
menu_node_node_load Implements hook_node_load().
menu_node_node_update Implements hook_node_update().
menu_node_record_delete (Deprecated) Delete a record from {menu_node} and run hook_menu_node_record_delete().
menu_node_record_delete_by_link Delete a record from {menu_node} by mlid and run hook_menu_node_record_delete().
menu_node_record_delete_by_node Delete a record from {menu_node} by nid and run hook_menu_node_record_delete().
menu_node_record_save Save records to the {menu_node} table.
menu_node_tree Public function for generating a tree representation of nodes in a menu.
_menu_node_invoke Wrapper function for module hooks.
_menu_node_tree A private recursive sort function.