Visualize - Generate a graphic of your models/tables

By Andy Dawson (AD7six)
Based upon the script published by cakeexplorer (http://cakeexplorer.wordpress.com/2007/12/14/build-image-of-current-cake-schema), this script allows you to use Graphviz (http://www.graphviz.org) to generate a graphical representation of your models and/or tables

To install
ensure you are able to run the cake console commands
install graphviz
copy the below script into /app/vendors/shells/visualize.php

To run the script
at the command line:
cd /app/folder/is/here
cake visualize help
cake visualize

The output you'll find in the /app/config/sql folder

You can see a screencast if it in action here: http://www.ad7six.com/files/visualize.mpeg
Download code
<?php
/**
 * Visualize console task
 * 
 * This task can be used to generate a graphical representation of your tables or models.
 *
 * PHP versions 4 and 5
 *
 * Copyright (c) Tomenko Yevgeny
 *
 * Licensed under The MIT License
 * Redistributions of files must retain the above copyright notice.
 *
 * @version        1.2.0
 * @modifiedby        Andy Dawson
 * @lastmodified    2007-12-20 23:39:02 +0100 (Thu, 16 Aug 2007) $
 * @license        http://www.opensource.org/licenses/mit-license.php The MIT License
 */
uses('Folder','File','model'.DS.'connection_manager');
class 
VisualizeShell extends Shell {

    var 
$DOC_DIR ;

    var 
$PREFIX APP_DIR;

    var 
$graphToolPath 'dot.exe'// cake visualize -tool C:\dev\_tools_\graphviz-2.16\bin\dot.exe


    
function help() {
        
$this->out('CakePHP visualise, Usage examples:');
        
$this->out('cake visualize help');
        
$this->out('    - this text');
        
$this->out('cake visualize tables');
        
$this->out('    - generate graphic based on table structure');
        
$this->out('cake visualize models');
        
$this->out('    - generate graphic based on model association definitions');
        
$this->out('cake visualise [-tool graphVizTool]');
        
$this->hr();
    }

    function 
initialize() {
        if (
DS == '/') {
            
$this->graphToolPath = array(
                
'dot'
                
'dot -Gmode=heir',
                
'neato',
                
'neato -Gmodel=subset'
            
);
        }
        
$this->DOC_DIR APP 'config' DS 'sql';
        
$this->PREFIX'img_';
        if (isset(
$this->params['tool'])) {
            
$this->graphToolPath $this->params['tool'];
        }
        return 
true;
    }

    function 
main() {
        if (!isset(
$this->args[0])) {
            
$this->generateDataFromTables();    
            
$this->writeDotFile($this->DOC_DIR't');
            
$this->generateDataFromModels();    
            
$this->writeDotFile($this->DOC_DIR'm');
            return;
        } elseif (
$this->args[0] == 'help') {
            
$this->help();
            return;
        } elseif (
$this->args[0] == 'tables') {
            
$mode 't';
            
$this->generateDataFromTables();    
        } elseif (
$this->args[0] == 'models') {
            
$mode 'm';
            
$this->generateDataFromModels();    
        }
        
$this->writeDotFile($this->DOC_DIR$mode);
    }

    function 
generateDataFromModels() {
        foreach(
$this->getAllModels() as $model) {
            
$this->out("Looking at model: {$model}");
            
$model = new $model();
            if (!
$model->useTable) {
                continue;
            }
            
$this->data['tables'][$model->name] = $model->schema(true);
            foreach (
$this->data['tables'][$model->name] as $attrname => $attr) {
                if (!empty(
$attr['length'])) {
                    
$attr['type'] .= "[{$attr['length']}]";
                }
                
$this->data['nodes'][$model->name][$attrname] = $attr['type'];
                if (!empty(
$attr['default'])) {
                    
$this->data['nodes'][$model->name][$attrname] .= ", default: \\\"{$attr['default']}\\\"";
                }
            }

            foreach(
$model->__associations as $type) {
                foreach (
$model->$type as $alias => $association) {
                    
$otherModel $association['className'];
                    if (
$type == 'belongsTo') {
                        
$this->data['associations'][$model->name.$otherModel] = 
                            array(
'label'=> $model->name '->' $alias'node1'=> $model->name'node2'=> $otherModel);
                    } elseif (
in_array($type, array('hasOne''hasMany'))) {
                        
$this->data['associations'][$otherModel.$model->name] = 
                            array(
'label'=> $otherModel '->' $model->name'node1'=> $otherModel'node2'=> $model->name);
                    } elseif (
$type == 'hasAndBelongsToMany') {
                        
$names[] = $model->name;
                        
$names[] = $otherModel;
                        
sort($names);
                        
$modelName implode($names'');
                        if (!isset(
$modelName)) {
                            
$DynamicModel = new Model(array('name'=> $modelName'table'=> $association['joinTable'])); 
                            
$this->data['tables'][$modelName] = $DynamicModel->schema(true);
                            foreach (
$this->data['tables'][$modelName] as $attrname => $attr) {
                                if (!empty(
$attr['length'])) {
                                    
$attr['type'] .= "[{$attr['length']}]";
                                }
                                
$this->data['nodes'][$modelName][$attrname] = $attr['type'];
                                
$attrtype $attr['type'];
                                if (!empty(
$attr['default'])) {
                                    
$this->data['nodes'][$modelName][$attrname] .= ", default: \\\"{$attr['default']}\\\"";
                                }
                            }
                            
$this->data['associations'][$model->name.$otherModel] = 
                                array(
'label'=> $model->name '->' $modelName'node1'=> $model->name'node2'=> $modelName);
                            
$this->data['associations'][$otherModel.$model->name] = 
                                array(
'label'=> $otherModel '->' $modelName'node1'=> $otherModel'node2'=> $modelName);
                        }
                    }
                }
            }
        }
    }

    function 
generateDataFromTables() {
        foreach(
$this->getAllTables() as $table_name) {
            
$this->out("Looking at table: {$table_name}");
            
$modelName=$this->_modelName($table_name);
            
$this->data['tables'][$modelName] = $this->getSchemaInfo($modelName,$table_name);
        }
        foreach (
$this->data['tables'] as $table => $attributes) {
            if (
is_array($attributes) && count($attributes)>0) {
                foreach (
$attributes as $attrname => $attr) {
                    if (
substr($attrname, -3) == '_id') {
                        
# Create an association to other table
                        
$otherTable Inflector::camelize(r('_id','',$attrname));
                        if (!empty(
$this->data['tables'][$otherTable])) {
                            
$other_table $this->data['tables'][$otherTable];
                            
$this->data['associations'][] = array('label'=> $attrname'node1'=> $table'node2'=> $otherTable);
                        }
                    }
                    if (!empty(
$attr['length'])) {
                        
$attr['type'] .= "[{$attr['length']}]";
                    }
                    
$this->data['nodes'][$table][$attrname] = $attr['type'];
                    
$attrtype $attr['type'];
                    if (!empty(
$attr['default'])) {
                        
$this->data['nodes'][$table][$attrname] .= ", default: \\\"{$attr['default']}\\\"";
                    }
                }
            }
        }
    }

    function 
getAllModels() {
        
$Inflector =& Inflector::getInstance();
        
uses('Folder');
        
$folder = new Folder(MODELS);
        
$models $folder->findRecursive('.*php');
        
$folder = new Folder(BEHAVIORS);
        
$behaviors $folder->findRecursive('.*php');
        
$models array_diff($models$behaviors);
        foreach (
$models as $id => $model) {
            
$file = new File($model);
            
$models[$id] = $file->name();
        }
        
$models array_map(array(&$Inflector'camelize'), $models);
        
App::import('Model'$models);
        return 
$models;
    }

    function 
getAllTables($useDbConfig 'default') {
        
$db =& ConnectionManager::getDataSource($useDbConfig);
        
$usePrefix = empty($db->config['prefix']) ? ''$db->config['prefix'];
        if (
$usePrefix) {
            
$tables = array();
            foreach (
$db->listSources() as $table) {
                if (!
strncmp($table$usePrefixstrlen($usePrefix))) {
                    
$tables[] = substr($tablestrlen($usePrefix));
                }
            }
        } else {
            
$tables $db->listSources();
        }
        
$this->__tables $tables;
        return 
$tables;
    }

    function 
getSchemaInfo($modelName,$table_name) {
        
$attrs = array();
        if (
App::import('model',$modelName)) {
            
$model = & new $modelName();
            
$attrs=$model->schema();
            return 
$attrs;
        } else {
            
$DynamicModel = new Model(array('name'=> $modelName'table'=> $table_name)); 
            
$attrs=$DynamicModel->schema();
            return 
$attrs;
        }
        return 
false;
    }   

    function 
writeDotFile($target_dir$mode) {
            if (!
file_exists($target_dir) || !is_dir($target_dir)) {
            
$this->out("Creating directory \"{$target_dir}\"…");
            
$folder = & new Folder($target_dirtrue);
        }
        
$header $this->PREFIX+strftime('%Y-%m-%d %H:%M:%S',time());
        
$version=0;
        if (
$version 0) {
            
$header .= "\\nSchema version $version";
        }
        
$dotFile $target_dir .DS'mode_' $mode '.dot';
        if (
file_exists($dotFile)) {
            
$f = & new File($dotFile);
            
$f->delete();
        }
        
$f = & new File($dotFiletrue );

        
// Define a graph and some global settings
        
$f->append("digraph G {\n");
        
$f->append("\toverlap=false;\n");
        
$f->append("\tsplines=true;\n");
        
$f->append("\tnode [fontname=\"Helvetica\",fontsize=9];\n");
        
$f->append("\tedge [fontname=\"Helvetica\",fontsize=8];\n");
        
$f->append("\tranksep=0.1;\n");
        
$f->append("\tnodesep=0.1;\n");
        
//    $f->append("\tedge [decorate=\"true\"];\n");
        // Write header info
        
$f->append("\t_schema_info [shape=\"plaintext\", label=\"{$header}\", fontname=\"Helvetica\",fontsize=8];\n");

        
$assocs = array();
        
// Draw the tables as boxes
        
        
foreach ($this->data['nodes'] as $table=>$attributes) {
            
$f->append("\t\"{$table}\" [label=\"{{$table}|");
            foreach (
$attributes as $field=>$label) {
                
$f->append("{$field} : {$label}\\n");
            }
            
$f->append("}\" shape=\"record\"];\n");
        }
        
// Draw the relations
        
foreach ($this->data['associations'] as $assoc) {
            
$f->append("\t\"{$assoc['node1']}\" -> \"{$assoc['node2']}\" [label=\"{$assoc['label']}\"]\n");
        }

        
// Close the graph
        
$f->append("}\n");
        
$f->close();        // Create the images by using dot and neato (grapviz tools)
        
$this->out("Generated {$dotFile}\n");

        
$this->createImgs($dotFile$target_dir$mode);

        
// Remove the .dot file // Keep it for debugging and general info
        //$f->delete();
    
}

    function 
createImgs($dotFile$path$mode) {
        if (
is_string($this->graphToolPath)) {
            
$commands = array($this->graphToolPath);
        } else {
            
$commands $this->graphToolPath;
        }
        
uses ('Sanitize');
        foreach (
$commands as $command) {
            
$imgFile $path DS 'schematic_' $mode '_' Sanitize::paranoid($command) . ".png";
            if (
file_exists($imgFile)) {
                
$f = & new File($imgFile);
                
$f->delete();
            }
            if (
$this->createImg($command$dotFile$imgFile)) {
                
$this->out("Generated {$imgFile}\n");
            } else {
                break;
            }
        }
    }

    function 
createImg($command$dotFile$imgFile) {
        
$command "{$command} -Tpng  -o\"{$imgFile}\" \"{$dotFile}\"";
        
ob_start();
        
system($command,$return);
        
ob_clean();
        if (
$return != 0) {
            
$this->out("Command Error ($return):\n");          
            
$this->out("$command\n");
            return 
false;
        }
        return 
true;
    }
}
?>

 

Comments 606

CakePHP Team Comments Author Comments
 

Comment

1 How about an example

Thanks for the neat-sounding submission. I would love to see an example of how it works and what the output looks like before I try it myself.
Posted Jan 25, 2008 by ambiguator
 

Comment

2 See the screencast

Thanks for the neat-sounding submission. I would love to see an example of how it works and what the output looks like before I try it myself.
I added a link to a screencast of it in action. it's 5mb, 5 minutes and demonstrates the awsomeness of what cakeexplorer created (and I then tweaked).
Posted Jan 26, 2008 by Andy Dawson
 

Comment

3 SLick

I got it working on mac os x as well, Nice job
Posted Jan 26, 2008 by Sam DeVore
 

Bug

4 App import error

When I try to create the visualization I get the following error:

Fatal error: Class 'App' not found in /opt/local/apache2/htdocs/tahfa2/devapp/vendors/shells/visualize.php on line 201

Using 1.2 6311 on OS X - no problems running the console or visualize help.

Any ideas?

Thanks -
Posted Jan 28, 2008 by D Newton
 

Comment

5 for me

For me the key was to make sure that the graphviz was installed and that I updated the paths in the snippet to reflect the paths to the executables. For me it was something like the path to the executable for 'dot' is like
/Applications/Graphviz.app/Contents/MacOS/dot

around line 46 I changed the array to

 $this->graphToolPath = array(
               '/Applications/Graphviz.app/Contents/MacOS/dot',
               '/Applications/Graphviz.app/Contents/MacOS/dot -Gmode=heir',
               '/Applications/Graphviz.app/Contents/MacOS/neato',
               '/Applications/Graphviz.app/Contents/MacOS/neato -Gmodel=subset'
           );


Also I was using it with an app actually in the /app folder, the neato stuff I didn't bother to get working yet
Posted Jan 30, 2008 by Sam DeVore
 

Comment

6 Many thanks

Sam, thanks a bunch - that did the trick. This is helping me a lot with one of my clients. Hats off to you and Andy.
Posted Jan 30, 2008 by D Newton
 

Comment

7 fatal error


Welcome to CakePHP v1.2.0.6311 beta Console
---------------------------------------------------------------

App : app
Path: /var/www/udc/app
---------------------------------------------------------------
Looking at table: _config_parameters
Looking at table: _config_values
Looking at table: baskets
Looking at table: button_colors
Looking at table: button_patterns
Looking at table: button_styles
Looking at table: buttons
Looking at table: comments
Looking at table: descriptions
Looking at table: forums
Looking at table: images
Looking at table: in_bask
Looking at table: in_baskets
Looking at table: metadata
Looking at table: nodes
Looking at table: operations_logs
Looking at table: relations
Looking at table: sites
Looking at table: sites_descriptions
Looking at table: tags
Looking at table: threads
Looking at table: users
Generated /var/www/udc/app/config/sql/mode_t.dot

Generated /var/www/udc/app/config/sql/schematic_t_dot.png

Generated /var/www/udc/app/config/sql/schematic_t_dotGmodeheir.png

Generated /var/www/udc/app/config/sql/schematic_t_neato.png

Generated /var/www/udc/app/config/sql/schematic_t_neatoGmodelsubset.png

Looking at model: User
Looking at model: Forum
Looking at model: Basket
Looking at model: Image
Looking at model: Thread
Looking at model: Node1

Fatal error: Class 'Node1' not found in /var/www/udc/app/vendors/shells/visualize.php on line 82

* I do use a Node model. I've searched for Node1 string (case insensitive) and it was found only in visualize.php
* apache 2.2.4 / PHP Version 5.2.3-1ubuntu6.3 on Ubuntu 7.10

Any idea what might be going on under the hood?
Posted Feb 4, 2008 by Mateusz Mucha
 

Comment

8 Re Fatal Error

The list of models is derived from the php files you have in and under your models directory (excluding any behaviours). At a guess you have node_1.php or node1.php in there - have a look at the getAllModels method if that doesn't help.
Posted Feb 4, 2008 by Andy Dawson