Registry and testable refactoring

  1. Before:
  2. +function drupal_rebuild_code_registry($check = FALSE) {
  3. +  static $running;
  4. +  if ($check) {
  5. +    return $running;
  6. +  }
  7. +  $running = TRUE;
  8. +  // Flush the old registry.
  9. +  db_query("DELETE FROM {registry}");
  10. +  // We can't use module_invoke_all here because it depends on the registry
  11. +  // which is currently being rebuilt.
  12. +  $list = module_list(TRUE, FALSE, FALSE);
  13. +  $patterns = array();
  14. +  foreach ($list as $module) {
  15. +    $function = $module .'_hooks';
  16. +    if (function_exists($function)) {
  17. +      $result = (array)$function();
  18. +      foreach ($result as $pattern) {
  19. +        // For example 'form__alter'.
  20. +        $patterns[] = '/'. str_replace('__', '_.*_', $pattern) .'/';
  21. +      }
  22. +    }
  23. +  }
  24. +
  25. +  foreach ($list as $module) {
  26. +    _registry_parse_directory(drupal_get_path('module', $module), $patterns);
  27. +  }
  28. +
  29. +  _registry_parse_directory('includes', $patterns);
  30. +  $implementations = _registry_save_resource();
  31. +  cache_set('hooks', array('patterns' => $patterns, 'implementations' => $implementations));
  32. +}
  33. +
  34. +/**
  35. + * Parse all loadable files in a directory and save their function listings.
  36. + */
  37. +function _registry_parse_directory($path, $patterns) {
  38. +  static $map = array(T_FUNCTION => 'function', T_CLASS => 'class', T_INTERFACE => 'interface');
  39. +  $files = file_scan_directory($path, '\.(inc|module|install)$');
  40. +  foreach ($files as $filename => $file) {
  41. +    $tokens = token_get_all(file_get_contents($filename));
  42. +    while ($token = next($tokens)) {
  43. +      if (is_array($token) && isset($map[$token[0]])) {
  44. +        $result = _registry_save_resource($token, $tokens, $map[$token[0]], $filename, $patterns);
  45. +        // If this is a disabled module then we skip the whole file.
  46. +        if ($result == -1) {
  47. +          continue 2;
  48. +        }
  49. +        // We skip the body because classes might contain functions.
  50. +        _registry_skip_body($tokens);
  51. +      }
  52. +    }
  53. +  }
  54. +}
  55. +
  56. +/**
  57. + * Save a resource into the database.
  58. + *
  59. + * @param mixed $token
  60. + * @param array $tokens
  61. + * @param string $type
  62. + * @param string $module_path
  63. + * @param string $filename
  64. + */
  65. +function _registry_save_resource($token = NULL, &$tokens = NULL, $type = NULL, $filename = NULL, $patterns = NULL) {
  66. +  static $implementations, $resources, $dirs, $node_functions;
  67. +  if (!isset($token)) {
  68. +    return $implementations;
  69. +  }
  70. +  if (empty($node_functions)) {
  71. +    $types = node_get_types();
  72. +    foreach ($types as $node_type) {
  73. +      $module = $node_type->module == 'node' ? 'node_content' : $node_type->module;
  74. +      foreach (array('load', 'validate', 'insert', 'update', 'delete', 'view', 'prepare', 'form') as $hook) {
  75. +        $node_functions[$module .'_'. $hook] = $module;
  76. +      }
  77. +    }
  78. +  }
  79. +  next($tokens); // Eat a space.
  80. +  $token = next($tokens);
  81. +  if ($token == '&') {
  82. +    $token = next($tokens);
  83. +  }
  84. +  $resource_name = $token[1];
  85. +  if (isset($resources[$type][$resource_name])) {
  86. +    return;
  87. +  }
  88. +  $resources[$type][$resource_name] = TRUE;
  89. +  $file_parts = explode('.', $filename);
  90. +  $module = '';
  91. +  $hook = '';
  92. +  $count = count($file_parts);
  93. +  if (isset($node_functions[$resource_name]) && $type == 'function') {
  94. +    $module = $node_functions[$resource_name];
  95. +  }
  96. +  else {
  97. +    if ($count == 2 && $file_parts[1] == 'inc') {
  98. +      if (!isset($dirs[$filename])) {
  99. +        $dir_parts = explode('/', $file_parts[0]);
  100. +        array_pop($dir_parts);
  101. +        $dirs[$filename] = array_pop($dir_parts);
  102. +      }
  103. +      $module = $dirs[$filename];
  104. +    }
  105. +    if (($count ==  2 && ($file_parts[1] == 'module' || $file_parts[1] == 'install')) || ($count == 3 && $file_parts[2] == 'inc')) {
  106. +      $module = basename($file_parts[0]);
  107. +      if ($module != 'includes' && !in_array($module, module_list())) {
  108. +        // We indicate that this file needs to be skipped.
  109. +        return -1;
  110. +      }
  111. +    }
  112. +  }
  113. +  if ($module && strpos($resource_name, $module) === 0) {
  114. +    $hook = substr($resource_name, strlen($module) + 1);
  115. +    foreach ($patterns as $pattern) {
  116. +      if (preg_match($pattern, $hook)) {
  117. +        $implementations[$hook][] = $module;
  118. +      }
  119. +    }
  120. +  }
  121. +  db_query("INSERT INTO {registry} (name, type, module, hook, file) VALUES ('%s', '%s', '%s', '%s', '%s')", array($resource_name, $type, $module, $hook, "./$filename"));
  122. +}
  123. +
  124. +/**
  125. + * Skip the body of a code block, as defined by { and }.
  126. + *
  127. + * This function assumes that the body starts at the next instance
  128. + * of { from the current position.
  129. + *
  130. + * @param array $tokens
  131. + */
  132. +function _registry_skip_body(&$tokens) {
  133. +  $num_braces = 1;
  134. +
  135. +  $token = '';
  136. +  // Get to the first open brace.
  137. +  while ($token != '{' && ($token = next($tokens)));
  138. +
  139. +  // Scan through the rest of the tokens until we reach the matching
  140. +  // end brace.
  141. +  while ($num_braces && ($token = next($tokens))) {
  142. +    if ($token == '{') {
  143. +      ++$num_braces;
  144. +    }
  145. +    elseif ($token == '}') {
  146. +      --$num_braces;
  147. +    }
  148. +  }
  149. +}
  150.  
  151.  
  152. After:
  153. +/**
  154. + * @defgroup registry Code registry
  155. + * @{
  156. + * The code registry engine.
  157. + *
  158. + * Drupal maintains an internal registry of all functions or classes in the
  159. + * system. That in turn allows Drupal to lazy-load code files selectively
  160. + * as needed, reducing the amount of code that needs to be parsed on each
  161. + * request.  The list of files included is then cached per menu callback
  162. + * so that they can be loaded by the menu router.  That way, a given page
  163. + * request will have all the code it needs and little else, minimizing the
  164. + * time wasted parsing unneeded code.
  165. + */
  166. +
  167. +/**
  168. + * Rescan all enabled modules and rebuild the registry.
  169. + *
  170. + * This function rescans all code in modules or the includes directory and
  171. + * stores a mapping of function, file, and hook implementation to the database.
  172. + *
  173. + * @param $check
  174. + *  If TRUE, return whether or not a rebuild is currently in progress. That is
  175. + *  needed so that this process can call module_implements(), which in turn
  176. + *  needs to bypass the registry if the registry is still in the process of
  177. + *  being rebuilt.
  178. + * @return
  179. + *  If $checked is TRUE, returns TRUE if the registry is in the process of
  180. + *  being rebuilt and FALSE otherwise.  If $checked is FALSE, this function
  181. + *  returns nothing.
  182. + */
  183. +function drupal_rebuild_code_registry($check = FALSE) {
  184. +
  185. +  // Simple recursion blocking. See DocBlock above.
  186. +  static $running;
  187. +  if ($check) {
  188. +    return $running;
  189. +  }
  190. +  $running = TRUE;
  191. +  // Flush the old registry.
  192. +  db_query("DELETE FROM {registry}");
  193. +  // We can't use module_invoke_all here because it depends on the registry
  194. +  // which is currently being rebuilt.
  195. +  $list = module_list(TRUE, FALSE, FALSE);
  196. +  $patterns = array();
  197. +  foreach ($list as $module) {
  198. +    $function = $module .'_hooks';
  199. +    if (function_exists($function)) {
  200. +      $result = (array)$function();
  201. +      foreach ($result as $pattern) {
  202. +        // For example 'form__alter'.
  203. +        $patterns[] = '/'. str_replace('__', '_.*_', $pattern) .'/';
  204. +      }
  205. +    }
  206. +  }
  207. +
  208. +  foreach ($list as $module) {
  209. +    _registry_parse_directory(drupal_get_path('module', $module), $patterns);
  210. +  }
  211. +
  212. +  _registry_parse_directory('includes', $patterns);
  213. +  $implementations = _registry_hook_implementations();
  214. +  cache_set('hooks', array('patterns' => $patterns, 'implementations' => $implementations));
  215. +  
  216. +  // Reset our recursion blocker.
  217. +  $running = FALSE;
  218. +}
  219. +
  220. +/**
  221. + * Parse all loadable files in a directory and save their function and class listings.
  222. + *
  223. + * @param $path
  224. + *  The path relative to Drupal root to scan.
  225. + * @param $patterns
  226. + *  The function pattern to identify as a hook.  That allows us to record
  227. + *  what hook implementations exist and in what module/file.
  228. + */
  229. +function _registry_parse_directory($path, $patterns) {
  230. +  static $map = array(T_FUNCTION => 'function', T_CLASS => 'class', T_INTERFACE => 'interface');
  231. +  
  232. +  $active_modules = module_list();
  233. +  
  234. +  $files = file_scan_directory($path, '\.(inc|module|install)$');
  235. +  foreach ($files as $filename => $file) {
  236. +    $tokens = token_get_all(file_get_contents($filename));
  237. +    while ($token = next($tokens)) {
  238. +      // Ignore all tokens except for those we are specifically saving.
  239. +      if (is_array($token) && isset($map[$token[0]])) {
  240. +        if ($resource_name = _registry_get_resource_name($tokens, $map[$token[0]]) ) {
  241. +          $module = _registry_get_resource_module($resource_name, $filename);
  242. +          if ($module != 'includes' && !in_array($module, $active_modules)) {
  243. +            // If this is a disabled module then we skip the whole file.
  244. +            continue 2;
  245. +          }
  246. +          
  247. +          // Now save the resource record to the database.
  248. +          $result = _registry_save_resource($resource_name, $map[$token[0]], $module, $hook, $filename);
  249. +          // We skip the body because classes might contain functions.
  250. +          _registry_skip_body($tokens);
  251. +        }
  252. +      }
  253. +    }
  254. +  }
  255. +}
  256. +
  257. +/**
  258. + * Derive the name of the next resource in the token stream.
  259. + *
  260. + * @param array $tokens
  261. + *  The collection of tokens for the current file being parsed.
  262. + * @param string $type
  263. + *  The human-readable token name: One of "function", "class", or "interface".
  264. + * @return
  265. + *  The name of the resource, or FALSE if the resource has already been processed.
  266. + */
  267. +function _registry_get_resource_name(&$tokens, $type) {
  268. +  // Keep a running list of all resources we've saved so far, so that we never
  269. +  // save one more than once.
  270. +  static $resources;
  271. +
  272. +  // Determine the name of the resource.
  273. +  next($tokens); // Eat a space.
  274. +  $token = next($tokens);
  275. +  if ($token == '&') {
  276. +    $token = next($tokens);
  277. +  }
  278. +  $resource_name = $token[1];
  279. +  
  280. +  // Ensure that we never save it more than once.
  281. +  if (isset($resources[$type][$resource_name])) {
  282. +    return FALSE;
  283. +  }
  284. +  $resources[$type][$resource_name] = TRUE;
  285. +  
  286. +  return $resource_name;
  287. +}
  288. +
  289. +/**
  290. + * Determine the module that the given resource beongs to.
  291. + *
  292. + * In the case of node "hooks", the module is determined by calculating all
  293. + * possible node hooks and the module they correspond to.  Otherwise, the module
  294. + * is derived from the file name or directory name of the file.  
  295. + *
  296. + * Detectable files follow one of the following patterns:
  297. + *  - <module>.module
  298. + *  - <module>.install
  299. + *  - <module>.<some-arbitrary-string>.inc
  300. + *  - <module>/<some-arbitrary-string>.inc
  301. + *
  302. + * Note that the last option will treat any code in Drupal's core "includes"
  303. + * directory as belonging to the module "includes".  That is by design.
  304. + *
  305. + * In order for a module to provide a hook on behalf of another module, the
  306. + * name of the file the implementation exists in must match the module the hook
  307. + * applies to, not the providing module.  That is, if module foo is providing
  308. + * the implementation of hook_example() on behalf of module bar, then the function
  309. + * must reside in foo/bar.something.inc for it to associate with module bar
  310. + * correctly.
  311. + *
  312. + * @param string $resource_name
  313. + *  The name of the resource; the function or class name.
  314. + * @param string $filename
  315. + *  The name of the file in which the resource resides, relative to Drupal root.
  316. + * @return
  317. + *  The name of the module that "owns" the resource.
  318. + */
  319. +function _registry_get_resource_module($resource_name, $filename) {
  320. +  static $dirs, $node_functions;
  321. +  
  322. +  // Node "hooks" aren't "real hooks", but still get called indirectly.  Therefore,
  323. +  // we build up a list of all possible node hooks for the current node types
  324. +  // that we can match against later.
  325. +  if (empty($node_functions)) {
  326. +    $types = node_get_types();
  327. +    foreach ($types as $node_type) {
  328. +      $module = $node_type->module == 'node' ? 'node_content' : $node_type->module;
  329. +      foreach (array('load', 'validate', 'insert', 'update', 'delete', 'view', 'prepare', 'form') as $hook) {
  330. +        $node_functions[$module .'_'. $hook] = $module;
  331. +      }
  332. +    }
  333. +  }
  334. +  
  335. +  // Extract the module from the file name or directory name.
  336. +  $file_parts = explode('.', $filename);
  337. +  $module = '';
  338. +  $hook = '';
  339. +  $count = count($file_parts);
  340. +  if (isset($node_functions[$resource_name]) && $type == 'function') {
  341. +    $module = $node_functions[$resource_name];
  342. +  }
  343. +  else {
  344. +    if ($count == 2 && $file_parts[1] == 'inc') {
  345. +      if (!isset($dirs[$filename])) {
  346. +        $dir_parts = explode('/', $file_parts[0]);
  347. +        array_pop($dir_parts);
  348. +        $dirs[$filename] = array_pop($dir_parts);
  349. +      }
  350. +      $module = $dirs[$filename];
  351. +    }
  352. +    if (($count ==  2 && ($file_parts[1] == 'module' || $file_parts[1] == 'install')) || ($count == 3 && $file_parts[2] == 'inc')) {
  353. +      $module = basename($file_parts[0]);
  354. +    }
  355. +  }
  356. +  
  357. +  return $module;
  358. +}
  359. +