PageRenderTime 101ms CodeModel.GetById 39ms RepoModel.GetById 0ms app.codeStats 0ms

/src/moduleutils/PhutilBootloader.php

http://github.com/facebook/libphutil
PHP | 291 lines | 208 code | 51 blank | 32 comment | 28 complexity | 3e823f5e7c85e381815dd11e50ab74ac MD5 | raw file
Possible License(s): Apache-2.0
  1. <?php
  2. /**
  3. * IMPORTANT: Do not call any libphutil functions in this class, including
  4. * functions like @{function:id}, @{function:idx} and @{function:pht}. They
  5. * may not have loaded yet.
  6. */
  7. final class PhutilBootloader {
  8. private static $instance;
  9. private $registeredLibraries = array();
  10. private $libraryMaps = array();
  11. private $extensionMaps = array();
  12. private $extendedMaps = array();
  13. private $currentLibrary = null;
  14. private $classTree = array();
  15. private $inMemoryMaps = array();
  16. public static function getInstance() {
  17. if (!self::$instance) {
  18. self::$instance = new PhutilBootloader();
  19. }
  20. return self::$instance;
  21. }
  22. private function __construct() {
  23. // This method intentionally left blank.
  24. }
  25. public function getClassTree() {
  26. return $this->classTree;
  27. }
  28. public function registerInMemoryLibrary($name, $map) {
  29. $this->registeredLibraries[$name] = "memory:$name";
  30. $this->inMemoryMaps[$name] = $map;
  31. $this->getLibraryMap($name);
  32. }
  33. public function registerLibrary($name, $path) {
  34. if (basename($path) != '__phutil_library_init__.php') {
  35. throw new PhutilBootloaderException(
  36. 'Only directories with a __phutil_library_init__.php file may be '.
  37. 'registered as libphutil libraries.');
  38. }
  39. $path = dirname($path);
  40. // Detect attempts to load the same library multiple times from different
  41. // locations. This might mean you're doing something silly like trying to
  42. // include two different versions of something, or it might mean you're
  43. // doing something subtle like running a different version of 'arc' on a
  44. // working copy of Arcanist.
  45. if (isset($this->registeredLibraries[$name])) {
  46. $old_path = $this->registeredLibraries[$name];
  47. if ($old_path != $path) {
  48. throw new PhutilLibraryConflictException($name, $old_path, $path);
  49. }
  50. }
  51. $this->registeredLibraries[$name] = $path;
  52. // For libphutil v2 libraries, load all functions when we load the library.
  53. if (!class_exists('PhutilSymbolLoader', false)) {
  54. $root = $this->getLibraryRoot('phutil');
  55. $this->executeInclude($root.'/symbols/PhutilSymbolLoader.php');
  56. }
  57. $loader = new PhutilSymbolLoader();
  58. $loader
  59. ->setLibrary($name)
  60. ->setType('function');
  61. try {
  62. $loader->selectAndLoadSymbols();
  63. } catch (PhutilBootloaderException $ex) {
  64. // Ignore this, it happens if a global function's file is removed or
  65. // similar. Worst case is that we fatal when calling the function, which
  66. // is no worse than fataling here.
  67. } catch (PhutilMissingSymbolException $ex) {
  68. // Ignore this, it happens if a global function is removed. Everything
  69. // else loaded so proceed forward: worst case is a fatal when we
  70. // hit a function call to a function which no longer exists, which is
  71. // no worse than fataling here.
  72. }
  73. if (empty($_SERVER['PHUTIL_DISABLE_RUNTIME_EXTENSIONS'])) {
  74. $extdir = $path.DIRECTORY_SEPARATOR.'extensions';
  75. if (Filesystem::pathExists($extdir)) {
  76. $extensions = id(new FileFinder($extdir))
  77. ->withSuffix('php')
  78. ->withType('f')
  79. ->withFollowSymlinks(true)
  80. ->setForceMode('php')
  81. ->find();
  82. foreach ($extensions as $extension) {
  83. $this->loadExtension(
  84. $name,
  85. $path,
  86. $extdir.DIRECTORY_SEPARATOR.$extension);
  87. }
  88. }
  89. }
  90. return $this;
  91. }
  92. public function registerLibraryMap(array $map) {
  93. $this->libraryMaps[$this->currentLibrary] = $map;
  94. return $this;
  95. }
  96. public function getLibraryMap($name) {
  97. if (isset($this->extendedMaps[$name])) {
  98. return $this->extendedMaps[$name];
  99. }
  100. if (empty($this->libraryMaps[$name])) {
  101. $root = $this->getLibraryRoot($name);
  102. $this->currentLibrary = $name;
  103. if (isset($this->inMemoryMaps[$name])) {
  104. $this->libraryMaps[$name] = $this->inMemoryMaps[$name];
  105. } else {
  106. $okay = include $root.'/__phutil_library_map__.php';
  107. if (!$okay) {
  108. throw new PhutilBootloaderException(
  109. "Include of '{$root}/__phutil_library_map__.php' failed!");
  110. }
  111. }
  112. $map = $this->libraryMaps[$name];
  113. $version = isset($map['__library_version__'])
  114. ? $map['__library_version__']
  115. : 1;
  116. switch ($version) {
  117. case 1:
  118. throw new Exception(
  119. 'libphutil v1 libraries are no longer supported.');
  120. case 2:
  121. // NOTE: In version 2 of the library format, all parents (both
  122. // classes and interfaces) are stored in the 'xmap'. The value is
  123. // either a string for a single parent (the common case) or an array
  124. // for multiple parents.
  125. foreach ($map['xmap'] as $child => $parents) {
  126. foreach ((array)$parents as $parent) {
  127. $this->classTree[$parent][] = $child;
  128. }
  129. }
  130. break;
  131. default:
  132. throw new Exception("Unsupported library version '{$version}'!");
  133. }
  134. }
  135. $map = $this->libraryMaps[$name];
  136. // If there's an extension map for this library, merge the maps.
  137. if (isset($this->extensionMaps[$name])) {
  138. $emap = $this->extensionMaps[$name];
  139. foreach (array('function', 'class', 'xmap') as $dict_key) {
  140. if (!isset($emap[$dict_key])) {
  141. continue;
  142. }
  143. $map[$dict_key] += $emap[$dict_key];
  144. }
  145. }
  146. $this->extendedMaps[$name] = $map;
  147. return $map;
  148. }
  149. public function getLibraryMapWithoutExtensions($name) {
  150. // This just does all the checks to make sure the library is valid, then
  151. // we throw away the result.
  152. $this->getLibraryMap($name);
  153. return $this->libraryMaps[$name];
  154. }
  155. public function getLibraryRoot($name) {
  156. if (empty($this->registeredLibraries[$name])) {
  157. throw new PhutilBootloaderException(
  158. "The phutil library '{$name}' has not been loaded!");
  159. }
  160. return $this->registeredLibraries[$name];
  161. }
  162. public function getAllLibraries() {
  163. return array_keys($this->registeredLibraries);
  164. }
  165. public function loadLibrary($path) {
  166. $root = null;
  167. if (!empty($_SERVER['PHUTIL_LIBRARY_ROOT'])) {
  168. if ($path[0] != '/') {
  169. $root = $_SERVER['PHUTIL_LIBRARY_ROOT'];
  170. }
  171. }
  172. $okay = $this->executeInclude($root.$path.'/__phutil_library_init__.php');
  173. if (!$okay) {
  174. throw new PhutilBootloaderException(
  175. "Include of '{$path}/__phutil_library_init__.php' failed!");
  176. }
  177. }
  178. public function loadLibrarySource($library, $source) {
  179. $path = $this->getLibraryRoot($library).'/'.$source;
  180. $okay = $this->executeInclude($path);
  181. if (!$okay) {
  182. throw new PhutilBootloaderException("Include of '{$path}' failed!");
  183. }
  184. }
  185. private function executeInclude($path) {
  186. // Suppress warning spew if the file does not exist; we'll throw an
  187. // exception instead. We still emit error text in the case of syntax errors.
  188. $old = error_reporting(E_ALL & ~E_WARNING);
  189. $okay = include_once $path;
  190. error_reporting($old);
  191. return $okay;
  192. }
  193. private function loadExtension($library, $root, $path) {
  194. $old_functions = get_defined_functions();
  195. $old_functions = array_fill_keys($old_functions['user'], true);
  196. $old_classes = array_fill_keys(get_declared_classes(), true);
  197. $old_interfaces = array_fill_keys(get_declared_interfaces(), true);
  198. $ok = $this->executeInclude($path);
  199. if (!$ok) {
  200. throw new PhutilBootloaderException(
  201. "Include of extension file '{$path}' failed!");
  202. }
  203. $new_functions = get_defined_functions();
  204. $new_functions = array_fill_keys($new_functions['user'], true);
  205. $new_classes = array_fill_keys(get_declared_classes(), true);
  206. $new_interfaces = array_fill_keys(get_declared_interfaces(), true);
  207. $add_functions = array_diff_key($new_functions, $old_functions);
  208. $add_classes = array_diff_key($new_classes, $old_classes);
  209. $add_interfaces = array_diff_key($new_interfaces, $old_interfaces);
  210. // NOTE: We can't trust the path we loaded to be the location of these
  211. // symbols, because it might have loaded other paths.
  212. foreach ($add_functions as $func => $ignored) {
  213. $rfunc = new ReflectionFunction($func);
  214. $fpath = Filesystem::resolvePath($rfunc->getFileName(), $root);
  215. $this->extensionMaps[$library]['function'][$func] = $fpath;
  216. }
  217. foreach ($add_classes + $add_interfaces as $class => $ignored) {
  218. $rclass = new ReflectionClass($class);
  219. $cpath = Filesystem::resolvePath($rclass->getFileName(), $root);
  220. $this->extensionMaps[$library]['class'][$class] = $cpath;
  221. $xmap = $rclass->getInterfaceNames();
  222. $parent = $rclass->getParentClass();
  223. if ($parent) {
  224. $xmap[] = $parent->getName();
  225. }
  226. if ($xmap) {
  227. foreach ($xmap as $parent_class) {
  228. $this->classTree[$parent_class][] = $class;
  229. }
  230. if (count($xmap) == 1) {
  231. $xmap = head($xmap);
  232. }
  233. $this->extensionMaps[$library]['xmap'][$class] = $xmap;
  234. }
  235. }
  236. // Clear the extended library cache (should one exist) so we know that
  237. // we need to rebuild it.
  238. unset($this->extendedMaps[$library]);
  239. }
  240. }