Index: menu.inc =================================================================== RCS file: /cvs/drupal/drupal/includes/menu.inc,v retrieving revision 1.255.2.38 diff -up -r1.255.2.38 menu.inc --- includes/menu.inc 09 Dec 2010 11:57:18 -0000 1.255.2.38 +++ includes/menu.inc 09 Dec 2010 11:57:18 -0000 @@ -1717,11 +1717,39 @@ /** * Collect, alter and store the menu definitions. + * + * This (modified) function should remain in menu.inc, + * so it can be called by other modules without require_once. + * + * @param $reset + * @param $changes object to collect $insert, $update, $delete arrays + * @return the menu array */ -function menu_router_build($reset = FALSE) { +function menu_router_build($reset = FALSE, $changes = NULL) { static $menu; - if (!isset($menu) || $reset) { + require_once 'includes/menu.rebuild_router.inc'; + $menu = _menu_router_build_rebuild($changes); + _menu_router_cache($menu); + } + return $menu; +} + + +/** + * Get callbacks from hook_menu implementations + * + * TODO: + * This belongs into menu.rebuild_router.inc, + * but remains here to make the patch more readable. + * The functionality has been in menu_router_build before. + * + * @return array callbacks + * arrays returned by hook_menu, with array_merge. + * In addition, each item knows about its module name. + */ +function _menu_router_build_calc_callbacks() { + if (true) { // We need to manually call each module so that we can know which module // a given item came from. $callbacks = array(); @@ -1736,10 +1764,8 @@ } // Alter the menu as defined in modules, keys are like user/%user. drupal_alter('menu', $callbacks); - $menu = _menu_router_build($callbacks); - _menu_router_cache($menu); } - return $menu; + return $callbacks; } /** @@ -2268,9 +2294,22 @@ } /** - * Helper function to build the router table based on the data from hook_menu. + * Calculate the menu from given callbacks, + * but don't touch the DB. + * + * Implementation based on the old function + * _menu_router_build($callbacks) + * + * TODO: + * - Move into a separate file (menu.rebuild_router.inc) + * - Split up into smaller functions? + * For now this is not done to make the patch easier to compare. + * (hopefully it is synced with the old _menu_router_build) + * + * @param $callbacks array collected from hook_menu implementations + * @return array($menu, $masks) */ -function _menu_router_build($callbacks) { +function _menu_router_build_calc_menu($callbacks) { // First pass: separate callbacks from paths, making paths ready for // matching. Calculate fitness, and fill some default values. $menu = array(); @@ -2353,13 +2392,6 @@ } array_multisort($sort, SORT_NUMERIC, $menu); - if (!$menu) { - // We must have a serious error - there is no data to save. - watchdog('php', 'Menu router rebuild failed - some paths may not work correctly.', array(), WATCHDOG_ERROR); - return array(); - } - // Delete the existing router since we have some data to replace it. - db_query('DELETE FROM {menu_router}'); // Apply inheritance rules. foreach ($menu as $path => $v) { $item = &$menu[$path]; @@ -2435,36 +2467,18 @@ 'module' => '', ); - // Calculate out the file to be included for each callback, if any. + // Find the file to be included for each callback, if any. if ($item['file']) { $file_path = $item['file path'] ? $item['file path'] : drupal_get_path('module', $item['module']); $item['include file'] = $file_path .'/'. $item['file']; } - - $title_arguments = $item['title arguments'] ? serialize($item['title arguments']) : ''; - db_query("INSERT INTO {menu_router} - (path, load_functions, to_arg_functions, access_callback, - access_arguments, page_callback, page_arguments, fit, - number_parts, tab_parent, tab_root, - title, title_callback, title_arguments, - type, block_callback, description, position, weight, file) - VALUES ('%s', '%s', '%s', '%s', - '%s', '%s', '%s', %d, - %d, '%s', '%s', - '%s', '%s', '%s', - %d, '%s', '%s', '%s', %d, '%s')", - $path, $item['load_functions'], $item['to_arg_functions'], $item['access callback'], - serialize($item['access arguments']), $item['page callback'], serialize($item['page arguments']), $item['_fit'], - $item['_number_parts'], $item['tab_parent'], $item['tab_root'], - $item['title'], $item['title callback'], $title_arguments, - $item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight'], $item['include file']); } - // Sort the masks so they are in order of descending fit, and store them. + + // Sort the masks so they are in order of descending fit. $masks = array_keys($masks); rsort($masks); - variable_set('menu_masks', $masks); - - return $menu; + + return array($menu, $masks); } /** --- includes/menu.rebuild_router.inc +++ includes/menu.rebuild_router.inc @@ -0,0 +1,204 @@ +insert[$path:string] array new db row + * $changes->update[$path:string] array modified fields in db row + * $changes->delete[$path:string] string path to be deleted + */ +function _menu_router_build_rebuild($changes = NULL) { + + // $callbacks - the arrays returned by hook_menu implementations + $callbacks = _menu_router_build_calc_callbacks(); + + // $menu - has some more information compared to 'callbacks'. + // $masks - no idea, but it's necessary. + list($menu, $masks) = _menu_router_build_calc_menu($callbacks); + + if (!$menu) { + // We must have a serious error - there is no data to save. + watchdog('php', 'Menu router rebuild failed - some paths may not work correctly.', array(), WATCHDOG_ERROR); + return array(); + } + + // calculate new table contents + $rows_new = _menu_router_build_calc_rows($menu); + + // compare with old table contents + if (!is_object($changes)) { + $changes = new stdClass; + } + // load the rows as with db_fetch_array, keyed by 'path' column + $rows_old = _menu_router_build_load_rows(); + // compare old and new rows + _menu_router_build_calc_changes($rows_old, $rows_new, $changes); + + // save changes + _menu_router_build_save_insert($changes->insert); + _menu_router_build_save_update($changes->update); + _menu_router_build_save_delete($changes->delete); + + // save the masks + variable_set('menu_masks', $masks); + + return $menu; +} + + +/** + * Calculate the new content for the menu_router table + * + * @param $menu array as generated by _menu_router_build_calc_menu() + * @return array the rows, as in db_fetch_array(). + */ +function _menu_router_build_calc_rows(array $menu) { + foreach ($menu as $path => $v) { + $item = &$menu[$path]; + $rows[$path] = array( + 'path' => $path, + 'load_functions' => $item['load_functions'], + 'to_arg_functions' => $item['to_arg_functions'], + 'access_callback' => $item['access callback'], + 'access_arguments' => serialize($item['access arguments']), + 'page_callback' => $item['page callback'], + 'page_arguments' => serialize($item['page arguments']), + 'fit' => $item['_fit'], + 'number_parts' => $item['_number_parts'], + 'tab_parent' => $item['tab_parent'], + 'tab_root' => $item['tab_root'], + 'title' => $item['title'], + 'title_callback' => $item['title callback'], + 'title_arguments' => $item['title arguments'] ? serialize($item['title arguments']) : '', + 'type' => $item['type'], + 'block_callback' => $item['block callback'], + 'description' => $item['description'], + 'position' => $item['position'], + 'weight' => $item['weight'], + 'file' => $item['include file'], + ); + } + + return $rows; +} + + +/** + * Find the changes to be stored in the menu_router table. + * + * @param $rows_old + * @param $rows_new + * @param $changes stdClass + * object to collect changes to menu_router table + * $changes->insert[$path:string] array new db row + * $changes->update[$path:string] array modified fields in db row + * $changes->delete[$path:string] string path to be deleted + * @return $changes stdClass + */ +function _menu_router_build_calc_changes($rows_old, $rows_new, $changes = NULL) { + if (!is_object($changes)) { + $changes = new stdClass; + } + $changes->insert = array(); + $changes->update = array(); + $changes->delete = array(); + foreach ($rows_new as $path => $row_new) { + if (!isset($rows_old[$path])) { + $changes->insert[$path] = $row_new; + } else { + $row_old = $rows_old[$path]; + $row_changes = array(); + foreach ($row_new as $key => $value) { + if ($value != $row_old[$key]) { + $row_changes[$key] = $value; + } + } + if (!empty($row_changes)) { + $changes->update[$path] = $row_changes; + } + unset($rows_old[$path]); + } + } + // delete remaining rows + foreach ($rows_old as $path => $row_old) { + $changes->delete[$path] = $path; + } + return $changes; +} + + +/** + * Load the old contents of menu_router + * Does an unrestricted SELECT on menu_router. + * + * @return array rows as in db_fetch_array, keyed by 'path' column + */ +function _menu_router_build_load_rows() { + $rows = array(); + $result = db_query("SELECT * FROM {menu_router}"); + while ($row = db_fetch_array($result)) { + $rows[$row['path']] = $row; + } + return $rows; +} + +/** + * Execute INSERT queries + * + * @param $insert array of rows to insert + */ +function _menu_router_build_save_insert($insert) { + if (count($insert)) { + // the $sql is always the same + $fields = array(); + $tokens = array(); + $example_row = end($insert); + foreach ($example_row as $k => $v) { + $fields[] = $k; + $tokens[] = "'%s'"; + } + $fields = implode(', ', $fields); + $tokens = implode(', ', $tokens); + // TODO: ON DUPLICATE KEY UPDATE ? + $sql = "INSERT INTO {menu_router} ($fields) VALUES ($tokens)"; + // insert the rows + foreach ($insert as $row) { + $args = array_values($row); + db_query($sql, $args); + } + } +} + +/** + * Execute UPDATE queries + * + * @param $update array of rows with modifications + */ +function _menu_router_build_save_update($update) { + foreach ($update as $path => $row_changes) { + unset($row_changes['path']); + $set = array(); + foreach ($row_changes as $k => $v) { + $set[] = "$k = '%s'"; + } + $set = implode(', ', $set); + $sql = "UPDATE {menu_router} SET $set WHERE path = '%s'"; + $args = array_values($row_changes); + $args[] = $path; + db_query($sql, $args); + } +} + +/** + * Execute DELETE queries + * + * @param $delete array of paths (PRIMARY KEY) to delete + */ +function _menu_router_build_save_delete($delete) { + $sql = "DELETE FROM {menu_router} WHERE path = '%s'"; + foreach ($delete as $path) { + db_query($sql, $path); + } +} +