#!/usr/bin/python """Create a Graphviz DOT file of PHP class dependencies in a folder""" import handy, optparse, os, re, stat exprPhpClass = r"^\s+class (?P\w+)( extends (?P\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) # copy everything returned into our classInfo 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() # bootstrap if run as a script not a module if __name__ == "__main__": main()