<?php

/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */

/**
 * DB_Table_Generator - Generates DB_Table subclass skeleton code
 * 
 * Parts of this class were adopted from the DB_DataObject PEAR package.
 * 
 * PHP versions 4 and 5
 *
 * LICENSE:
 * 
 * Copyright (c) 1997-2007, Paul M. Jones <pmjones@php.net>
 *                          Alan Knowles <alan@akbkhome.com>
 *                          David C. Morse <morse@php.net>
 *                          Mark Wiesemann <wiesemann@php.net>
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    * Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    * Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the 
 *      documentation and/or other materials provided with the distribution.
 *    * The names of the authors may not be used to endorse or promote products 
 *      derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
 * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * @category Database
 * @package  DB_Table
 * @author   Alan Knowles <alan@akbkhome.com> 
 * @author   David C. Morse <morse@php.net>
 * @license  http://opensource.org/licenses/bsd-license.php New BSD License
 * @version  CVS: $Id: Generator.php,v 1.17 2008/05/14 18:36:27 wiesemann Exp $
 * @link     http://pear.php.net/package/DB_Table
 */

// {{{ Includes

/**#@+
 * Include basic classes
 */
/**
 * The PEAR class (used for errors)
 */
require_once 'PEAR.php';

/**
 * DB_Table table abstraction class
 */
require_once 'DB/Table.php';

/**
 * DB_Table_Manager class (used to reverse engineer indices)
 */
require_once 'DB/Table/Manager.php';
/**#@-*/

// }}}
// {{{ Error code constants

/**#@+
 * Error codes
 */
/**
 * Parameter is not a DB/MDB2 object
 */
define('DB_TABLE_GENERATOR_ERR_DB_OBJECT', -301);

/**
 * Parameter is not a DB/MDB2 object
 */
define('DB_TABLE_GENERATOR_ERR_INDEX_COL', -302);

/**
 * Error while creating file/directory
 */
define('DB_TABLE_GENERATOR_ERR_FILE', -303);
/**#@-*/

// }}}
// {{{ Error messages
/**
 * US-English default error messages.
 */
$GLOBALS['_DB_TABLE_GENERATOR']['default_error'] = array(
        DB_TABLE_GENERATOR_ERR_DB_OBJECT =>
            'Invalid DB/MDB2 object parameter. Function',
        DB_TABLE_GENERATOR_ERR_INDEX_COL =>
            'Index column is not a valid column name. Index column',
        DB_TABLE_GENERATOR_ERR_FILE =>
            'Can\'t create file/directory:'
);

// merge default and user-defined error messages
if (!isset($GLOBALS['_DB_TABLE_GENERATOR']['error'])) {
    $GLOBALS['_DB_TABLE_GENERATOR']['error'] = array();
}
foreach ($GLOBALS['_DB_TABLE_GENERATOR']['default_error'] as $code => $message) {
    if (!array_key_exists($code, $GLOBALS['_DB_TABLE_GENERATOR']['error'])) {
        $GLOBALS['_DB_TABLE_GENERATOR']['error'][$code] = $message;
    }
}

// }}}
// {{{ class DB_Table_Generator

/**
 * class DB_Table_Generator - Generates DB_Table subclass skeleton code
 *
 * This class generates the php code necessary to use the DB_Table
 * package to interact with an existing database. This requires the
 * generation of a skeleton subclass definition be generated for each
 * table in the database, in which the $col, $idx, and $auto_inc_col
 * properties are constructed using a table schema that is obtained
 * by querying the database.
 *
 * The class can also generate a file, named 'Database.php' by default,
 * that includes (require_once) each of the table subclass definitions,
 * instantiates one object of each DB_Table subclass (i.e., one object
 * for each table), instantiates a parent DB_Table_Database object,
 * adds all the tables to that parent, attempts to guess foreign key
 * relationships between tables based on the column names, and adds
 * the inferred references to the parent object.
 *
 * All of the code is written to a directory whose path is given by
 * the property $class_write_path. By default, this is the current
 * directory.  By default, the name of the class constructed for a
 * table named 'thing' is "Thing_Table". That is, the class name is
 * the table name, with the first letter upper case, with a suffix
 * '_Table'.  This suffix can be changed by setting the $class_suffix
 * property. The file containing a subclass definition is the
 * subclass name with a php extension, e.g., 'Thing_Table.php'. The
 * object instantiated from that subclass is the same as the table
 * name, with no suffix, e.g., 'thing'.
 *
 * To generate the code for all of the tables in a database named
 * $database, instantiate a MDB2 or DB object named $db that connects
 * to the database of interest, and execute the following code:
 * <code>
 *     $generator = new DB_Table_Generator($db, $database);
 *     $generator->class_write_path = $class_write_path;
 *     $generator->generateTableClassFiles();
 *     $generator->generateDatabaseFile();
 * </code>
 * Here $class_write_path should be the path (without a trailing
 * separator) to a directory in which all of the code should be
 * written. If this directory does not exist, it will be created.
 * If the directory does already exist, exising files will not
 * be overwritten. If $class_write_path is not set (i.e., if this
 * line is omitted) all the code will be written to the current
 * directory.  If ->generateDatabaseFile() is called, it must be
 * called after ->generateTableClassFiles().
 *
 * By default, ->generateTableClassFiles() and ->generateDatabaseFiles()
 * generate code for all of the tables in the current database. To
 * generate code for a specified list of tables, set the value of the
 * public $tables property to a sequential list of table names before
 * calling either of these methods. Code can be generated for three
 * tables named 'table1', 'table2', and 'table3' as follows:
 * <code>
 *     $generator = new DB_Table_Generator($db, $database);
 *     $generator->class_write_path = $class_write_path;
 *     $generator->tables = array('table1', 'table2', 'table3');
 *     $generator->generateTableClassFiles();
 *     $generator->generateDatabaseFile();
 * </code>
 * If the $tables property is not set to a non-null value prior
 * to calling ->generateTableClassFiles() then, by default, the
 * database is queried for a list of all table names, by calling the
 * ->getTableNames() method from within ->generateTableClassFiles().
 *
 * PHP version 4 and 5
 *
 * @category Database
 * @package  DB_Table
 * @author   Alan Knowles <alan@akbkhome.com> 
 * @author   David C. Morse <morse@php.net>
 * @license  http://www.gnu.org/copyleft/lesser.html LGPL
 * @version  Release: 1.5.5
 * @link     http://pear.php.net/package/DB_Table
 */
