g->_escapeArray($attributes); $ret = array(); foreach ($attributes as $name => $value) { $ret[] = "$name=$value"; } $ret = implode(',', $ret); return $ret; } /** * Shortcut to invoke logger->message(). * * @param string $message * @param int $level */ public function debug($message = '\n', $level = LOG_DEBUG) { $this->logger->debug($message, $level); } /** * Build a list of files to scan below a given base. * * TODO support base as a file instead of a directory. * * @param string $base * A starting path or array of paths for file lookup. * @param array $blackList * An array of file names to reject, whatever their path. * @param string $pattern * A regexp for file paths to match. * * @return array * An array of compliant file paths. */ public function getFiles($base, $blackList = array(), $pattern = '/.*\.(inc|module|php)$/') { $files = array(); foreach ($base as $start) { $dir = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($start, \FilesystemIterator::KEY_AS_PATHNAME | \FilesystemIterator::SKIP_DOTS)); while ($dir->valid()) { $name = $dir->key(); if (preg_match($pattern, $name) && !in_array(basename($name), $blackList)) { $files[] = $name; } elseif ($this->logger->debugLevel >= LOG_INFO) { if (in_array(basename($name), $blackList)) { $this->debug("$name blacklisted.\n", LOG_INFO); } else { $this->debug("$name does not match source paths.\n", LOG_DEBUG); } } $dir->next(); } } $files = array_unique($files); sort($files); return $files; } /** * Constructor helper: initialize GraphViz settings. */ public function initGraph() { $this->g = new \Image_Graphviz(); $this->g->setDirected(TRUE); $this->g->setAttributes(array( 'rankdir' => 'LR', 'overlap' => FALSE, )); } public function registerClass(ClassInstance $class) { $this->debug(' Registering class '. $class->name . "\n"); if (!empty($class->parent)) { if (!isset($this->registeredClasses[$class->parent])) { $this->debug(" Registering parent ". $class->parent . " for " . $class->name . "\n"); $gpParent = new \PGPClass($class->parent); $cpParent = new ClassInstance($gpParent, 1, $this->logger); $this->registerClass($cpParent); } } foreach ($class->interfaces as $interface) { if (!isset($this->registeredClasses[$interface])) { $this->debug(" Registering interface $interface for {$class->name}\n"); $gpInterface = new \PGPClass($interface); $cpInterface = new InterfaceInstance($gpInterface, 1, $this->logger); $this->registerInterface($cpInterface); } } // At this point, all ascendants are registered. if (!isset($this->registeredClasses[$class->name])) { $this->registeredClasses[$class->name] = $class; } // Override implicit status of a previously declared class if needed. if (!$class->implicit) { $this->registeredClasses[$class->name]->implicit = 0; } } public function registerInterface(InterfaceInstance $interface) { $this->debug("Registering interface {$interface->name}\n"); if (!empty($interface->parent)) { if (!isset($this->registeredInterfaces[$interface->parent])) { $this->debug(" Registering parent ". $interface->parent . " for " . $interface->name . "\n"); $gpParent = new \PGPClass($interface->parent); $cpParent = new InterfaceInstance($gpParent, 1, $this->logger); $this->registerInterface($cpParent); } } // At this point, all ascendants are registered. if (!isset($this->registeredInterfaces[$interface->name])) { $this->registeredInterfaces[$interface->name] = $interface; } // Override implicit status of a previously declared interface if needed. if (!$interface->implicit) { $this->registeredInterfaces[$interface->name]->implicit = 0; } } /** * Symbol extractor using grammar_parser. * * @param string $base */ public function extractGrammar($base) { // These files cause a segfault during parse, so we skip them for now. // Possible patch at http://drupal.org/node/1485370 $blackList = array( 'RouteCollection.php', 'HeaderBag.php', 'NativeSessionHandler.php', 'SimpleXMLElement.php', 'ContainerBuilderTest.php', 'YamlFileLoaderTest.php', 'ResponseTest.php', 'ConfigDataCollectorTest.php', 'KernelTest.php', 'UrlMatcherTest.php', ); $files = $this->getFiles($base, $blackList); foreach ($files as $filename) { $source = file_get_contents($filename, FALSE); $reader = new \PGPReader($source); unset($source); $this->debug("Parsing $filename\n", LOG_INFO); $reader->buildGrammar(); foreach ($reader->getInterfaces() as $interface) { $this->registerInterface(new InterfaceInstance($interface->data, 0, $this->logger)); } foreach ($reader->getClasses() as $class) { $this->registerClass(new ClassInstance($class->data, 0, $this->logger)); } $reader->reset(); unset($reader); } } public function __construct($base, Logger $logger) { $this->logger = $logger; $this->extractGrammar($base); $this->initGraph(); } public function implementsAttributes() { $attributes = array( 'arrowhead' => 'empty', 'arrowtail' => 'none', 'style' => 'dotted', ); $attributes = $this->g->_escapeArray($attributes); return $attributes; } public function buildSymbols($collection = array(), $kind = NULL) { foreach ($collection as $name => $symbol) { $this->debug("Building $kind $name\n"); $this->g->addNode($name, $symbol->attributes()); if ($symbol->parent) { $this->g->addEdge(array($name => $symbol->parent)); } if (empty($symbol->interfaces)) { continue; } foreach ($symbol->interfaces as $interface_name) { $this->g->addEdge(array($name => $interface_name), $this->implementsAttributes()); } } } /** * Build the graph once extraction has been performed. */ public function build($imager = 'dot', $format = 'svg') { $this->debug("Starting build for " . count($this->registeredInterfaces) . " interfaces and " . count($this->registeredClasses) . " classes.\n", WATCHDOG_INFO); $this->buildSymbols($this->registeredInterfaces, 'interface'); $this->buildSymbols($this->registeredClasses, 'class'); if (empty($imager) || $imager == 'dump') { // $this->debug(print_r($g->_getGroups(), TRUE)); $this->debug("Interfaces:\n"); $this->debug(print_r($this->registeredInterfaces, TRUE)); $this->debug("Classes\n"); $this->debug(print_r($this->registeredClasses, TRUE)); //$this->debug(print_r($this->g)); $ret = $this->g->parse(); } else { ob_start(); $this->g->image($format, $imager); $ret = ob_get_contents(); ob_end_clean(); } return $ret; } }