#!/usr/bin/python

"""
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)

        # 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()
aftnn.orgcontentcode → phphierarchy