class DB_Table_Generator
{

    // {{{ Properties

    /**
     * Name of the database
     *
     * @var string
     * @access public
     */
    var $name = null;

    /**
     * The PEAR DB/MDB2 object that connects to the database.
     *
     * @var object
     * @access private
     */
    var $db = null;

    /**
     * The backend type. May have values 'db' or 'mdb2'
     *
     * @var string
     * @access private
     */
    var $backend = null;

    /**
    * If there is an error on instantiation, this captures that error.
    *
    * This property is used only for errors encountered in the constructor
    * at instantiation time.  To check if there was an instantiation error...
    *
    * <code>
    *     $obj =& new DB_Table_Generator();
    *     if ($obj->error) {
    *         // ... error handling code here ...
    *     }
    * </code>
    *
    * @var object PEAR_Error
    * @access public
    */
    var $error = null;

    /**
     * Numerical array of table name strings
     *
     * @var array
     * @access public
     */
    var $tables = array();

    /**
     * Class being extended (DB_Table or generic subclass)
     *
     * @var string
     * @access public
     */
    var $extends = 'DB_Table';

    /**
     * Path to definition of the class $this->extends
     *
     * @var string
     * @access public
     */
    var $extends_file = 'DB/Table.php';

    /**
     * Suffix to add to table names to obtain corresponding class names
     *
     * @var string
     * @access public
     */
    var $class_suffix = "_Table";

    /**
     * Path to directory in which subclass definitions should be written
     *
     * Value should not include a trailing "/".
     *
     * @var string
     * @access public
     */
    var $class_write_path = '';

    /**
     * Include path to subclass definition files from database file
     *
     * Used to create require_once statements in the Database.php file,
     * which is in the same directory as the class definition files. Leave
     * as empty string if your PHP include_path contains ".". The value
     * should not include a trailing "/", which is added automatically
     * to values other than the empty string.
     *
     * @var string
     * @access public
     */
    var $class_include_path = '';

    /**
     * Array of column definitions
     *
     * Array $this->col[table_name][column_name] = column definition.
     * Column definition is an array with the same format as the $col
     * property of a DB_Table object
     *
     * @var array
     * @access public
     */
    var $col = array();

    /**
     * Array of index/constraint definitions.
     *
     * Array $this->idx[table_table][index_name] = Index definition.
     * The index definition is an array with the same format as the
     * DB_Table $idx property property array.
     *
     * @var array
     * @access public
     */
     var $idx = array();

    /**
     * Array of auto_increment column names
     *
     * Array $this->auto_inc_col[table_name] = auto-increment column
     *
     * @var array
     * @access public
     */
     var $auto_inc_col = array();

    /**
     * Array of primary keys
     *
     * @var array
     * @access public
     */
     var $primary_key = array();

    /**
     * MDB2 'idxname_format' option, format of index names
     *
     * For use in printf() formatting. Use '%s' to use index names as
     * returned by getTableConstraints/Indexes, and '%s_idx' to add an
     * '_idx' suffix. For MySQL, use the default value '%'.
     *
     * @var string
     * @access public
     */
    var $idxname_format = '%s';

    // }}}
    // {{{ function DB_Table_Generator(&$db, $name)

