"""
Create a Graphviz DOT file of PHP class dependencies in a folder
Afternoon <aftnn at aftnn.org>, 2004
"""
import handy, optparse, os, re, stat
exprPhpClass = r"^\s+class (?P<class>\w+)( extends (?P<baseclass>\w+))?\s+{?.+$"
rePhpClass = re.compile(exprPhpClass)
graphSettings = """
rankdir = "BT";
"""
def walktree(top, callback):
"""Apply a callback to every file in a directory"""
for f in os.listdir(top):
pathname = os.path.join(top, f)
mode = os.stat(pathname)[stat.ST_MODE]
if stat.S_ISDIR(mode):
walktree(pathname, callback)
elif stat.S_ISREG(mode):
callback(pathname)
def listPHPFiles(folder):
"""Return a list of the filenames of PHP files in the specified folder"""
class PHPFileStore:
files = []
def addIfPHP(self, x):
if x[-4:] == ".php": self.files.append(x)
s = PHPFileStore()
walktree(folder, s.addIfPHP)
return s.files
def parsePHPClassInfo(filename):
"""Find class info in a PHP file"""
f = open(filename)
classInfo = {}
for line in f:
m = rePhpClass.match(line)
if m:
d = m.groupdict()
if "baseclass" in d:
classInfo[d["class"]] = d["baseclass"]
else:
classInfo[d["class"]] = None
f.close()
return classInfo
def quote(s):
return "\"%s\"" % s
def dictAsDOTFile(dict):
"""Render a dictionary with dict[child] = parent structure as a Graphviz dot file"""
sources = []
dot = "digraph classes {\n"
for c in dict.keys():
if dict[c]:
if dict[c] not in dict.keys():
dot += "\t%s;\n" % quote(dict[c])
if quote(dict[c]) not in sources:
sources.append(quote(dict[c]))
dot += "\t%s -> %s;\n" % (quote(c), quote(dict[c]))
else:
dot += "\t%s;\n" % quote(c)
if quote(c) not in sources:
sources.append("%s" % quote(c))
dot += "\n"
dot += "\t{ rank = same; %s }" % "; ". join(sources)
dot += graphSettings
dot += "}\n"
return dot
def graphFolders(folders):
"""Find all PHP files in each of the folders and print out a Graphviz DOT file showing their structure"""
files = []
classInfo = {}
for fol in folders:
files += listPHPFiles(fol)
for filename in files:
ci = parsePHPClassInfo(filename)
for c in ci.keys():
classInfo[c] = ci[c]
return dictAsDOTFile(classInfo)
def main():
try:
parser = optparse.OptionParser(usage = "usage: %prog folder1 ... foldern")
parser.formatter = handy.NonsuckyHelpFormatter()
(opts, args) = parser.parse_args()
if not len(args):
parser.print_help()
print "%s: error: please specify one or more folders to search" % sys.argv[0]
sys.exit()
print graphFolders(args)
except KeyboardInterrupt: sys.exit()
if __name__ == "__main__": main()