Graph.php 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. <?php
  2. namespace OSInet\Class_Grapher;
  3. /**
  4. * Graph builder.
  5. */
  6. class Graph {
  7. /**
  8. * A PEAR class.
  9. *
  10. * @var Image_GraphViz
  11. */
  12. public $g;
  13. /**
  14. * @var Logger
  15. */
  16. public $logger = NULL;
  17. var $runtimeClasses = array();
  18. var $runtimeInterfaces = array();
  19. var $classes = array();
  20. var $interfaces = array();
  21. var $registeredClasses = array();
  22. var $registeredInterfaces = array();
  23. /**
  24. * Represent attributes as a string in GraphViz format.
  25. *
  26. * @param array $attributes
  27. */
  28. public function attributes(array $attributes) {
  29. $attributes = $this->g->_escapeArray($attributes);
  30. $ret = array();
  31. foreach ($attributes as $name => $value) {
  32. $ret[] = "$name=$value";
  33. }
  34. $ret = implode(',', $ret);
  35. return $ret;
  36. }
  37. /**
  38. * Shortcut to invoke logger->message().
  39. *
  40. * @param string $message
  41. * @param int $level
  42. */
  43. public function debug($message = '\n', $level = LOG_DEBUG) {
  44. $this->logger->debug($message, $level);
  45. }
  46. /**
  47. * Build a list of files to scan below a given base.
  48. *
  49. * TODO support base as a file instead of a directory.
  50. *
  51. * @param string $base
  52. * A starting path for file lookup.
  53. * @param array $blackList
  54. * An array of file names to reject, whatever their path.
  55. * @param string $pattern
  56. * A regexp for file paths to match.
  57. *
  58. * @return array
  59. * An array of compliant file paths.
  60. */
  61. public function getFiles($base, $blackList = array(),
  62. $pattern = '/.*\.(inc|module|php)$/') {
  63. $dir = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($base,
  64. \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
  65. $files = array();
  66. while ($dir->valid()) {
  67. $name = $dir->key();
  68. if (preg_match($pattern, $name) && !in_array(basename($name), $blackList)) {
  69. $files[] = $name;
  70. }
  71. elseif ($this->logger->debugLevel >= LOG_INFO) {
  72. if (in_array(basename($name), $blackList)) {
  73. $this->debug("$name blacklisted.\n", LOG_INFO);
  74. }
  75. else {
  76. $this->debug("$name does not match source paths.\n", LOG_DEBUG);
  77. }
  78. }
  79. $dir->next();
  80. }
  81. $files = array_unique($files);
  82. sort($files);
  83. return $files;
  84. }
  85. /**
  86. * Constructor helper: initialize GraphViz settings.
  87. */
  88. public function initGraph() {
  89. $this->g = new \Image_Graphviz();
  90. $this->g->setDirected(TRUE);
  91. $this->g->setAttributes(array(
  92. 'rankdir' => 'LR',
  93. 'overlap' => FALSE,
  94. ));
  95. }
  96. public function registerClass(ClassInstance $class) {
  97. $this->debug(' Registering class '. $class->name . "\n");
  98. if (!empty($class->parent)) {
  99. if (!isset($this->registeredClasses[$class->parent])) {
  100. $this->debug(" Registering parent ". $class->parent . " for " . $class->name . "\n");
  101. $gpParent = new \PGPClass($class->parent);
  102. $cpParent = new ClassInstance($gpParent, 1, $this->logger);
  103. $this->registerClass($cpParent);
  104. }
  105. }
  106. foreach ($class->interfaces as $interface) {
  107. if (!isset($this->registeredClasses[$interface])) {
  108. $this->debug(" Registering interface $interface for {$class->name}\n");
  109. $gpInterface = new \PGPClass($interface);
  110. $cpInterface = new InterfaceInstance($gpInterface, 1, $this->logger);
  111. $this->registerInterface($cpInterface);
  112. }
  113. }
  114. // At this point, all ascendants are registered.
  115. if (!isset($this->registeredClasses[$class->name])) {
  116. $this->registeredClasses[$class->name] = $class;
  117. }
  118. // Override implicit status of a previously declared class if needed.
  119. if (!$class->implicit) {
  120. $this->registeredClasses[$class->name]->implicit = 0;
  121. }
  122. }
  123. public function registerInterface(InterfaceInstance $interface) {
  124. $this->debug("Registering interface {$interface->name}\n");
  125. if (!empty($interface->parent)) {
  126. if (!isset($this->registeredInterfaces[$interface->parent])) {
  127. $this->debug(" Registering parent ". $interface->parent . " for " . $interface->name . "\n");
  128. $gpParent = new \PGPClass($interface->parent);
  129. $cpParent = new InterfaceInstance($gpParent, 1, $this->logger);
  130. $this->registerInterface($cpParent);
  131. }
  132. }
  133. // At this point, all ascendants are registered.
  134. if (!isset($this->registeredInterfaces[$interface->name])) {
  135. $this->registeredInterfaces[$interface->name] = $interface;
  136. }
  137. // Override implicit status of a previously declared interface if needed.
  138. if (!$interface->implicit) {
  139. $this->registeredInterfaces[$interface->name]->implicit = 0;
  140. }
  141. }
  142. /**
  143. * Symbol extractor using grammar_parser.
  144. *
  145. * @param string $base
  146. */
  147. public function extractGrammar($base) {
  148. // These files cause a segfault during parse, so we skip them for now.
  149. // Possible patch at http://drupal.org/node/1485370
  150. $blackList = array(
  151. 'RouteCollection.php',
  152. 'HeaderBag.php',
  153. 'NativeSessionHandler.php',
  154. 'SimpleXMLElement.php',
  155. 'ContainerBuilderTest.php',
  156. 'YamlFileLoaderTest.php',
  157. 'ResponseTest.php',
  158. 'ConfigDataCollectorTest.php',
  159. 'KernelTest.php',
  160. 'UrlMatcherTest.php',
  161. );
  162. $files = $this->getFiles($base, $blackList);
  163. foreach ($files as $filename) {
  164. $source = file_get_contents($filename, FALSE);
  165. $reader = new \PGPReader($source);
  166. unset($source);
  167. $this->debug("Parsing $filename\n", LOG_INFO);
  168. $reader->buildGrammar();
  169. foreach ($reader->getInterfaces() as $interface) {
  170. $this->registerInterface(new InterfaceInstance($interface->data, 0, $this->logger));
  171. }
  172. foreach ($reader->getClasses() as $class) {
  173. $this->registerClass(new ClassInstance($class->data, 0, $this->logger));
  174. }
  175. $reader->reset();
  176. unset($reader);
  177. }
  178. }
  179. public function __construct($base, Logger $logger) {
  180. $this->logger = $logger;
  181. $this->extractGrammar($base);
  182. $this->initGraph();
  183. }
  184. public function implementsAttributes() {
  185. $attributes = array(
  186. 'arrowhead' => 'empty',
  187. 'arrowtail' => 'none',
  188. 'style' => 'dotted',
  189. );
  190. $attributes = $this->g->_escapeArray($attributes);
  191. return $attributes;
  192. }
  193. public function buildSymbols($collection = array(), $kind = NULL) {
  194. foreach ($collection as $name => $symbol) {
  195. $this->debug("Building $kind $name\n");
  196. $this->g->addNode($name, $symbol->attributes());
  197. if ($symbol->parent) {
  198. $this->g->addEdge(array($name => $symbol->parent));
  199. }
  200. if (empty($symbol->interfaces)) {
  201. continue;
  202. }
  203. foreach ($symbol->interfaces as $interface_name) {
  204. $this->g->addEdge(array($name => $interface_name), $this->implementsAttributes());
  205. }
  206. }
  207. }
  208. /**
  209. * Build the graph once extraction has been performed.
  210. */
  211. public function build($imager = 'dot', $format = 'svg') {
  212. $this->debug("Starting build for "
  213. . count($this->registeredInterfaces) . " interfaces and "
  214. . count($this->registeredClasses) . " classes.\n",
  215. WATCHDOG_INFO);
  216. $this->buildSymbols($this->registeredInterfaces, 'interface');
  217. $this->buildSymbols($this->registeredClasses, 'class');
  218. if (empty($imager) || $imager == 'dump') {
  219. // $this->debug(print_r($g->_getGroups(), TRUE));
  220. $this->debug("Interfaces:\n");
  221. $this->debug(print_r($this->registeredInterfaces, TRUE));
  222. $this->debug("Classes\n");
  223. $this->debug(print_r($this->registeredClasses, TRUE));
  224. //$this->debug(print_r($this->g));
  225. $ret = $this->g->parse();
  226. }
  227. else {
  228. ob_start();
  229. $this->g->image($format, $imager);
  230. $ret = ob_get_contents();
  231. ob_end_clean();
  232. }
  233. return $ret;
  234. }
  235. }