    /**
     * Constructor
     *
     * If an error is encountered during instantiation, the error
     * message is stored in the $this->error property of the resulting
     * object. See $error property docblock for a discussion of error
     * handling.
     *
     * @param object &$db  DB/MDB2 database connection object
     * @param string $name database name string
     *
     * @return object DB_Table_Generator
     * @access public
     */
    function DB_Table_Generator(&$db, $name)
    {
        // Is $db an DB/MDB2 object or null?
        if (is_a($db, 'db_common')) {
            $this->backend = 'db';
        } elseif (is_a($db, 'mdb2_driver_common')) {
            $this->backend = 'mdb2';
        } else {
            $this->error =&
                DB_Table_Generator::throwError(DB_TABLE_GENERATOR_ERR_DB_OBJECT,
                'DB_Table_Generator');
            return;
        }
        $this->db   =& $db;
        $this->name =  $name;

    }

    // }}}
    // {{{ function &throwError($code, $extra = null)

    /**
     * Specialized version of throwError() modeled on PEAR_Error.
     *
     * Throws a PEAR_Error with a DB_Table_Generator error message based
     * on a DB_Table_Generator constant error code.
     *
     * @param string $code  A DB_Table_Generator error code constant.
     * @param string $extra Extra text for the error (in addition to the
     *                       regular error message).
     *
     * @return object PEAR_Error
     * @access public
     * @static
     */
    function &throwError($code, $extra = null)
    {
        // get the error message text based on the error code
        $text = 'DB_TABLE_GENERATOR ERROR - ' . "\n"
              . $GLOBALS['_DB_TABLE_GENERATOR']['error'][$code];

        // add any additional error text
        if ($extra) {
            $text .= ' ' . $extra;
        }

        // done!
        $error = PEAR::throwError($text, $code);
        return $error;
    }

    // }}}
    // {{{ function setErrorMessage($code, $message = null)

    /**
     * Overwrites one or more error messages, e.g., to internationalize them.
     *
     * @param mixed  $code    If string, the error message with code $code will be
     *                        overwritten by $message. If array, each key is a
     *                        code and each value is a new message.
     * @param string $message Only used if $key is not an array.
     *
     * @return void
     * @access public
     */
    function setErrorMessage($code, $message = null)
    {
        if (is_array($code)) {
            foreach ($code as $single_code => $single_message) {
                $GLOBALS['_DB_TABLE_GENERATOR']['error'][$single_code]
                    = $single_message;
            }
        } else {
            $GLOBALS['_DB_TABLE_GENERATOR']['error'][$code] = $message;
        }
    }

    // }}}
    // {{{ function getTableNames()

    /**
     * Gets a list of tables from the database
     *
     * Upon successful completion, names are stored in the $this->tables
     * array. If an error is encountered, a PEAR Error is returned, and
     * $this->tables is reset to null.
     *
     * @return mixed true on success, PEAR Error on failure
     * @access public
     */
    function getTableNames()
    {

        if ($this->backend == 'db') {
            // try getting a list of schema tables first. (postgres)
            $this->db->expectError(DB_ERROR_UNSUPPORTED);
            $this->tables = $this->db->getListOf('schema.tables');
            $this->db->popExpect();
            if (PEAR::isError($this->tables)) {
                // try a list of tables, not qualified by 'schema'
                $this->db->expectError(DB_ERROR_UNSUPPORTED);
                $this->tables = $this->db->getListOf('tables');
                $this->db->popExpect();
            }
        } else {
            // Temporarily change 'portability' MDB2 option
            $portability = $this->db->getOption('portability');
            $this->db->setOption('portability',
                MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE);

            $this->db->loadModule('Manager');
            $this->db->loadModule('Reverse');

            // Get list of tables
            $this->tables = $this->db->manager->listTables();

            // Restore original MDB2 'portability'
            $this->db->setOption('portability', $portability);
        }
        if (PEAR::isError($this->tables)) {
            $error        = $this->tables;
            $this->tables = null;
            return $error;
        } else {
            $this->tables = array_map(array($this, 'tableName'),
                                      $this->tables);
            return true;
        }
    }

    // }}}
    // {{{ function getTableDefinition($table)

