registry_autoload.module 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <?php
  2. /**
  3. * @file
  4. * Main module for enabling core registry to support namespaced files.
  5. */
  6. // Core hooks
  7. // --------------------------------------------------------------------------
  8. /**
  9. * Implements hook_boot().
  10. */
  11. function registry_autoload_boot() {
  12. // Empty implementation so registry_rebuild can rebuild the registry in the
  13. // bootstrap modules phase.
  14. }
  15. /**
  16. * Implements hook_module_implements_alter().
  17. */
  18. function registry_autoload_module_implements_alter(&$implementations, $hook) {
  19. // Move our hook implementation to the bottom, so we are called last.
  20. if ($hook == 'registry_files_alter') {
  21. $group = $implementations['registry_autoload'];
  22. unset($implementations['registry_autoload']);
  23. $implementations['registry_autoload'] = $group;
  24. }
  25. }
  26. /**
  27. * Implements hook_registry_files_alter().
  28. */
  29. function registry_autoload_registry_files_alter(&$files, $indexed_modules) {
  30. $parsed_files = registry_get_parsed_files();
  31. $autoload_searcher = new RegistryAutoloadSearcher($parsed_files);
  32. $registry = array();
  33. // Search for modules that specify registry_autoload in info.
  34. foreach ($indexed_modules as $module) {
  35. if (empty($module->info['registry_autoload'])) {
  36. $module->info['registry_autoload'] = variable_get('registry_autoload_global_formats', array());
  37. }
  38. if (empty($module->info['registry_autoload']) && empty($module->info['registry_autoload_files'])) {
  39. continue;
  40. }
  41. // Ensure that properties exist.
  42. $module->info += array(
  43. 'registry_autoload' => array(),
  44. 'registry_autoload_files' => array(),
  45. );
  46. $class_files = array();
  47. // Detect class files based on namespaces.
  48. foreach ($module->info['registry_autoload'] as $search_path => $format) {
  49. $path = $module->dir;
  50. if (!is_numeric($search_path)) {
  51. // If there is an arbitrary path, use an absolute format path.
  52. $format = $format . '/absolute';
  53. // Support DRUPAL_ROOT as the single allowed constant.
  54. if (strpos($search_path, 'DRUPAL_ROOT/') === 0) {
  55. // Because all paths are relative, this works.
  56. $path = str_replace('DRUPAL_ROOT/', '', $search_path);
  57. }
  58. else {
  59. $path .= '/' . $search_path;
  60. }
  61. }
  62. $class_files = array_merge($class_files, $autoload_searcher->findClassFiles($path, $format));
  63. }
  64. // Merge in user provided class files.
  65. foreach ($module->info['registry_autoload_files'] as $class_file) {
  66. $class_files[] = $module->dir . '/' . $class_file;
  67. }
  68. $registry = array_merge($registry, $autoload_searcher->processFiles($class_files, $module));
  69. }
  70. // Give other modules a chance to alter the registry before we save it.
  71. drupal_alter('registry_autoload_registry', $registry);
  72. foreach ($registry as $entry) {
  73. // Add the files to the registry.
  74. // Note: Because the hash is the same it won't try to reparse it.
  75. $files[$entry->filename] = array(
  76. 'module' => $entry->module,
  77. 'weight' => $entry->weight,
  78. );
  79. if (empty($entry->needs_update)) {
  80. continue;
  81. }
  82. // And update the registry_file table.
  83. db_merge('registry_file')
  84. ->key(array('filename' => $entry->filename))
  85. ->fields(array(
  86. 'hash' => $entry->hash,
  87. ))
  88. ->execute();
  89. $names = array();
  90. foreach ($entry->classes as $class_name => $info) {
  91. $names[] = $class_name;
  92. db_merge('registry')->key(array(
  93. 'name' => $class_name,
  94. 'type' => $info['type'],
  95. ))->fields(array(
  96. 'filename' => $entry->filename,
  97. 'module' => $entry->module,
  98. 'weight' => $entry->weight,
  99. ))->execute();
  100. }
  101. if (!empty($names)) {
  102. // Now delete all classes for filename no longer existing.
  103. db_delete('registry')
  104. ->condition('filename', $entry->filename)
  105. ->condition('name', $names, 'NOT IN')
  106. ->execute();
  107. }
  108. }
  109. }
  110. // Helper classes
  111. // --------------------------------------------------------------------------
  112. /**
  113. * RegistryAutoloadSearcher helper class.
  114. *
  115. * Note: This class is not registered via the registry so it is available
  116. * while the registry is being rebuild.
  117. */
  118. class RegistryAutoloadSearcher {
  119. // @todo Make configurable.
  120. /**
  121. * The namespace $map maps the namespaces provided to subdirectories.
  122. *
  123. * @var array
  124. */
  125. protected $formatMap = array(
  126. 'PSR-0' => 'lib',
  127. 'PSR-4' => 'src',
  128. 'PHPUnit' => 'tests/src',
  129. 'PSR-0/absolute' => '',
  130. 'PSR-4/absolute' => '',
  131. 'PHPUnit/absolute' => '',
  132. );
  133. /**
  134. * The current content of the {registry_file} table.
  135. *
  136. * @see registry_get_parsed_files()
  137. *
  138. * @var array $parsedFiles
  139. */
  140. protected $parsedFiles = array();
  141. /**
  142. * Constructs a RegistryAutoloadSearcher object.
  143. *
  144. * @param array $parsed_files
  145. * The current content of the {registry_file} table.
  146. */
  147. public function __construct(array $parsed_files) {
  148. $this->parsedFiles = $parsed_files;
  149. }
  150. /**
  151. * Finds class files in the given module for the given format.
  152. *
  153. * @param string $path
  154. * The path to search class files in.
  155. * @param string $format
  156. * The format to scan for as specified in the $formatMap.
  157. *
  158. * @return array
  159. * Returns all found php files for the given module and format.
  160. */
  161. public function findClassFiles($path, $format) {
  162. if (!isset($this->formatMap[$format])) {
  163. return array();
  164. }
  165. $directory = $path;
  166. $subdir = $this->formatMap[$format];
  167. if (!empty($subdir)) {
  168. $directory .= '/' . $subdir;
  169. }
  170. $class_files = file_scan_directory($directory, '/.*.php$/');
  171. return array_keys($class_files);
  172. }
  173. /**
  174. * Processes files containing classes for the given module.
  175. *
  176. * @param array $class_files
  177. * The files to scan for classes.
  178. * @param object $module
  179. * The module object as given to hook_registry_files_alter().
  180. *
  181. * @return array
  182. * An associative array keyed by filename with object values.
  183. * The objects have the following properties:
  184. * - classes: An associative array keyed by namespace+name with properties:
  185. * - type: Type of the class, can be 'interface' or 'class'.
  186. * - name: Name of the class or interface.
  187. * This can be empty if needs_update below is FALSE.
  188. * - filename: The filename of the file.
  189. * - module: The module this file belongs to.
  190. * - weight: The weight of the module this file belongs to.
  191. * - hash: The file_hash() of the filename.
  192. * - needs_update: Whether the registry needs to be updated or not.
  193. */
  194. public function processFiles(array $class_files, $module) {
  195. $registry = array();
  196. foreach ($class_files as $filename) {
  197. // Check if file exists.
  198. if (!file_exists($filename)) {
  199. continue;
  200. }
  201. $hash = NULL;
  202. $needs_update = $this->checkFileNeedsUpdate($filename, $hash);
  203. $registry[$filename] = (object) array(
  204. 'classes' => array(),
  205. 'filename' => $filename,
  206. 'module' => $module->name,
  207. 'weight' => $module->weight,
  208. // We create a unique structure to populate both registry tables.
  209. 'hash' => $hash,
  210. 'needs_update' => $needs_update,
  211. );
  212. if ($needs_update) {
  213. $this->parseFile($registry[$filename], $filename);
  214. }
  215. }
  216. return $registry;
  217. }
  218. /**
  219. * Checks if the is in need of an update.
  220. *
  221. * @param string $filename
  222. * The filename to check against the {registry_file} table.
  223. * @param string $hash
  224. * The hash passed by reference for later usage.
  225. *
  226. * @return bool
  227. * TRUE if the file needs an update, FALSE otherwise.
  228. */
  229. protected function checkFileNeedsUpdate($filename, &$hash) {
  230. // Check if hash matches still.
  231. $stored_hash = !empty($this->parsedFiles[$filename]['hash']) ? $this->parsedFiles[$filename]['hash'] : NULL;
  232. $hash = hash_file('sha256', $filename);
  233. if (!empty($stored_hash) && $stored_hash == $hash) {
  234. return FALSE;
  235. }
  236. return TRUE;
  237. }
  238. /**
  239. * Parses a file for classes and interfaces, including the namespace.
  240. *
  241. * This lifts the core limitation of not supporting namespaces.
  242. *
  243. * This code is now based on
  244. * \Symfony\Component\ClassLoader\ClassMapGenerator::findClasses()
  245. *
  246. * @param object $entry
  247. * The registry entry as defined in processFiles(). The classes key will be
  248. * updated in the entry if classes can be found.
  249. * @param string $filename
  250. * The filename to parse.
  251. *
  252. * @return bool
  253. * TRUE if classes could be found, FALSE otherwise.
  254. *
  255. * @see _registry_parse_file()
  256. */
  257. protected function parseFile($entry, $filename) {
  258. $contents = file_get_contents($filename);
  259. $namespace = '';
  260. // Check if this version of PHP supports traits.
  261. $traits = version_compare(PHP_VERSION, '5.4', '<')
  262. ? ''
  263. : '|trait';
  264. // Return early if there is no chance of matching anything in this file.
  265. if (!preg_match('{\b(?:class|interface' . $traits . ')\s}i', $contents)) {
  266. return array();
  267. }
  268. $tokens = token_get_all($contents);
  269. for ($i = 0, $max = count($tokens); $i < $max; $i++) {
  270. $token = $tokens[$i];
  271. if (is_string($token)) {
  272. continue;
  273. }
  274. $class = '';
  275. $token_name = token_name($token[0]);
  276. switch ($token_name) {
  277. case "T_NAMESPACE":
  278. $namespace = '';
  279. // If there is a namespace, extract it
  280. while (($t = $tokens[++$i]) && is_array($t)) {
  281. if (in_array($t[0], array(T_STRING, T_NS_SEPARATOR))) {
  282. $namespace .= $t[1];
  283. }
  284. }
  285. $namespace .= '\\';
  286. break;
  287. case "T_CLASS":
  288. case "T_INTERFACE":
  289. case "T_TRAIT":
  290. // Find the classname
  291. while (($t = $tokens[++$i]) && is_array($t)) {
  292. if (T_STRING === $t[0]) {
  293. $class .= $t[1];
  294. }
  295. elseif ($class !== '' && T_WHITESPACE == $t[0]) {
  296. break;
  297. }
  298. }
  299. $class_name = ltrim($namespace.$class, '\\');
  300. $type = 'class';
  301. if ($token[0] == T_INTERFACE) {
  302. $type = 'interface';
  303. }
  304. $entry->classes[$class_name] = array(
  305. 'name' => $class_name,
  306. 'type' => $type,
  307. );
  308. break;
  309. default:
  310. break;
  311. }
  312. }
  313. return TRUE;
  314. }
  315. }