1
0

Graph.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. <?php
  2. namespace Osinet\ClassGrapher;
  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->legacyLog($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 or array of paths 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. $files = array();
  64. foreach ($base as $start) {
  65. $dir = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($start,
  66. \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS));
  67. while ($dir->valid()) {
  68. $name = $dir->key();
  69. if (preg_match($pattern, $name) && !in_array(basename($name), $blackList)) {
  70. $files[] = $name;
  71. }
  72. elseif ($this->logger->debugLevel >= LOG_INFO) {
  73. if (in_array(basename($name), $blackList)) {
  74. $this->debug("$name blacklisted.\n", LOG_INFO);
  75. }
  76. else {
  77. $this->debug("$name does not match source paths.\n", LOG_DEBUG);
  78. }
  79. }
  80. $dir->next();
  81. }
  82. }
  83. $files = array_unique($files);
  84. sort($files);
  85. return $files;
  86. }
  87. /**
  88. * Constructor helper: initialize GraphViz settings.
  89. */
  90. public function initGraph() {
  91. $this->g = new \Image_Graphviz();
  92. $this->g->setDirected(TRUE);
  93. $this->g->setAttributes(array(
  94. 'rankdir' => 'LR',
  95. 'overlap' => FALSE,
  96. ));
  97. }
  98. public function registerClass(ClassInstance $class) {
  99. $this->debug(' Registering class '. $class->name . "\n");
  100. if (!empty($class->parent)) {
  101. if (!isset($this->registeredClasses[$class->parent])) {
  102. $this->debug(" Registering parent ". $class->parent . " for " . $class->name . "\n");
  103. $gpParent = new \PGPClass($class->parent);
  104. $cpParent = new ClassInstance($gpParent, 1, $this->logger);
  105. $this->registerClass($cpParent);
  106. }
  107. }
  108. foreach ($class->interfaces as $interface) {
  109. if (!isset($this->registeredClasses[$interface])) {
  110. $this->debug(" Registering interface $interface for {$class->name}\n");
  111. $gpInterface = new \PGPClass($interface);
  112. $cpInterface = new InterfaceInstance($gpInterface, 1, $this->logger);
  113. $this->registerInterface($cpInterface);
  114. }
  115. }
  116. // At this point, all ascendants are registered.
  117. if (!isset($this->registeredClasses[$class->name])) {
  118. $this->registeredClasses[$class->name] = $class;
  119. }
  120. // Override implicit status of a previously declared class if needed.
  121. if (!$class->implicit) {
  122. $this->registeredClasses[$class->name]->implicit = 0;
  123. }
  124. }
  125. public function registerInterface(InterfaceInstance $interface) {
  126. $this->debug("Registering interface {$interface->name}\n");
  127. if (!empty($interface->parent)) {
  128. if (!isset($this->registeredInterfaces[$interface->parent])) {
  129. $this->debug(" Registering parent ". $interface->parent . " for " . $interface->name . "\n");
  130. $gpParent = new \PGPClass($interface->parent);
  131. $cpParent = new InterfaceInstance($gpParent, 1, $this->logger);
  132. $this->registerInterface($cpParent);
  133. }
  134. }
  135. // At this point, all ascendants are registered.
  136. if (!isset($this->registeredInterfaces[$interface->name])) {
  137. $this->registeredInterfaces[$interface->name] = $interface;
  138. }
  139. // Override implicit status of a previously declared interface if needed.
  140. if (!$interface->implicit) {
  141. $this->registeredInterfaces[$interface->name]->implicit = 0;
  142. }
  143. }
  144. /**
  145. * Symbol extractor using grammar_parser.
  146. *
  147. * @param string $base
  148. */
  149. public function extractGrammar($base) {
  150. // These files cause a segfault during parse, so we skip them for now.
  151. // Possible patch at http://drupal.org/node/1485370
  152. $blackList = array(
  153. 'RouteCollection.php',
  154. 'HeaderBag.php',
  155. 'NativeSessionHandler.php',
  156. 'SimpleXMLElement.php',
  157. 'ContainerBuilderTest.php',
  158. 'YamlFileLoaderTest.php',
  159. 'ResponseTest.php',
  160. 'ConfigDataCollectorTest.php',
  161. 'KernelTest.php',
  162. 'UrlMatcherTest.php',
  163. );
  164. $files = $this->getFiles($base, $blackList);
  165. foreach ($files as $filename) {
  166. $source = file_get_contents($filename, FALSE);
  167. $reader = new \PGPReader($source);
  168. unset($source);
  169. $this->debug("Parsing $filename\n", LOG_INFO);
  170. $reader->buildGrammar();
  171. foreach ($reader->getInterfaces() as $interface) {
  172. $this->registerInterface(new InterfaceInstance($interface->data, 0, $this->logger));
  173. }
  174. foreach ($reader->getClasses() as $class) {
  175. $this->registerClass(new ClassInstance($class->data, 0, $this->logger));
  176. }
  177. $reader->reset();
  178. unset($reader);
  179. }
  180. }
  181. public function __construct($base, Logger $logger) {
  182. $this->logger = $logger;
  183. $this->extractGrammar($base);
  184. $this->initGraph();
  185. }
  186. public function implementsAttributes() {
  187. $attributes = array(
  188. 'arrowhead' => 'empty',
  189. 'arrowtail' => 'none',
  190. 'style' => 'dotted',
  191. );
  192. $attributes = $this->g->_escapeArray($attributes);
  193. return $attributes;
  194. }
  195. public function buildSymbols($collection = array(), $kind = NULL) {
  196. foreach ($collection as $name => $symbol) {
  197. $this->debug("Building $kind $name\n");
  198. $this->g->addNode($name, $symbol->attributes());
  199. if ($symbol->parent) {
  200. $this->g->addEdge(array($name => $symbol->parent));
  201. }
  202. if (empty($symbol->interfaces)) {
  203. continue;
  204. }
  205. foreach ($symbol->interfaces as $interface_name) {
  206. $this->g->addEdge(array($name => $interface_name), $this->implementsAttributes());
  207. }
  208. }
  209. }
  210. /**
  211. * Build the graph once extraction has been performed.
  212. */
  213. public function build($imager = 'dot', $format = 'svg') {
  214. $this->debug("Starting build for "
  215. . count($this->registeredInterfaces) . " interfaces and "
  216. . count($this->registeredClasses) . " classes.\n",
  217. WATCHDOG_INFO);
  218. $this->buildSymbols($this->registeredInterfaces, 'interface');
  219. $this->buildSymbols($this->registeredClasses, 'class');
  220. if (empty($imager) || $imager == 'dump') {
  221. // $this->debug(print_r($g->_getGroups(), TRUE));
  222. $this->debug("Interfaces:\n");
  223. $this->debug(print_r($this->registeredInterfaces, TRUE));
  224. $this->debug("Classes\n");
  225. $this->debug(print_r($this->registeredClasses, TRUE));
  226. //$this->debug(print_r($this->g));
  227. $ret = $this->g->parse();
  228. }
  229. else {
  230. ob_start();
  231. $this->g->image($format, $imager);
  232. $ret = ob_get_contents();
  233. ob_end_clean();
  234. }
  235. return $ret;
  236. }
  237. }