    /**
     * Gets column and index definitions by querying database
     *
     * Upon return, column definitions are stored in $this->col[$table],
     * and index definitions in $this->idx[$table].
     *
     * Calls DB/MDB2::tableInfo() for column definitions, and uses
     * the DB_Table_Manager class to obtain index definitions.
     *
     * @param string $table name of table
     *
     * @return mixed true on success, PEAR Error on failure
     * @access public
     */
    function getTableDefinition($table)
    {
        /*
        // postgres strip the schema bit from the
        if (!empty($options['generator_strip_schema'])) {
            $bits = explode('.', $table,2);
            $table = $bits[0];
            if (count($bits) > 1) {
                $table = $bits[1];
            }
        }
        */

        if ($this->backend == 'db') {

            $defs = $this->db->tableInfo($table);
            if (PEAR::isError($defs)) {
                return $defs;
            }
            $this->columns[$table] = $defs;

        } else {

            // Temporarily change 'portability' MDB2 option
            $portability = $this->db->getOption('portability');
            $this->db->setOption('portability',
                MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE);

            $this->db->loadModule('Manager');
            $this->db->loadModule('Reverse');

            // Columns
            $defs = $this->db->reverse->tableInfo($table);
            if (PEAR::isError($defs)) {
                return $defs;
            }

            // rename the 'length' key, so it matches db's return.
            foreach ($defs as $k => $v) {
                if (isset($defs[$k]['length'])) {
                    $defs[$k]['len'] = $defs[$k]['length'];
                }
            }

            $this->columns[$table] = $defs;

            // Temporarily set 'idxname_format' MDB2 option to $this->idx_format
            $idxname_format = $this->db->getOption('idxname_format');
            $this->db->setOption('idxname_format', $this->idxname_format);
        }

        // Default - no auto increment column
        $this->auto_inc_col[$table] = null;

        // Loop over columns to create $this->col[$table]
        $this->col[$table] = array();
        foreach ($defs as $t) {

            $name = $t['name'];
            $col  = array();

            switch (strtoupper($t['type'])) {
            case 'INT2':     // postgres
            case 'TINYINT':
            case 'TINY':     //mysql
            case 'SMALLINT':
                $col['type'] = 'smallint';
                break;
            case 'INT4':      // postgres
            case 'SERIAL4':   // postgres
            case 'INT':
            case 'SHORT':     // mysql
            case 'INTEGER':
            case 'MEDIUMINT':
            case 'YEAR':
                $col['type'] = 'integer';
                break;
            case 'BIGINT':
            case 'LONG':    // mysql
            case 'INT8':    // postgres
            case 'SERIAL8': // postgres
                $col['type'] = 'bigint';
                break;
            case 'REAL':
            case 'NUMERIC':
            case 'NUMBER': // oci8
            case 'FLOAT':  // mysql
            case 'FLOAT4': // real (postgres)
                $col['type'] = 'single';
                break;
            case 'DOUBLE':
            case 'DOUBLE PRECISION': // double precision (firebird)
            case 'FLOAT8':           // double precision (postgres)
                $col['type'] = 'double';
                break;
            case 'DECIMAL':
            case 'MONEY':   // mssql and maybe others
                $col['type'] = 'decimal';
                break;
            case 'BIT':
            case 'BOOL':
            case 'BOOLEAN':
                $col['type'] = 'boolean';
                break;
            case 'STRING':
            case 'CHAR':
                $col['type'] = 'char';
                break;
            case 'VARCHAR':
            case 'VARCHAR2':
            case 'TINYTEXT':
                $col['type'] = 'varchar';
                break;
            case 'TEXT':
            case 'MEDIUMTEXT':
            case 'LONGTEXT':
                $col['type'] = 'clob';
                break;
            case 'DATE':
                $col['type'] = 'date';
                break;
            case 'TIME':
                $col['type'] = 'time';
                break;
            case 'DATETIME':  // mysql
            case 'TIMESTAMP':
                $col['type'] = 'timestamp';
                break;
            case 'ENUM':
            case 'SET':         // not really but oh well
            case 'TIMESTAMPTZ': // postgres
            case 'BPCHAR':      // postgres
            case 'INTERVAL':    // postgres (eg. '12 days')
            case 'CIDR':        // postgres IP net spec
            case 'INET':        // postgres IP
            case 'MACADDR':     // postgress network Mac address.
            case 'INTEGER[]':   // postgres type
            case 'BOOLEAN[]':   // postgres type
                $col['type'] = 'varchar';
                break;
            default:
                $col['type'] = $t['type'] . ' (Unknown type)';
                break;
            }

            // Set length and scope if required
            if (in_array($col['type'], array('char','varchar','decimal'))) {
                if (isset($t['len'])) {
                    $col['size'] = (int) $t['len'];
                } elseif ($col['type'] == 'varchar') {
                    $col['size'] = 255; // default length
                } elseif ($col['type'] == 'char') {
                    $col['size'] = 128; // default length
                } elseif ($col['type'] == 'decimal') {
                    $col['size'] = 15; // default length
                }
                if ($col['type'] == 'decimal') {
                    $col['scope'] = 2;
                }
            }
            if (isset($t['notnull'])) {
                if ($t['notnull']) {
                    $col['require'] = true;
                }
            }
            if (isset($t['autoincrement'])) {
                $this->auto_inc_col[$table] = $name;
            }
            if (isset($t['flags'])) {
                $flags = $t['flags'];
                if (preg_match('/not[ _]null/i', $flags)) {
                    $col['require'] = true;
                }
                if (preg_match("/(auto_increment|nextval\()/i", $flags)) {
                    $this->auto_inc_col[$table] = $name;
                }
            }
            $require = isset($col['require']) ? $col['require'] : false;
            if ($require) {
                if (isset($t['default'])) {
                    $default = $t['default'];
                    $type    = $col['type'];
                    if (in_array($type,
                                 array('smallint', 'integer', 'bigint'))) {
                        $default = (int) $default;
                    } elseif (in_array($type, array('single', 'double'))) {
                        $default = (float) $default;
                    } elseif ($type == 'boolean') {
                        $default = (int) $default ? 1 : 0;
                    }
                    $col['default'] = $default;
                }
            }
            $this->col[$table][$name] = $col;

        }

        // Make array with lower case column array names as keys
        $col_lc = array();
        foreach ($this->col[$table] as $name => $def) {
            $name_lc          = strtolower($name);
            $col_lc[$name_lc] = $name;
        }

        // Constraints/Indexes
        $DB_indexes = DB_Table_Manager::getIndexes($this->db, $table);
        if (PEAR::isError($DB_indexes)) {
            return $DB_indexes;
        }

        // Check that index columns correspond to valid column names.
        // Try to correct problems with capitalization, if necessary.
        foreach ($DB_indexes as $type => $indexes) {
            foreach ($indexes as $name => $fields) {
                foreach ($fields as $key => $field) {

                    // If index column is not a valid column name
                    if (!array_key_exists($field, $this->col[$table])) {

                        // Try a case-insensitive match
                        $field_lc = strtolower($field);
                        if (isset($col_lc[$field_lc])) {
                            $correct = $col_lc[$field_lc];
                            $DB_indexes[$type][$name][$key]
                                 = $correct;
                        } else {
                            $code   =  DB_TABLE_GENERATOR_ERR_INDEX_COL;
                            $return =&
                                DB_Table_Generator::throwError($code, $field);
                        }

                    }
                }
            }
        }

        // Generate index definitions, if any, as php code
        $n_idx = 0;
        $u     = array();

        $this->idx[$table]         = array();
        $this->primary_key[$table] = null;
        foreach ($DB_indexes as $type => $indexes) {
            if (count($indexes) > 0) {
                foreach ($indexes as $name => $fields) {
                    $this->idx[$table][$name]         = array();
                    $this->idx[$table][$name]['type'] = $type;
                    if (count($fields) == 1) {
                        $key = $fields[0];
                    } else {
                        $key = array();
                        foreach ($fields as $value) {
                            $key[] = $value;
                        }
                    }
                    $this->idx[$table][$name]['cols'] = $key;
                    if ($type == 'primary') {
                        $this->primary_key[$table] = $key;
                    }
                }
            }
        }

        if ($this->backend == 'mdb2') {
            // Restore original MDB2 'idxname_format' and 'portability'
            $this->db->setOption('idxname_format', $idxname_format);
            $this->db->setOption('portability', $portability);
        }

        return true;
    }

