Commit 2e18cb89 authored by Dries's avatar Dries

- Patch #221964 by chx, dopry, webernet, moshe, webchick, justinrandall, flobruit

  et al.  Can you say 'registry'?  Drupal now maintains an internal registry of
  all functions or classes in the system, allowing it to lazy-load code files as
  needed (reducing the amount of code that must be parsed on each request). The
  list of included files is cached per menu callback for subsequent loading by
  the menu router. This way, a given page request will have all the code it needs
  but little else, minimizing time spent parsing unneeded code.
parent c100468c
......@@ -949,6 +949,9 @@ function _drupal_bootstrap($phase) {
// Initialize the default database.
require_once './includes/database.inc';
db_set_active();
// Register autoload functions so that we can access classes and interfaces.
spl_autoload_register('drupal_autoload_class');
spl_autoload_register('drupal_autoload_interface');
break;
case DRUPAL_BOOTSTRAP_ACCESS:
......@@ -1134,3 +1137,182 @@ function ip_address() {
return $ip_address;
}
/**
* @ingroup registry
* @{
*/
/**
* Confirm that a function is available.
*
* If the function is already available, this function does nothing.
* If the function is not available, it tries to load the file where the
* function lives. If the file is not available, it returns false, so that it
* can be used as a drop-in replacement for function_exists().
*
* @param $function
* The name of the function to check or load.
* @return
* TRUE if the function is now available, FALSE otherwise.
*/
function drupal_function_exists($function) {
static $checked = array();
if (defined('MAINTENANCE_MODE')) {
return function_exists($function);
}
if (isset($checked[$function])) {
return $checked[$function];
}
$checked[$function] = FALSE;
if (function_exists($function)) {
registry_mark_code('function', $function);
$checked[$function] = TRUE;
return TRUE;
}
$file = db_result(db_query("SELECT filename FROM {registry} WHERE name = '%s' AND type = '%s'", $function, 'function'));
if ($file) {
require_once($file);
$checked[$function] = function_exists($function);
if ($checked[$function]) {
registry_mark_code('function', $function);
}
}
return $checked[$function];
}
/**
* Confirm that an interface is available.
*
* This function parallels drupal_function_exists(), but is rarely
* called directly. Instead, it is registered as an spl_autoload()
* handler, and PHP calls it for us when necessary.
*
* @param $interface
* The name of the interface to check or load.
* @return
* TRUE if the interface is currently available, FALSE otherwise.
*/
function drupal_autoload_interface($interface) {
return _registry_check_code('interface', $interface);
}
/**
* Confirm that a class is available.
*
* This function parallels drupal_function_exists(), but is rarely
* called directly. Instead, it is registered as an spl_autoload()
* handler, and PHP calls it for us when necessary.
*
* @param $class
* The name of the class to check or load.
* @return
* TRUE if the class is currently available, FALSE otherwise.
*/
function drupal_autoload_class($class) {
return _registry_check_code('class', $class);
}
/**
* Helper for registry_check_{interface, class}.
*/
function _registry_check_code($type, $name) {
$file = db_result(db_query("SELECT filename FROM {registry} WHERE name = '%s' AND type = '%s'", $name, $type));
if ($file) {
require_once($file);
registry_mark_code($type, $name);
return TRUE;
}
}
/**
* Collect the resources used for this request.
*
* @param $type
* The type of resource.
* @param $name
* The name of the resource.
* @param $return
* Boolean flag to indicate whether to return the resources.
*/
function registry_mark_code($type, $name, $return = FALSE) {
static $resources = array();
if ($type && $name) {
if (!isset($resources[$type])) {
$resources[$type] = array();
}
if (!in_array($name, $resources[$type])) {
$resources[$type][] = $name;
}
}
if ($return) {
return $resources;
}
}
/**
* Rescan all enabled modules and rebuild the registry.
*
* Rescans all code in modules or includes directory, storing a mapping of
* each function, file, and hook implementation in the database.
*/
function drupal_rebuild_code_registry() {
require_once './includes/registry.inc';
_drupal_rebuild_code_registry();
}
/**
* Save hook implementations cache.
*
* @param $hook
* Array with the hook name and list of modules that implement it.
* @param $write_to_persistent_cache
* Whether to write to the persistent cache.
*/
function registry_cache_hook_implementations($hook, $write_to_persistent_cache = FALSE) {
static $implementations;
if ($hook) {
// Newer is always better, so overwrite anything that's come before.
$implementations[$hook['hook']] = $hook['modules'];
}
if ($write_to_persistent_cache === TRUE) {
cache_set('hooks', $implementations, 'cache_registry');
}
}
/**
* Save the files required by the registry for this path.
*/
function registry_cache_path_files() {
if ($used_code = registry_mark_code(NULL, NULL, TRUE)) {
$files = array();
$type_sql = array();
$params = array();
foreach ($used_code as $type => $names) {
$type_sql[] = "(name IN (" . db_placeholders($names, 'varchar') . ") AND type = '%s')";
$params = array_merge($params, $names);
$params[] = $type;
}
$res = db_query("SELECT DISTINCT filename FROM {registry} WHERE " . implode(' OR ', $type_sql), $params);
while ($row = db_fetch_object($res)) {
$files[] = $row->filename;
}
if ($files) {
$menu = menu_get_item();
cache_set('registry:' . $menu['path'], implode(';', $files), 'cache_registry');
}
}
}
/**
* @} End of "ingroup registry".
*/
......@@ -1460,11 +1460,15 @@ function l($text, $path, $options = array()) {
* react to the closing of the page by calling hook_exit().
*/
function drupal_page_footer() {
if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) {
page_set_cache();
}
module_invoke_all('exit');
registry_cache_hook_implementations(FALSE, TRUE);
registry_cache_path_files();
}
/**
......@@ -2700,7 +2704,7 @@ function drupal_render(&$elements) {
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $function) {
if (function_exists($function)) {
if (drupal_function_exists($function)) {
$elements = $function($elements);
}
}
......@@ -2762,7 +2766,7 @@ function drupal_render(&$elements) {
// which allows the output'ed text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $function) {
if (function_exists($function)) {
if (drupal_function_exists($function)) {
$content = $function($content, $elements);
}
}
......@@ -3142,7 +3146,7 @@ function drupal_uninstall_schema($module) {
*/
function drupal_get_schema_unprocessed($module, $table = NULL) {
// Load the .install file to get hook_schema.
module_load_include('install', $module);
module_load_install($module);
$schema = module_invoke($module, 'schema');
if (!is_null($table) && isset($schema[$table])) {
......@@ -3528,6 +3532,7 @@ function drupal_flush_all_caches() {
// Change query-strings on css/js files to enforce reload for all users.
_drupal_flush_css_js();
drupal_rebuild_code_registry();
drupal_clear_css_cache();
drupal_clear_js_cache();
system_theme_data();
......@@ -3536,7 +3541,7 @@ function drupal_flush_all_caches() {
node_types_rebuild();
// Don't clear cache_form - in-progress form submissions may break.
// Ordered so clearing the page cache will always be the last action.
$core = array('cache', 'cache_block', 'cache_filter', 'cache_page');
$core = array('cache', 'cache_block', 'cache_filter', 'cache_registry', 'cache_page');
$cache_tables = array_merge(module_invoke_all('flush_caches'), $core);
foreach ($cache_tables as $table) {
cache_clear_all('*', $table, TRUE);
......
......@@ -327,7 +327,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
// We first check to see if there's a function named after the $form_id.
// If there is, we simply pass the arguments on to it to get the form.
if (!function_exists($form_id)) {
if (!drupal_function_exists($form_id)) {
// In cases where many form_ids need to share a central constructor function,
// such as the node editing form, modules can implement hook_forms(). It
// maps one or more form_ids to the correct constructor functions.
......@@ -348,6 +348,7 @@ function drupal_retrieve_form($form_id, &$form_state) {
}
if (isset($form_definition['callback'])) {
$callback = $form_definition['callback'];
drupal_function_exists($callback);
}
}
......@@ -504,13 +505,13 @@ function drupal_prepare_form($form_id, &$form, &$form_state) {
$form += _element_info('form');
if (!isset($form['#validate'])) {
if (function_exists($form_id . '_validate')) {
if (drupal_function_exists($form_id . '_validate')) {
$form['#validate'] = array($form_id . '_validate');
}
}
if (!isset($form['#submit'])) {
if (function_exists($form_id . '_submit')) {
if (drupal_function_exists($form_id . '_submit')) {
// We set submit here so that it can be altered.
$form['#submit'] = array($form_id . '_submit');
}
......@@ -712,7 +713,7 @@ function _form_validate($elements, &$form_state, $form_id = NULL) {
// #value data.
elseif (isset($elements['#element_validate'])) {
foreach ($elements['#element_validate'] as $function) {
if (function_exists($function)) {
if (drupal_function_exists($function)) {
$function($elements, $form_state, $complete_form);
}
}
......@@ -749,7 +750,7 @@ function form_execute_handlers($type, &$form, &$form_state) {
}
foreach ($handlers as $function) {
if (function_exists($function)) {
if (drupal_function_exists($function)) {
if ($type == 'submit' && ($batch =& batch_get())) {
// Some previous _submit handler has set a batch. We store the call
// in a special 'control' batch set, for execution at the correct
......@@ -1032,7 +1033,7 @@ function _form_builder_handle_input_element($form_id, &$form, &$form_state, $com
// checkboxes and files.
if (isset($form['#process']) && !$form['#processed']) {
foreach ($form['#process'] as $process) {
if (function_exists($process)) {
if (drupal_function_exists($process)) {
$form = $process($form, isset($edit) ? $edit : NULL, $form_state, $complete_form);
}
}
......
......@@ -687,8 +687,9 @@ function drupal_check_profile($profile) {
$requirements = array();
foreach ($installs as $install) {
require_once $install->filename;
if (module_hook($install->name, 'requirements')) {
$requirements = array_merge($requirements, module_invoke($install->name, 'requirements', 'install'));
$function = $install->name. '_requirements';
if (function_exists($function)) {
$requirements = array_merge($requirements, $function('install'));
}
}
return $requirements;
......
......@@ -115,7 +115,7 @@ function drupal_mail($module, $key, $to, $language, $params = array(), $from = N
// Build the e-mail (get subject and body, allow additional headers) by
// invoking hook_mail() on this module. We cannot use module_invoke() as
// we need to have $message by reference in hook_mail().
if (function_exists($function = $module . '_mail')) {
if (drupal_function_exists($function = $module . '_mail')) {
$function($key, $message, $params);
}
......
......@@ -339,11 +339,16 @@ function menu_execute_active_handler($path = NULL) {
menu_rebuild();
}
if ($router_item = menu_get_item($path)) {
$cache = cache_get('registry:' . $router_item['path'], 'cache_registry');
if (!empty($cache->data)) {
foreach(explode(';', $cache->data) as $file) {
require_once($file);
}
}
if ($router_item['access']) {
if ($router_item['file']) {
require_once($router_item['file']);
if (drupal_function_exists($router_item['page_callback'])) {
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
return call_user_func_array($router_item['page_callback'], $router_item['page_arguments']);
}
else {
return MENU_ACCESS_DENIED;
......@@ -1665,7 +1670,7 @@ function menu_router_build($reset = FALSE) {
// We need to manually call each module so that we can know which module
// a given item came from.
$callbacks = array();
foreach (module_implements('menu') as $module) {
foreach (module_implements('menu', NULL, TRUE) as $module) {
$router_items = call_user_func($module . '_menu');
if (isset($router_items) && is_array($router_items)) {
foreach (array_keys($router_items) as $path) {
......@@ -2201,12 +2206,12 @@ function _menu_router_build($callbacks) {
$load_functions[$k] = NULL;
}
else {
if (function_exists($matches[1] . '_to_arg')) {
if (drupal_function_exists($matches[1] . '_to_arg')) {
$to_arg_functions[$k] = $matches[1] . '_to_arg';
$load_functions[$k] = NULL;
$match = TRUE;
}
if (function_exists($matches[1] . '_load')) {
if (drupal_function_exists($matches[1] . '_load')) {
$function = $matches[1] . '_load';
// Create an array of arguments that will be passed to the _load
// function when this menu path is checked, if 'load arguments'
......@@ -2293,12 +2298,6 @@ function _menu_router_build($callbacks) {
if (!isset($item['page arguments']) && isset($parent['page arguments'])) {
$item['page arguments'] = $parent['page arguments'];
}
if (!isset($item['file']) && isset($parent['file'])) {
$item['file'] = $parent['file'];
}
if (!isset($item['file path']) && isset($parent['file path'])) {
$item['file path'] = $parent['file path'];
}
}
}
}
......@@ -2326,34 +2325,25 @@ function _menu_router_build($callbacks) {
'tab_parent' => '',
'tab_root' => $path,
'path' => $path,
'file' => '',
'file path' => '',
'include file' => '',
);
// Calculate out 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)
type, block_callback, description, position, weight)
VALUES ('%s', '%s', '%s', '%s',
'%s', '%s', '%s', %d,
%d, '%s', '%s',
'%s', '%s', '%s',
%d, '%s', '%s', '%s', %d, '%s')",
%d, '%s', '%s', '%s', %d)",
$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']);
$item['type'], $item['block callback'], $item['description'], $item['position'], $item['weight']);
}
// Sort the masks so they are in order of descending fit, and store them.
$masks = array_keys($masks);
......
......@@ -221,7 +221,7 @@ function _module_build_dependencies($files) {
*/
function module_exists($module) {
$list = module_list();
return array_key_exists($module, $list);
return isset($list[$module]);
}
/**
......@@ -231,7 +231,14 @@ function module_load_install($module) {
// Make sure the installation API is available
include_once './includes/install.inc';
module_load_include('install', $module);
$file = module_load_include('install', $module);
// Ensure that you can module_invoke something inside the newly-loaded
// file during install.
$module_list = module_list();
if (!isset($module_list[$module])) {
$module_list[$module]['filename'] = $file;
module_list(TRUE, FALSE, FALSE, $module_list);
}
}
/**
......@@ -253,6 +260,7 @@ function module_load_include($type, $module, $name = NULL) {
if (is_file($file)) {
require_once $file;
return $file;
}
else {
return FALSE;
......@@ -292,7 +300,7 @@ function module_enable($module_list) {
// Refresh the module list to include the new enabled module.
module_list(TRUE, FALSE);
// Force to regenerate the stored list of hook implementations.
module_implements('', FALSE, TRUE);
drupal_rebuild_code_registry();
}
foreach ($invoke_modules as $module) {
......@@ -301,7 +309,7 @@ function module_enable($module_list) {
// We check for the existence of node_access_needs_rebuild() since
// at install time, module_enable() could be called while node.module
// is not enabled yet.
if (function_exists('node_access_needs_rebuild') && !node_access_needs_rebuild() && module_hook($module, 'node_grants')) {
if (drupal_function_exists('node_access_needs_rebuild') && !node_access_needs_rebuild() && module_hook($module, 'node_grants')) {
node_access_needs_rebuild(TRUE);
}
}
......@@ -333,7 +341,7 @@ function module_disable($module_list) {
// Refresh the module list to exclude the disabled modules.
module_list(TRUE, FALSE);
// Force to regenerate the stored list of hook implementations.
module_implements('', FALSE, TRUE);
drupal_rebuild_code_registry();
}
// If there remains no more node_access module, rebuilding will be
......@@ -376,7 +384,13 @@ function module_disable($module_list) {
* implemented in that module.
*/
function module_hook($module, $hook) {
return function_exists($module . '_' . $hook);
$function = $module . '_' . $hook;
if (defined('MAINTENANCE_MODE')) {
return function_exists($function);
}
else {
return drupal_function_exists($function);
}
}
/**
......@@ -395,22 +409,26 @@ function module_hook($module, $hook) {
* An array with the names of the modules which are implementing this hook.
*/
function module_implements($hook, $sort = FALSE, $refresh = FALSE) {
static $implementations;
static $implementations = array();
if ($refresh) {
$implementations = array();
return;
}
else if (!defined('MAINTENANCE_MODE') && empty($implementations)) {
if ($cache = cache_get('hooks', 'cache_registry')) {
$implementations = $cache->data;
}
}
if (!isset($implementations[$hook])) {
$implementations[$hook] = array();
$list = module_list(FALSE, TRUE, $sort);
foreach ($list as $module) {
foreach (module_list() as $module) {
if (module_hook($module, $hook)) {
$implementations[$hook][] = $module;
}
}
}
registry_cache_hook_implementations(array('hook' => $hook, 'modules' => $implementations[$hook]));
// The explicit cast forces a copy to be made. This is needed because
// $implementations[$hook] is only a reference to an element of
......@@ -438,9 +456,8 @@ function module_invoke() {
$module = $args[0];
$hook = $args[1];
unset($args[0], $args[1]);
$function = $module . '_' . $hook;
if (module_hook($module, $hook)) {
return call_user_func_array($function, $args);
return call_user_func_array($module . '_' . $hook, $args);
}
}
/**
......@@ -461,12 +478,14 @@ function module_invoke_all() {
$return = array();
foreach (module_implements($hook) as $module) {
$function = $module . '_' . $hook;
$result = call_user_func_array($function, $args);
if (isset($result) && is_array($result)) {
$return = array_merge_recursive($return, $result);
}
else if (isset($result)) {
$return[] = $result;
if (drupal_function_exists($function)) {
$result = call_user_func_array($function, $args);
if (isset($result) && is_array($result)) {
$return = array_merge_recursive($return, $result);
}
else if (isset($result)) {
$return[] = $result;
}
}
}
......
......@@ -177,7 +177,9 @@ function _init_theme($theme, $base_theme = array(), $registry_callback = '_theme
}
}
$registry_callback($theme, $base_theme, $theme_engine);
if (drupal_function_exists($registry_callback)) {
$registry_callback($theme, $base_theme, $theme_engine);
}
}
/**
......@@ -628,7 +630,7 @@ function theme() {
// call_user_func_array.
$args = array(&$variables, $hook);
foreach ($info['preprocess functions'] as $preprocess_function) {
if (function_exists($preprocess_function)) {
if (drupal_function_exists($preprocess_function)) {
call_user_func_array($preprocess_function, $args);
}
}
......
......@@ -202,7 +202,7 @@ function xmlrpc_server_call($xmlrpc_server, $methodname, $args) {
}
}
if (!function_exists($method)) {
if (!drupal_function_exists($method)) {
return xmlrpc_error(-32601, t('Server error. Requested function @method does not exist.', array("@method" => $method)));
}
// Call the mapped function
......
......@@ -785,8 +785,8 @@ function install_tasks($profile, $task) {
// The end of the install process. Remember profile used.
if ($task == 'done') {
// Rebuild menu to get content type links registered by the profile,
// and possibly any other menu items created through the tasks.
// Rebuild menu and registry to get content type links registered by the
// profile, and possibly any other menu items created through the tasks.
menu_rebuild();
// Register actions declared by any modules.
......
......@@ -4,3 +4,6 @@ description = "Aggregates syndicated content (RSS, RDF, and Atom feeds)."
package = Core - optional
version = VERSION
core = 7.x
files[] = aggregator.module
files[] = aggregator.admin.inc
files[] = aggregator.pages.inc
......@@ -84,7 +84,6 @@ function aggregator_menu() {
'description' => "Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized.",
'page callback' => 'aggregator_admin_overview',
'access arguments' => array('administer news feeds'),
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/add/feed'] = array(
'title' => 'Add feed',
......@@ -93,7 +92,6 @@ function aggregator_menu() {
'access arguments' => array('administer news feeds'),
'type' => MENU_LOCAL_TASK,
'parent' => 'admin/content/aggregator',
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/add/category'] = array(
'title' => 'Add category',
......@@ -102,7 +100,6 @@ function aggregator_menu() {
'access arguments' => array('administer news feeds'),
'type' => MENU_LOCAL_TASK,
'parent' => 'admin/content/aggregator',
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/remove/%aggregator_feed'] = array(
'title' => 'Remove items',
......@@ -110,7 +107,6 @@ function aggregator_menu() {
'page arguments' => array('aggregator_admin_remove_feed', 4),
'access arguments' => array('administer news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/update/%aggregator_feed'] = array(
'title' => 'Update items',
......@@ -118,7 +114,6 @@ function aggregator_menu() {
'page arguments' => array(4),
'access arguments' => array('administer news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.admin.inc',
);
$items['admin/content/aggregator/list'] = array(
'title' => 'List',
......@@ -132,40 +127,34 @@ function aggregator_menu() {
'type' => MENU_LOCAL_TASK,
'weight' => 10,
'access arguments' => array('administer news feeds'),
'file' => 'aggregator.admin.inc',
);
$items['aggregator'] = array(
'title' => 'Feed aggregator',
'page callback' => 'aggregator_page_last',
'access arguments' => array('access news feeds'),
'weight' => 5,
'file' => 'aggregator.pages.inc',
);
$items['aggregator/sources'] = array(
'title' => 'Sources',
'page callback' => 'aggregator_page_sources',
'access arguments' => array('access news feeds'),
'file' => 'aggregator.pages.inc',
);
$items['aggregator/categories'] = array(
'title' => 'Categories',
'page callback' => 'aggregator_page_categories',
'access callback' => '_aggregator_has_categories',
'file' => 'aggregator.pages.inc',
);
$items['aggregator/rss'] = array(
'title' => 'RSS feed',
'page callback' => 'aggregator_page_rss',
'access arguments' => array('access news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.pages.inc',
);
$items['aggregator/opml'] = array(
'title' => 'OPML feed',
'page callback' => 'aggregator_page_opml',
'access arguments' => array('access news feeds'),
'type' => MENU_CALLBACK,
'file' => 'aggregator.pages.inc',
);
$items['aggregator/categories/%aggregator_category'] = array(
'title callback' => '_aggregator_category_title',
......@@ -174,7 +163,6 @@ function aggregator_menu() {
'page arguments' => array(2),
'access callback' => 'user_access',
'access arguments' => array('access news feeds'),
'file' => 'aggregator.pages.inc',
);
$items['aggregator/categories/%aggregator_category/view'] = array(
'title' => 'View',
......@@ -187,7 +175,6 @@ function aggregator_menu() {
'page arguments' => array('aggregator_page_category', 2),
'access arguments' => array('administer news feeds'),
'type' => MENU_LOCAL_TASK,
'file' => 'aggregator.pages.inc',
);
$items['aggregator/categories/%aggregator_category/configure'] = array(
'title' => 'Configure',
......@@ -196,14 +183,12 @@ function aggregator_menu() {
'access arguments' => array('administer news feeds'),
'type' => MENU_LOCAL_TASK,
'weight' => 1,
'file' => 'aggregator.admin.inc',
);
$items['aggregator/sources/%aggregator_feed'] = array(
'page callback' => 'aggregator_page_source',
'page arguments' => array(2),
'access arguments' => array('access news feeds'),
'type' => MENU_CALLBACK,