263 lines
7.7 KiB
PHP
263 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace OSInet\Class_Grapher;
|
|
|
|
/**
|
|
* Graph builder.
|
|
*/
|
|
class Graph {
|
|
/**
|
|
* A PEAR class.
|
|
*
|
|
* @var Image_GraphViz
|
|
*/
|
|
public $g;
|
|
|
|
/**
|
|
* @var Logger
|
|
*/
|
|
public $logger = NULL;
|
|
|
|
var $runtimeClasses = array();
|
|
var $runtimeInterfaces = array();
|
|
var $classes = array();
|
|
var $interfaces = array();
|
|
var $registeredClasses = array();
|
|
var $registeredInterfaces = array();
|
|
|
|
/**
|
|
* Represent attributes as a string in GraphViz format.
|
|
*
|
|
* @param array $attributes
|
|
*/
|
|
public function attributes(array $attributes) {
|
|
$attributes = $this->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;
|
|
}
|
|
}
|