    // }}}
    // {{{ function buildTableClass($table, $indent = '')

    /**
     * Returns one skeleton DB_Table subclass definition, as php code
     *
     * The returned subclass definition string contains values for the
     * $col (column), $idx (index) and $auto_inc_col properties, with
     * no method definitions.
     *
     * @param string $table  name of table
     * @param string $indent string of whitespace for base indentation
     *
     * @return string skeleton DB_Table subclass definition
     * @access public
     */
    function buildTableClass($table, $indent = '')
    {
        $s   = array();
        $idx = array();
        $u   = array();
        $v   = array();
        $l   = 0;

        $s[]     = $indent . '/*';
        $s[]     = $indent . ' * Create the table object';
        $s[]     = $indent . ' */';
        $s[]     = $indent . 'class ' . $this->className($table)
                 . " extends {$this->extends} {\n";
        $indent .= '    ';

        $s[]     = $indent . '/*';
        $s[]     = $indent . ' * Column definitions';
        $s[]     = $indent . ' */';
        $s[]     = $indent . 'var $col = array(' . "\n";
        $indent .= '    ';

        // Begin loop over columns
        foreach ($this->col[$table] as $name => $col) {

            // Generate DB_Table column definitions as php code
            $t  = array();
            $t1 = array();
            $l1 = 0;

            $name     = $indent . "'{$name}'";
            $l        = max($l, strlen($name));
            $v[$name] = "array(\n";
            $indent  .= '    ';
            foreach ($col as $key => $value) {
                if (is_string($value)) {
                    $value = "'{$value}'";
                } elseif (is_bool($value)) {
                    $value = $value ? 'true' : 'false';
                } else {
                    $value = (string) $value;
                }
                $l1   = max($l1, strlen($key) + 2);
                $t1[] = array("'{$key}'", $value) ;
            }
            foreach ($t1 as $value) {
                $t[] = $indent . str_pad($value[0], $l1, ' ', STR_PAD_RIGHT)
                     . ' => ' . $value[1];
            }
            $v[$name] .= implode(",\n", $t) . "\n";
            $indent    = substr($indent, 0, -4);
            $v[$name] .= $indent . ')';
        } //end loop over columns

        foreach ($v as $key => $value) {
            $u[] = str_pad($key, $l, ' ', STR_PAD_RIGHT)
                 . ' => ' . $value;
        }
        $s[]    = implode(",\n\n", $u) . "\n";
        $indent = substr($indent, 0, -4);
        $s[]    = $indent . ");\n";

        // Generate index definitions, if any, as php code
        if (count($this->idx[$table]) > 0) {
            $u = array();
            $v = array();
            $l = 0;

            $s[]     = $indent . '/*';
            $s[]     = $indent . ' * Index definitions';
            $s[]     = $indent . ' */';
            $s[]     = $indent . 'var $idx = array(' . "\n";
            $indent .= '    ';
            foreach ($this->idx[$table] as $name => $def) {
                $type      = $def['type'];
                $cols      = $def['cols'];
                $name      = $indent . "'{$name}'";
                $l         = max($l, strlen($name));
                $v[$name]  = "array(\n";
                $indent   .= '    ';
                $v[$name] .= $indent . "'type' => '{$type}',\n";
                if (is_array($cols)) {
                    $v[$name] .= $indent . "'cols' => array(\n";
                    $indent   .= '    ';
                    $t         = array();
                    foreach ($cols as $value) {
                        $t[] = $indent . "'{$value}'";
                    }
                    $v[$name] .= implode(",\n", $t) . "\n";
                    $indent    = substr($indent, 0, -4);
                    $v[$name] .= $indent . ")\n";
                } else {
                    $v[$name] = $v[$name] . $indent . "'cols' => '{$cols}'\n";
                }
                $indent    = substr($indent, 0, -4);
                $v[$name] .= $indent . ")";
            }

            foreach ($v as $key => $value) {
                $u[] = str_pad($key, $l, ' ', STR_PAD_RIGHT)
                     . ' => ' . $value;
            }
            $s[]    = implode(",\n\n", $u) . "\n";
            $indent = substr($indent, 0, -4);
            $s[]    = $indent . ");\n";
        } // end index generation

        // Write auto_inc_col
        if (isset($this->auto_inc_col[$table])) {
            $s[] = $indent . '/*';
            $s[] = $indent . ' * Auto-increment declaration';
            $s[] = $indent . ' */';
            $s[] = $indent . 'var $auto_inc_col = '
                           . "'{$this->auto_inc_col[$table]}';\n";
        }
        $indent = substr($indent, 0, -4);
        $s[]    = $indent . '}';

        // Implode and return lines of class definition
        return implode("\n", $s) . "\n";

    }

    // }}}
    // {{{ function buildTableClasses()

    /**
     * Returns a string containing all table class definitions in one file
     *
     * The returned string contains the contents of a single php file with
     * definitions of DB_Table subclasses associated with all of the tables
     * in $this->tables. If $this->tables is initially null, method
     * $this->getTableNames() is called internally to generate a list of
     * table names.
     *
     * The returned string includes the opening and closing <?php and ?>
     * script elements, and the require_once line needed to include the
     * $this->extend_class (i.e., DB_Table or a subclass) that is being
     * extended. To use, write this string to a new php file.
     *
     * Usage:
     * <code>
     *     $generator = new DB_Table_Generator($db, $database);
     *     echo $generator->buildTablesClasses();
     * </code>
     *
     * @return mixed a string with all table class definitions,
     *                PEAR Error on failure
     * @access public
     */
    function buildTableClasses()
    {
        // If $this->tables is null, call getTableNames()
        if (!$this->tables) {
            $return = $this->getTableNames();
            if (PEAR::isError($return)) {
                return $return;
            }
        }

        $s   = array();
        $s[] = '<?php';
        $s[] = '/*';
        $s[] = ' * Include basic class';
        $s[] = ' */';
        $s[] = "require_once '{$this->extends_file}';\n";
        foreach ($this->tables as $table) {
            $return = $this->getTableDefinition($table);
            if (PEAR::isError($return)) {
                return $return;
            }
            $s[] = $this->buildTableClass($table) . "\n";
        }
        $s[] = '?>';
        return implode("\n", $s);
    }

    // }}}
    // {{{ function generateTableClassFiles()

    /**
     * Writes all table class definitions to separate files
     *
     * Usage:
     * <code>
     *     $generator = new DB_Table_Generator($db, $database);
     *     $generator->generateTableClassFiles();
     * </code>
     *
     * @return mixed true on success, PEAR Error on failure
     * @access public
     */
    function generateTableClassFiles()
    {
        // If $this->tables is null, call getTableNames()
        if (!$this->tables) {
            $return = $this->getTableNames();
            if (PEAR::isError($return)) {
                return $return;
            }
        }

        // Write all table class definitions to separate files
        foreach ($this->tables as $table) {
            $classname = $this->className($table);
            $filename  = $this->classFileName($classname);
            $base      = $this->class_write_path;
            if ($base) {
                if (!file_exists($base)) {
                    include_once 'System.php';
                    if (!@System::mkdir(array('-p', $base))) {
                        return $this->throwError(DB_TABLE_GENERATOR_ERR_FILE,
                            $base);
                    }

                }
                $filename = "{$base}/{$filename}";
            }
            if (!file_exists($filename)) {
                $s      = array();
                $s[]    = '<?php';
                $s[]    = '/*';
                $s[]    = ' * Include basic class';
                $s[]    = ' */';
                $s[]    = "require_once '{$this->extends_file}';\n";
                $return = $this->getTableDefinition($table);
                if (PEAR::isError($return)) {
                    return $return;
                }
                $s[] = $this->buildTableClass($table);
                $s[] = '?>';
                $s[] = '';
                $out = implode("\n", $s);
                if (!$file = @fopen($filename, 'wb')) {
                    return $this->throwError(DB_TABLE_GENERATOR_ERR_FILE,
                            $filename);
                }
                fputs($file, $out);
                fclose($file);
            }
        }

        return true;
    }

    // }}}
    // {{{ function generateDatabaseFile($object_name = null)

    /**
     * Writes a file to instantiate Table and Database objects
     *
     * After successful completion, a file named 'Database.php' will be
     * have been created in the $this->class_write_path directory. This
     * file should normally be included in application php scripts. It
     * can be renamed by the user.
     *
     * Usage:
     * <code>
     *     $generator = new DB_Table_Generator($db, $database);
     *     $generator->generateTableClassFiles();
     *     $generator->generateDatabaseFile();
     * </code>
     *
     * @param string $object_name variable name for DB_Table_Database object
     *
     * @return mixed true on success, PEAR Error on failure
     * @access public
     */
    function generateDatabaseFile($object_name = null)
    {
        // Set name for DB_Table_Database object
        if ($object_name) {
            $object_name = "\${$object_name}";
        } else {
            $object_name = '$db'; //default
        }
        $backend = strtoupper($this->backend); // 'DB' or 'MDB2'

        if ('DB' == $backend) {
            $dsn = $this->db->dsn;
        } else {
            $dsn = $this->db->getDSN('array');
        }

        // Create array d[] containing lines of database php file
        $d   = array();
        $d[] = '<?php';
        $d[] = '/*';
        $d[] = ' * Include basic classes';
        $d[] = ' */';
        $d[] = "require_once '{$backend}.php';";
        $d[] = "require_once 'DB/Table/Database.php';";

        // Require_once statements for subclass definitions
        foreach ($this->tables as $table) {
            $classname      = $this->className($table);
            $class_filename = $this->classFileName($classname);
            if ($this->class_include_path) {
                $d[] = 'require_once '
                     . "'{$this->class_include_path}/{$class_filename}';";
            } else {
                $d[] = "require_once '{$class_filename}';";
            }
        }
        $d[] = '';

        $d[] = '/*';
        $d[] = ' * NOTE: User must uncomment & edit code to create $dsn';
        $d[] = ' */';
        $d[] = "//\$phptype  = '{$dsn['phptype']}';";
        $d[] = "//\$username = '{$dsn['username']}';";
        $d[] = "//\$password = ''; // put your password here";
        $d[] = "//\$hostname = '{$dsn['hostspec']}';";
        $d[] = "//\$database = '{$dsn['database']}';";
        $d[] = "//\$create   = false; // 'drop', 'safe', 'verify', 'alter'";
        $d[] = '//$dsn      = "{$phptype}://{$username}:{$password}@{$hostname}'
             . '/{$database}";';
        $d[] = '';

        $d[] = '/*';
        $d[] = " * Instantiate {$backend} connection object \$conn";
        $d[] = ' */';
        $d[] = "\$conn =& {$backend}::connect(\$dsn);";
        $d[] = 'if (PEAR::isError($conn)) {';
        $d[] = '    echo "Error connecting to database server\n";';
        $d[] = '    echo $conn->getMessage();';
        $d[] = '    die;';
        $d[] = '}';
        $d[] = '';

        $d[] = '/*';
        $d[] = ' * Create one instance of each DB_Table subclass';
        $d[] = ' */';
        foreach ($this->tables as $table) {
            $classname = $this->className($table);

            $d[] = "\${$table} = new {$classname}("
                 . '$conn, ' . "'{$table}'" . ', $create);';
            $d[] = "if (PEAR::isError(\${$table}->error)) {";
            $d[] = '    echo "Can\'t create table object.\n";';
            $d[] = "    echo \${$table}->error->getMessage();";
            $d[] = '    die;';
            $d[] = '}';

        }
        $d[] = '';

        $d[] = '/*';
        $d[] = ' * Instantiate a parent DB_Table_Database object';
        $d[] = ' */';
        $d[] = "{$object_name} = new DB_Table_Database(\$conn, \$database);";
        $d[] = "if (PEAR::isError({$object_name}->error)) {";
        $d[] = '    echo "Can\'t create database object.\n";';
        $d[] = "    echo {$object_name}->error->getMessage();";
        $d[] = '    die;';
        $d[] = '}';
        $d[] = '';

        $d[] = '/*';
        $d[] = ' * Add DB_Table objects to parent DB_Table_Database object';
        $d[] = ' */';
        foreach ($this->tables as $table) {
            $classname = $this->className($table);

            $d[] = "\$result = {$object_name}->addTable(\${$table});";
            $d[] = 'if (PEAR::isError($result)) {';
            $d[] = '    echo "Can\'t add table object to database object.\n";';
            $d[] = '    echo $result->getMessage();';
            $d[] = '    die;';
            $d[] = '}';
        }
        $d[] = '';

        // Add foreign key references: If the name of an integer column
        // matches "/id$/i" (i.e., the names ends with id, ID, or Id), the
        // remainder of the name matches the name $rtable of another table,
        // and $rtable has an integer primary key, then the column is
        // assumed to be a foreign key that references $rtable.

        $d[] = '/*';
        $d[] = ' * Add auto-guessed foreign references';
        $d[] = ' */';
        foreach ($this->col as $table => $col) {
            foreach ($col as $col_name => $def) {

                // Only consider integer columns
                $ftype = $def['type'];
                if (!in_array($ftype, array('integer','smallint','bigint'))) {
                    continue;
                }
                if (preg_match("/id$/i", $col_name)) {
                    $column_base = preg_replace('/_?id$/i', '', $col_name);
                    foreach ($this->tables as $rtable) {
                        if (!preg_match("/^{$rtable}$/i", $column_base)) {
                            continue;
                        }
                        if (preg_match("/^{$table}$/i", $column_base)) {
                            continue;
                        }
                        if (!isset($this->primary_key[$rtable])) {
                            continue;
                        }
                        $rkey = $this->primary_key[$rtable];
                        if (is_array($rkey)) {
                            continue;
                        }
                        $rtype = $this->col[$rtable][$rkey]['type'];
                        if (!in_array($rtype,
                            array('integer','smallint','bigint'))) {
                            continue;
                        }
                        $d[] = "\$result = {$object_name}->addRef('{$table}', "
                             . "'{$col_name}', '{$rtable}');";
                        $d[] = 'if (PEAR::isError($result)) {';
                        $d[] = '    echo "Can\'t add foreign key reference.\n";';
                        $d[] = '    echo $result->getMessage();';
                        $d[] = '    die;';
                        $d[] = '}';
                    }
                }
            }
        }
        $d[] = '';
        $d[] = '/*';
        $d[] = ' * Add any additional foreign key references here';
        $d[] = ' *';
        $d[] = ' * Add any linking table declarations here';
        $d[] = ' * Uncomment next line to add all possible linking tables;';
        $d[] = ' */';
        $d[] = "//\$result = {$object_name}->addAllLinks();";
        $d[] = '//if (PEAR::isError($result)) {';
        $d[] = '//    echo "Can\'t add linking tables.\n";';
        $d[] = '//    echo $result->getMessage();';
        $d[] = '//    die;';
        $d[] = '//}';
        $d[] = '';

        // Closing script element
        $d[] = '?>';
        $d[] = '';

        // Open and write file
        $base = $this->class_write_path;
        if ($base) {
            if (!file_exists($base)) {
                include_once 'System.php';
                if (!@System::mkdir(array('-p', $base))) {
                    return $this->throwError(DB_TABLE_GENERATOR_ERR_FILE, $base);
                }
            }
            $filename = $base . '/Database.php';
        } else {
            $filename = 'Database.php';
        }
        if (!$file = @fopen($filename, 'wb')) {
            return $this->throwError(DB_TABLE_GENERATOR_ERR_FILE, $filename);
        }
        $out = implode("\n", $d);
        fputs($file, $out);
        fclose($file);

        return true;
    }

    // }}}
    // {{{ function className($table)

    /**
     * Convert a table name into a class name
     *
     * Converts all non-alphanumeric characters to '_', capitalizes
     * first letter, and adds $this->class_suffix to end. Override
     * this if you want something else.
     *
     * @param string $table name of table
     *
     * @return string class name;
     * @access public
     */
    function className($table)
    {
        $name = preg_replace('/[^A-Z0-9]/i', '_', ucfirst(trim($table)));
        return  $name . $this->class_suffix;
    }

    // }}}
    // {{{ function tableName($table)

    /**
     * Returns a valid variable name from a table name
     *
     * Converts all non-alphanumeric characters to '_'. Override
     * this if you want something else.
     *
     * @param string $table name of table
     *
     * @return string variable name;
     * @access public
     */
    function tableName($table)
    {
        return preg_replace('/[^A-Z0-9]/i', '_', trim($table));
    }

    // }}}
    // {{{ function classFileName($class_name)

    /**
     * Returns the path to a file containing a class definition
     *
     * Appends '.php' to class name.
     *
     * @param string $class_name name of class
     *
     * @return string file name
     * @access public
     */
    function classFileName($class_name)
    {
        $filename = $class_name . '.php';
        return $filename;
    }

    // }}}

}
// }}}
