* @copyright 1997-2006 The PHP Group * @license http://www.php.net/license/3_01.txt PHP License 3.01 * @version CVS: $Id: Generator.php 336719 2015-05-05 10:37:33Z alan_k $ * @link http://pear.php.net/package/DB_DataObject */ /* * Security Notes: * This class uses eval to create classes on the fly. * The table name and database name are used to check the database before writing the * class definitions, we now check for quotes and semi-colon's in both variables * so I cant see how it would be possible to generate code even if * for some crazy reason you took the classname and table name from User Input. * * If you consider that wrong, or can prove it.. let me know! */ /** * * Config _$ptions * [DB_DataObject] * ; optional default = DB/DataObject.php * extends_location = * ; optional default = DB_DataObject * extends = * ; alter the extends field when updating a class (defaults to only replacing DB_DataObject) * generator_class_rewrite = ANY|specific_name // default is DB_DataObject * */ /** * Needed classes * We lazy load here, due to problems with the tests not setting up include path correctly. * FIXME! */ class_exists('DB_DataObject') ? '' : /*require_once 'DB/DataObject.php'*/ require_once '../DataObject.php'; //require_once('Config.php'); /** * Generator class * * @package DB_DataObject */ class DB_DataObject_Generator extends DB_DataObject { /* =========================================================== */ /* Utility functions - for building db config files */ /* =========================================================== */ /** * Array of table names * * @var array * @access private */ public $tables; /** * associative array table -> array of table row objects * * @var array * @access private */ public $_definitions; /** * active table being output * * @var string * @access private */ public $table; // active tablename /** * links (generated) * * @var array * @access private */ public $_fkeys; // active tablename /** * Output File was config object, now just string * Used to generate the Tables * * @var string outputbuffer for table definitions * @access private */ public $_newConfig; /** * class being extended (can be overridden by [DB_DataObject] extends=xxxx * * @var string * @access private */ public $_extends = 'DB_DataObject'; /** * line to use for require('DB/DataObject.php'); * * @var string * @access private */ public $_extendsFile = "DB/DataObject.php"; /** * class being generated * * @var string * @access private */ public $_className; /** * The 'starter' = call this to start the process * * @access public * @return none */ public function start() { $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; $databases = array(); foreach ($options as $k => $v) { if (substr($k, 0, 9) == 'database_') { $databases[substr($k, 9)] = $v; } } if (isset($options['database'])) { if ($db_driver == 'DB') { require_once 'DB.php'; $dsn = DB::parseDSN($options['database']); } else { require_once 'MDB2.php'; $dsn = MDB2::parseDSN($options['database']); } if (!isset($database[$dsn['database']])) { $databases[$dsn['database']] = $options['database']; } } foreach ($databases as $databasename => $database) { if (!$database) { continue; } $this->debug("CREATING FOR $databasename\n"); $class = get_class($this); $t = new $class; $t->_database_dsn = $database; $t->_database = $databasename; if ($db_driver == 'DB') { require_once 'DB.php'; $dsn = DB::parseDSN($database); } else { require_once 'MDB2.php'; $dsn = MDB2::parseDSN($database); } if (($dsn['phptype'] == 'sqlite') && is_file($databasename)) { $t->_database = basename($t->_database); } $t->_createTableList(); $t->_createForiegnKeys(); foreach (get_class_methods($class) as $method) { if (substr($method, 0, 8) != 'generate') { continue; } $this->debug("calling $method"); $t->$method(); } } $this->debug("DONE\n\n"); return null; } /** * Build a list of tables; * and store it in $this->tables and $this->_definitions[tablename]; * * @access private * @return none|object */ public function _createTableList() { $this->_connect(); $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; $is_MDB2 = ($db_driver != 'DB') ? true : false; if (is_object($__DB) && is_a($__DB, 'PEAR_Error')) { return (new PEAR)->raiseError($__DB->toString(), null, PEAR_ERROR_DIE); } if (!$is_MDB2) { // try getting a list of schema tables first. (postgres) $__DB->expectError(DB_ERROR_UNSUPPORTED); $this->tables = $__DB->getListOf('schema.tables'); $__DB->popExpect(); } else { /** * set portability and some modules to fetch the informations */ $db_options = (new PEAR)->getStaticProperty('MDB2', 'options'); if (empty($db_options)) { $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE); } $__DB->loadModule('Manager'); $__DB->loadModule('Reverse'); } if ((empty($this->tables) || (is_object($this->tables) && is_a($this->tables, 'PEAR_Error')))) { //if that fails fall back to clasic tables list. if (!$is_MDB2) { // try getting a list of schema tables first. (postgres) $__DB->expectError(DB_ERROR_UNSUPPORTED); $this->tables = $__DB->getListOf('tables'); $__DB->popExpect(); } else { $this->tables = $__DB->manager->listTables(); $sequences = $__DB->manager->listSequences(); foreach ($sequences as $k => $v) { $this->tables[] = $__DB->getSequenceName($v); } } } if (is_object($this->tables) && is_a($this->tables, 'PEAR_Error')) { return (new PEAR)->raiseError($this->tables->toString(), null, PEAR_ERROR_DIE); } // build views as well if asked to. if (!empty($options['build_views'])) { if (!$is_MDB2) { $views = $__DB->getListOf(is_string($options['build_views']) ? $options['build_views'] : 'views'); } else { $views = $__DB->manager->listViews(); } if (is_object($views) && is_a($views, 'PEAR_Error')) { return (new PEAR)->raiseError( 'Error getting Views (check the PEAR bug database for the fix to DB), ' . $views->toString(), null, PEAR_ERROR_DIE ); } $this->tables = array_merge($this->tables, $views); } // declare a temporary table to be filled with matching tables names $tmp_table = array(); foreach ($this->tables as $table) { if (isset($options['generator_include_regex']) && !preg_match($options['generator_include_regex'], $table)) { $this->debug("SKIPPING (generator_include_regex) : $table"); continue; } if (isset($options['generator_exclude_regex']) && preg_match($options['generator_exclude_regex'], $table)) { continue; } $strip = empty($options['generator_strip_schema']) ? false : $options['generator_strip_schema']; $strip = is_numeric($strip) ? (bool)$strip : $strip; $strip = (is_string($strip) && strtolower($strip) == 'true') ? true : $strip; // postgres strip the schema bit from the if (!empty($strip)) { if (!is_string($strip) || preg_match($strip, $table)) { $bits = explode('.', $table, 2); $table = $bits[0]; if (count($bits) > 1) { $table = $bits[1]; } } } $this->debug("EXTRACTING : $table"); $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $__DB->quoteIdentifier($table) : $table; if (!$is_MDB2) { $defs = $__DB->tableInfo($quotedTable); } else { $defs = $__DB->reverse->tableInfo($quotedTable); // rename the length value, so it matches db's return. } if (is_object($defs) && is_a($defs, 'PEAR_Error')) { // running in debug mode should pick this up as a big warning.. $this->debug("Error reading tableInfo: $table"); $this->raiseError('Error reading tableInfo, ' . $defs->toString()); continue; } // cast all definitions to objects - as we deal with that better. foreach ($defs as $def) { if (!is_array($def)) { continue; } // rename the length value, so it matches db's return. if (isset($def['length']) && !isset($def['len'])) { $def['len'] = $def['length']; } $this->_definitions[$table][] = (object)$def; } // we find a matching table, just store it into a temporary array $tmp_table[] = $table; } // the temporary table array is now the right one (tables names matching // with regex expressions have been removed) $this->tables = $tmp_table; //print_r($this->_definitions); return null; } /** * Auto generation of table data. * * it will output to db_oo_{database} the table definitions * * @access private * @return none|void */ public function generateDefinitions() { $this->debug("Generating Definitions file: "); if (!$this->tables) { $this->debug("-- NO TABLES -- \n"); return null; } $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); //$this->_newConfig = new Config('IniFile'); $this->_newConfig = ''; foreach ($this->tables as $this->table) { $this->_generateDefinitionsTable(); } $this->_connect(); // dont generate a schema if location is not set // it's created on the fly! if (empty($options['schema_location']) && empty($options["ini_{$this->_database}"])) { return null; } if (!empty($options['generator_no_ini'])) { // built in ini files.. return null; } $base = @$options['schema_location']; if (isset($options["ini_{$this->_database}"])) { $file = $options["ini_{$this->_database}"]; } else { $file = "{$base}/{$this->_database}.ini"; } if (!file_exists(dirname($file))) { require_once 'System.php'; (new System)->mkdir(array('-p', '-m', 0755, dirname($file))); } $this->debug("Writing ini as {$file}\n"); //touch($file); $tmpname = tempnam(session_save_path(), 'DataObject_'); //print_r($this->_newConfig); $fh = fopen($tmpname, 'w'); if (!$fh) { return (new PEAR)->raiseError( "Failed to create temporary file: $tmpname\n" . "make sure session.save_path is set and is writable\n", null, PEAR_ERROR_DIE ); } fwrite($fh, $this->_newConfig); fclose($fh); $perms = file_exists($file) ? fileperms($file) : 0755; // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy.. if (!@rename($tmpname, $file)) { unlink($file); rename($tmpname, $file); } chmod($file, $perms); //$ret = $this->_newConfig->writeInput($file,false); //if ((new PEAR)->isError($ret) ) { // return PEAR::raiseError($ret->message,null,PEAR_ERROR_DIE); // } return null; } /** * The table geneation part * * @access private * @return array|tabledef */ public function _generateDefinitionsTable() { global $_DB_DATAOBJECT; $options = (new PEAR)->getStaticProperty('DB_DataObject', 'options'); $defs = $this->_definitions[$this->table]; $this->_newConfig .= "\n[{$this->table}]\n"; $keys_out = "\n[{$this->table}__keys]\n"; $keys_out_primary = ''; $keys_out_secondary = ''; if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) { echo "TABLE STRUCTURE FOR {$this->table}\n"; print_r($defs); } $DB = $this->getDatabaseConnection(); $dbtype = $DB->phptype; $ret = array( 'table' => array(), 'keys' => array(), ); $ret_keys_primary = array(); $ret_keys_secondary = array(); foreach ($defs as $t) { $n = 0; $write_ini = true; switch (strtoupper($t->type)) { case 'INT': case 'INT2': // postgres case 'INT4': // postgres case 'INT8': // postgres case 'SERIAL4': // postgres case 'SERIAL8': // postgres case 'INTEGER': case 'TINYINT': case 'SMALLINT': case 'MEDIUMINT': case 'BIGINT': $type = DB_DATAOBJECT_INT; if ($t->len == 1) { $type += DB_DATAOBJECT_BOOL; } break; case 'REAL': case 'DOUBLE': case 'DOUBLE PRECISION': // double precision (firebird) case 'FLOAT': case 'FLOAT4': // real (postgres) case 'FLOAT8': // double precision (postgres) case 'DECIMAL': case 'MONEY': // mssql and maybe others case 'NUMERIC': case 'NUMBER': // oci8 $type = DB_DATAOBJECT_INT; // should really by FLOAT!!! / MONEY... break; case 'YEAR': $type = DB_DATAOBJECT_INT; break; case 'BIT': case 'BOOL': case 'BOOLEAN': $type = DB_DATAOBJECT_BOOL; // postgres needs to quote '0' if ($dbtype == 'pgsql') { $type += DB_DATAOBJECT_STR; } break; case 'STRING': case 'CHAR': case 'VARCHAR': case 'VARCHAR2': case 'TINYTEXT': case 'ENUM': case 'SET': // not really but oh well case 'POINT': // mysql geometry stuff - not really string - but will do.. 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 $type = DB_DATAOBJECT_STR; break; case 'TEXT': case 'MEDIUMTEXT': case 'LONGTEXT': case '_TEXT': //postgres (?? view ??) $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TXT; break; case 'DATE': $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE; break; case 'TIME': $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_TIME; break; case 'DATETIME': $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME; break; case 'TIMESTAMP': // do other databases use this??? $type = ($dbtype == 'mysql') ? DB_DATAOBJECT_MYSQLTIMESTAMP : DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME; break; case 'BLOB': /// these should really be ignored!!!??? case 'TINYBLOB': case 'MEDIUMBLOB': case 'LONGBLOB': case 'CLOB': // oracle character lob support case 'BYTEA': // postgres blob support.. $type = DB_DATAOBJECT_STR + DB_DATAOBJECT_BLOB; break; default: echo "*****************************************************************\n" . "** WARNING UNKNOWN TYPE **\n" . "** Found column '{$t->name}', of type '{$t->type}' **\n" . "** Please submit a bug, describe what type you expect this **\n" . "** column to be **\n" . "** ---------POSSIBLE FIX / WORKAROUND -------------------------**\n" . "** Try using MDB2 as the backend - eg set the config option **\n" . "** db_driver = MDB2 **\n" . "*****************************************************************\n"; $write_ini = false; break; } if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $t->name)) { echo "*****************************************************************\n" . "** WARNING COLUMN NAME UNUSABLE **\n" . "** Found column '{$t->name}', of type '{$t->type}' **\n" . "** Since this column name can't be converted to a php variable **\n" . "** name, and the whole idea of mapping would result in a mess **\n" . "** This column has been ignored... **\n" . "*****************************************************************\n"; continue; } if (!strlen(trim($t->name))) { continue; // is this a bug? } if (preg_match('/not[ _]null/i', $t->flags)) { $type += DB_DATAOBJECT_NOTNULL; } if (in_array($t->name, array('null', 'yes', 'no', 'true', 'false'))) { echo "*****************************************************************\n" . "** WARNING **\n" . "** Found column '{$t->name}', which is invalid in an .ini file **\n" . "** This line will not be writen to the file - you will have **\n" . "** define the keys()/method manually. **\n" . "*****************************************************************\n"; $write_ini = false; } else { $this->_newConfig .= "{$t->name} = $type\n"; } $ret['table'][$t->name] = $type; // i've no idea if this will work well on other databases? // only use primary key or nextval(), cause the setFrom blocks you setting all key items... // if no keys exist fall back to using unique //echo "\n{$t->name} => {$t->flags}\n"; $secondary_key_match = isset($options['generator_secondary_key_match']) ? $options['generator_secondary_key_match'] : 'primary|unique'; $m = array(); if (preg_match('/(auto_increment|nextval\(([^)]*))/i', rawurldecode($t->flags), $m) || (isset($t->autoincrement) && ($t->autoincrement === true))) { $sn = 'N'; if ($DB->phptype == 'pgsql' && !empty($m[2])) { $sn = preg_replace('/[("]+/', '', $m[2]); //echo urldecode($t->flags) . "\n" ; } // native sequences = 2 if ($write_ini) { $keys_out_primary .= "{$t->name} = $sn\n"; } $ret_keys_primary[$t->name] = $sn; } elseif ($secondary_key_match && preg_match('/(' . $secondary_key_match . ')/i', $t->flags)) { // keys.. = 1 $key_type = 'K'; if (!preg_match("/(primary)/i", $t->flags)) { $key_type = 'U'; } if ($write_ini) { $keys_out_secondary .= "{$t->name} = {$key_type}\n"; } $ret_keys_secondary[$t->name] = $key_type; } } $this->_newConfig .= $keys_out . (empty($keys_out_primary) ? $keys_out_secondary : $keys_out_primary); $ret['keys'] = empty($keys_out_primary) ? $ret_keys_secondary : $ret_keys_primary; if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) { print_r(array("dump for {$this->table}", $ret)); } return $ret; } /** * create the data for Foreign Keys (for links.ini) * Currenly only works with mysql / mysqli / posgtreas * to use, you must set option: generate_links=true * * @author Pascal Sch�ni */ public function _createForiegnKeys() { $options = (new PEAR)->getStaticProperty('DB_DataObject', 'options'); if (empty($options['generate_links'])) { return false; } $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; if (!in_array($__DB->phptype, array('mysql', 'mysqli', 'pgsql'))) { echo "WARNING: cant handle non-mysql and pgsql introspection for defaults."; return null; // cant handle non-mysql introspection for defaults. } $this->debug("generateForeignKeys: Start"); $DB = $this->getDatabaseConnection(); $fk = array(); switch ($DB->phptype) { case 'pgsql': foreach ($this->tables as $this->table) { $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $DB->quoteIdentifier($this->table) : $this->table; $res =& $DB->query("SELECT pg_catalog.pg_get_constraintdef(r.oid, true) AS condef FROM pg_catalog.pg_constraint r, pg_catalog.pg_class c WHERE c.oid=r.conrelid AND r.contype = 'f' AND c.relname = '" . $quotedTable . "'"); if ((new PEAR)->isError($res)) { die($res->getMessage()); } if ($db_driver == 'DB') { $fetchmode = DB_FETCHMODE_ASSOC; } else { $fetchmode = MDB2_FETCHMODE_ASSOC; } while ($row = $res->fetchRow($fetchmode)) { $treffer = array(); // this only picks up one of these.. see this for why: http://pear.php.net/bugs/bug.php?id=17049 preg_match( "/FOREIGN KEY \((\w*)\) REFERENCES (\w*)\((\w*)\)/i", $row['condef'], $treffer ); if (!count($treffer)) { continue; } $fk[$this->table][$treffer[1]] = $treffer[2] . ":" . $treffer[3]; } } break; case 'mysql': case 'mysqli': default: foreach ($this->tables as $this->table) { $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $DB->quoteIdentifier($this->table) : $this->table; $res =& $DB->query('SHOW CREATE TABLE ' . $quotedTable); if ((new PEAR)->isError($res)) { die($res->getMessage()); } if ($db_driver == 'DB') { $text = $res->fetchRow(DB_FETCHMODE_DEFAULT, 0); } else { $text = $res->fetchRow(MDB2_FETCHMODE_DEFAULT, 0); } $treffer = array(); // Extract FOREIGN KEYS preg_match_all( "/FOREIGN KEY \(`(\w*)`\) REFERENCES `(\w*)` \(`(\w*)`\)/i", $text[1], $treffer, PREG_SET_ORDER ); if (!count($treffer)) { continue; } foreach ($treffer as $i => $tref) { $fk[$this->table][$tref[1]] = $tref[2] . ":" . $tref[3]; } } } $this->_fkeys = $fk; return null; } /** * generate Foreign Keys (for links.ini) * Currenly only works with mysql / mysqli * to use, you must set option: generate_links=true * * @author Pascal Sch�ni */ public function generateForeignKeys() { $options = (new PEAR)->getStaticProperty('DB_DataObject', 'options'); if (empty($options['generate_links'])) { return false; } $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; if (!in_array($__DB->phptype, array('mysql', 'mysqli', 'pgsql'))) { echo "WARNING: cant handle non-mysql and pgsql introspection for defaults."; return null; // cant handle non-mysql introspection for defaults. } $this->debug("generateForeignKeys: Start"); $fk = $this->_fkeys; $links_ini = ""; foreach ($fk as $table => $details) { $links_ini .= "[$table]\n"; foreach ($details as $col => $ref) { $links_ini .= "$col = $ref\n"; } $links_ini .= "\n"; } // dont generate a schema if location is not set // it's created on the fly! $options = (new PEAR)->getStaticProperty('DB_DataObject', 'options'); if (!empty($options['schema_location'])) { $file = "{$options['schema_location']}/{$this->_database}.links.ini"; } elseif (isset($options["ini_{$this->_database}"])) { $file = preg_replace('/\.ini/', '.links.ini', $options["ini_{$this->_database}"]); } else { $this->debug("generateForeignKeys: SKIP - schema_location or ini_{database} was not set"); return null; } if (!file_exists(dirname($file))) { mkdir(dirname($file), 0755, true); } $this->debug("Writing ini as {$file}\n"); //touch($file); // not sure why this is needed? $tmpname = tempnam(session_save_path(), 'DataObject_'); $fh = fopen($tmpname, 'w'); if (!$fh) { return (new PEAR)->raiseError( "Failed to create temporary file: $tmpname\n" . "make sure session.save_path is set and is writable\n", null, PEAR_ERROR_DIE ); } fwrite($fh, $links_ini); fclose($fh); $perms = file_exists($file) ? fileperms($file) : 0755; // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy.. if (!@rename($tmpname, $file)) { unlink($file); rename($tmpname, $file); } chmod($file, $perms); return null; } /* * building the class files * for each of the tables output a file! */ public function generateClasses() { //echo "Generating Class files: \n"; $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $this->_extends = empty($options['extends']) ? $this->_extends : $options['extends']; $this->_extendsFile = !isset($options['extends_location']) ? $this->_extendsFile : $options['extends_location']; foreach ($this->tables as $this->table) { $this->table = trim($this->table); $this->classname = $this->getClassNameFromTableName($this->table); $i = ''; $outfilename = $this->getFileNameFromTableName($this->table); $oldcontents = ''; if (file_exists($outfilename)) { // file_get_contents??? $oldcontents = implode('', file($outfilename)); } $out = $this->_generateClassTable($oldcontents); $this->debug("writing $this->classname\n"); $tmpname = tempnam(session_save_path(), 'DataObject_'); $fh = fopen($tmpname, "w"); if (!$fh) { return (new PEAR)->raiseError( "Failed to create temporary file: $tmpname\n" . "make sure session.save_path is set and is writable\n", null, PEAR_ERROR_DIE ); } fputs($fh, $out); fclose($fh); $perms = file_exists($outfilename) ? fileperms($outfilename) : 0755; // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy.. if (!@rename($tmpname, $outfilename)) { unlink($outfilename); rename($tmpname, $outfilename); } chmod($outfilename, $perms); } //echo $out; return null; } /** * Convert a table name into a class name -> override this if you want a different mapping * * @access public * @param $table * @return string class name; */ public function getClassNameFromTableName($table) { $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix']; return $class_prefix . preg_replace('/[^A-Z0-9]/i', '_', ucfirst(trim($this->table))); } /** * Convert a table name into a file name -> override this if you want a different mapping * * @access public * @param $table * @return string file name; */ public function getFileNameFromTableName($table) { $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $base = $options['class_location']; if (strpos($base, '%s') !== false) { $base = dirname($base); } if (!file_exists($base)) { require_once 'System.php'; (new System)->mkdir(array('-p', $base)); } if (strpos($options['class_location'], '%s') !== false) { $outfilename = sprintf( $options['class_location'], preg_replace('/[^A-Z0-9]/i', '_', ucfirst($this->table)) ); } else { $outfilename = "{$base}/" . preg_replace('/[^A-Z0-9]/i', '_', ucfirst($this->table)) . ".php"; } return $outfilename; } /** * The table class geneation part - single file. * * @access private * @param string $input * @return none|string */ public function _generateClassTable($input = '') { // title = expand me! $foot = ""; $head = "table}\n"; $head .= $this->derivedHookPageLevelDocBlock(); $head .= " */\n"; $head .= $this->derivedHookExtendsDocBlock(); // requires - if you set extends_location = (blank) then no require line will be set // this can be used if you have an autoloader if (!empty($this->_extendsFile)) { $head .= "require_once '{$this->_extendsFile}';\n\n"; } // add dummy class header in... // class $head .= $this->derivedHookClassDocBlock(); $head .= "class {$this->classname} extends {$this->_extends} \n{"; $body = "\n ###START_AUTOCODE\n"; $body .= " /* the code below is auto generated do not remove the above tag */\n\n"; // table $p = str_repeat(' ', max(2, (18 - strlen($this->table)))); $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $var = (substr(phpversion(), 0, 1) > 4) ? 'public' : 'var'; $var = !empty($options['generator_var_keyword']) ? $options['generator_var_keyword'] : $var; $body .= " {$var} \$__table = '{$this->table}'; {$p}// table name\n"; // if we are using the option database_{databasename} = dsn // then we should add var $_database = here // as database names may not always match.. if (empty($GLOBALS['_DB_DATAOBJECT']['CONFIG'])) { DB_DataObject::_loadConfig(); } // Only include the $_database property if the omit_database_var is unset or false if (isset($options["database_{$this->_database}"]) && empty($GLOBALS['_DB_DATAOBJECT']['CONFIG']['generator_omit_database_var'])) { $p = str_repeat(' ', max(2, (16 - strlen($this->_database)))); $body .= " {$var} \$_database = '{$this->_database}'; {$p}// database name (used with database_{*} config)\n"; } if (!empty($options['generator_novars'])) { $var = '//' . $var; } $defs = $this->_definitions[$this->table]; // show nice information! $connections = array(); $sets = array(); foreach ($defs as $t) { if (!strlen(trim($t->name))) { continue; } if (!preg_match('/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/', $t->name)) { echo "*****************************************************************\n" . "** WARNING COLUMN NAME UNUSABLE **\n" . "** Found column '{$t->name}', of type '{$t->type}' **\n" . "** Since this column name can't be converted to a php variable **\n" . "** name, and the whole idea of mapping would result in a mess **\n" . "** This column has been ignored... **\n" . "*****************************************************************\n"; continue; } $pad = str_repeat(' ', max(2, (30 - strlen($t->name)))); $length = empty($t->len) ? '' : '(' . $t->len . ')'; $flags = strlen($t->flags) ? (' ' . trim($t->flags)) : ''; $body .= " {$var} \${$t->name}; {$pad}// {$t->type}{$length}{$flags}\n"; // can not do set as PEAR::DB table info doesnt support it. //if (substr($t->Type,0,3) == "set") // $sets[$t->Field] = "array".substr($t->Type,3); $body .= $this->derivedHookVar($t, strlen($p)); } $body .= $this->derivedHookPostVar($defs); // THIS IS TOTALLY BORKED old FC creation // IT WILL BE REMOVED!!!!! in DataObjects 1.6 // grep -r __clone * to find all it's uses // and replace them with $x = clone($y); // due to the change in the PHP5 clone design. $static = 'static'; if (substr(phpversion(), 0, 1) < 5) { $body .= "\n"; $body .= " /* ZE2 compatibility trick*/\n"; $body .= " function __clone() { return \$this;}\n"; } // depricated - in here for BC... if (!empty($options['static_get'])) { // simple creation tools ! (static stuff!) $body .= "\n"; $body .= " /* Static get */\n"; $body .= " $static function staticGet(\$k,\$v=NULL) { " . "return DB_DataObject::staticGet('{$this->classname}',\$k,\$v = null); }\n"; } // generate getter and setter methods $body .= $this->_generateGetters($input); $body .= $this->_generateSetters($input); $body .= $this->_generateLinkMethods($input); /* theoretically there is scope here to introduce 'list' methods based up 'xxxx_up' column!!! for heiracitcal trees.. */ // set methods //foreach ($sets as $k=>$v) { // $kk = strtoupper($k); // $body .=" function getSets{$k}() { return {$v}; }\n"; //} if (!empty($options['generator_no_ini'])) { $def = $this->_generateDefinitionsTable(); // simplify this!? $body .= $this->_generateTableFunction($def['table']); $body .= $this->_generateKeysFunction($def['keys']); $body .= $this->_generateSequenceKeyFunction($def); $body .= $this->_generateDefaultsFunction($this->table, $def['table']); } elseif (!empty($options['generator_add_defaults'])) { // I dont really like doing it this way (adding another option) // but it helps on older projects. $def = $this->_generateDefinitionsTable(); // simplify this!? $body .= $this->_generateDefaultsFunction($this->table, $def['table']); } $body .= $this->derivedHookFunctions($input); $body .= "\n /* the code above is auto generated do not remove the tag below */"; $body .= "\n ###END_AUTOCODE\n"; // stubs.. if (!empty($options['generator_add_validate_stubs'])) { foreach ($defs as $t) { if (!strlen(trim($t->name))) { continue; } $validate_fname = 'validate' . $this->getMethodNameFromColumnName($t->name); // dont re-add it.. if (preg_match('/\s+function\s+' . $validate_fname . '\s*\(/i', $input)) { continue; } $body .= "\n function {$validate_fname}()\n {\n return false;\n }\n"; } } $foot .= "}\n"; $full = $head . $body . $foot; if (!$input) { return $full; } if (!preg_match('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n)/s', $input)) { return $full; } if (!preg_match('/(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', $input)) { return $full; } /* this will only replace extends DB_DataObject by default, unless use set generator_class_rewrite to ANY or a name*/ $class_rewrite = 'DB_DataObject'; $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); if (empty($options['generator_class_rewrite']) || !($class_rewrite = $options['generator_class_rewrite'])) { $class_rewrite = 'DB_DataObject'; } if ($class_rewrite == 'ANY') { $class_rewrite = '[a-z_]+'; } $input = preg_replace( '/(\n|\r\n)class\s*[a-z0-9_]+\s*extends\s*' . $class_rewrite . '\s*(\n|\r\n)\{(\n|\r\n)/si', "\nclass {$this->classname} extends {$this->_extends} \n{\n", $input ); $ret = preg_replace( '/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', $body, $input ); if (!strlen($ret)) { return (new PEAR)->raiseError( "PREG_REPLACE failed to replace body, - you probably need to set these in your php.ini\n" . "pcre.backtrack_limit=1000000\n" . "pcre.recursion_limit=1000000\n", null, PEAR_ERROR_DIE ); } return $ret; } /** * hook to add extra page-level (in terms of phpDocumentor) DocBlock * * called once for each class, use it add extra page-level docs * @access public * @return string added to class eg. functions. */ public function derivedHookPageLevelDocBlock() { return ''; } /** * hook to add extra doc block (in terms of phpDocumentor) to extend string * * called once for each class, use it add extra comments to extends * string (require_once...) * @access public * @return string added to class eg. functions. */ public function derivedHookExtendsDocBlock() { return ''; } /** * hook to add extra class level DocBlock (in terms of phpDocumentor) * * called once for each class, use it add extra comments to class * string (require_once...) * @access public * @return string added to class eg. functions. */ public function derivedHookClassDocBlock() { return ''; } /** * hook for var lines * called each time a var line is generated, override to add extra var * lines * * @param object t containing type,len,flags etc. from tableInfo call * @param int padding number of spaces * @access public * @return string added to class eg. functions. */ public function derivedHookVar(&$t, $padding) { // This is so derived generator classes can generate variabels // It MUST NOT be changed here!!! return ""; } /** * hook for after var lines ( * called at the end of the output of var line have generated, override to add extra var * lines * * @param array cols containing array of objects with type,len,flags etc. from tableInfo call * @access public * @return string added to class eg. functions. */ public function derivedHookPostVar($t) { // This is so derived generator classes can generate variabels // It MUST NOT be changed here!!! return ""; } /** * Generate getter methods for class definition * * @param string $input Existing class contents * @return string * @access public */ public function _generateGetters($input) { $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $getters = ''; // only generate if option is set to true if (empty($options['generate_getters'])) { return ''; } // remove auto-generated code from input to be able to check if the method exists outside of the auto-code $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input); $getters .= "\n\n"; $defs = $this->_definitions[$this->table]; // loop through properties and create getter methods foreach ($defs as $t) { // build mehtod name $methodName = 'get' . $this->getMethodNameFromColumnName($t->name); if (!strlen(trim($t->name)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) { continue; } $getters .= " /**\n"; $getters .= " * Getter for \${$t->name}\n"; $getters .= " *\n"; $getters .= (stristr($t->flags, 'multiple_key')) ? " * @return object\n" : " * @return {$t->type}\n"; $getters .= " * @access public\n"; $getters .= " */\n"; $getters .= (substr(phpversion(), 0, 1) > 4) ? ' public ' : ' '; $getters .= "function $methodName() {\n"; $getters .= " return \$this->{$t->name};\n"; $getters .= " }\n\n"; } return $getters; } /** * Convert a column name into a method name (usually prefixed by get/set/validateXXXXX) * * @access public * @param $col * @return string method name; */ public function getMethodNameFromColumnName($col) { return ucfirst($col); } /** * Generate setter methods for class definition * * @param string Existing class contents * @return string * @access public */ public function _generateSetters($input) { $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $setters = ''; // only generate if option is set to true if (empty($options['generate_setters'])) { return ''; } // remove auto-generated code from input to be able to check if the method exists outside of the auto-code $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input); $setters .= "\n"; $defs = $this->_definitions[$this->table]; // loop through properties and create setter methods foreach ($defs as $t) { // build mehtod name $methodName = 'set' . $this->getMethodNameFromColumnName($t->name); if (!strlen(trim($t->name)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) { continue; } $setters .= " /**\n"; $setters .= " * Setter for \${$t->name}\n"; $setters .= " *\n"; $setters .= " * @param mixed input value\n"; $setters .= " * @access public\n"; $setters .= " */\n"; $setters .= (substr(phpversion(), 0, 1) > 4) ? ' public ' : ' '; $setters .= "function $methodName(\$value) {\n"; $setters .= " \$this->{$t->name} = \$value;\n"; $setters .= " }\n\n"; } return $setters; } /** * Generate link setter/getter methods for class definition * * @param string Existing class contents * @return string * @access public */ public function _generateLinkMethods($input) { $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $setters = ''; // only generate if option is set to true // generate_link_methods true:: if (empty($options['generate_link_methods'])) { //echo "skip lm? - not set"; return ''; } if (empty($this->_fkeys)) { // echo "skip lm? - fkyes empty"; return ''; } if (empty($this->_fkeys[$this->table])) { //echo "skip lm? - no fkeys for {$this->table}"; return ''; } // remove auto-generated code from input to be able to check if the method exists outside of the auto-code $input = preg_replace('/(\n|\r\n)\s*###START_AUTOCODE(\n|\r\n).*(\n|\r\n)\s*###END_AUTOCODE(\n|\r\n)/s', '', $input); $setters .= "\n"; $defs = $this->_fkeys[$this->table]; // $fk[$this->table][$tref[1]] = $tref[2] . ":" . $tref[3]; // loop through properties and create setter methods foreach ($defs as $k => $info) { // build mehtod name $methodName = is_callable($options['generate_link_methods']) ? $options['generate_link_methods']($k) : $k; if (!strlen(trim($k)) || preg_match("/function[\s]+[&]?$methodName\(/i", $input)) { continue; } $setters .= " /**\n"; $setters .= " * Getter / Setter for \${$k}\n"; $setters .= " *\n"; $setters .= " * @param mixed (optional) value to assign\n"; $setters .= " * @access public\n"; $setters .= " */\n"; $setters .= (substr(phpversion(), 0, 1) > 4) ? ' public ' : ' '; $setters .= "function $methodName() {\n"; $setters .= " return \$this->link('$k', func_get_args());\n"; $setters .= " }\n\n"; } return $setters; } /** * Generate table Function - used when generator_no_ini is set. * * @param array table array. * @return string * @access public */ public function _generateTableFunction($def) { $defines = explode(',', 'INT,STR,DATE,TIME,BOOL,TXT,BLOB,NOTNULL,MYSQLTIMESTAMP'); $ret = "\n" . " function table()\n" . " {\n" . " return array(\n"; foreach ($def as $k => $v) { $str = '0'; foreach ($defines as $dn) { if ($v & constant('DB_DATAOBJECT_' . $dn)) { $str .= ' + DB_DATAOBJECT_' . $dn; } } if (strlen($str) > 1) { $str = substr($str, 3); // strip the 0 + } // hopefully addslashes is good enough here!!! $ret .= ' \'' . addslashes($k) . '\' => ' . $str . ",\n"; } return $ret . " );\n" . " }\n"; } /** * Generate keys Function - used generator_no_ini is set. * * @param array keys array. * @return string * @access public */ public function _generateKeysFunction($def) { $ret = "\n" . " function keys()\n" . " {\n" . " return array("; foreach ($def as $k => $type) { // hopefully addslashes is good enough here!!! $ret .= '\'' . addslashes($k) . '\', '; } $ret = preg_replace('#, $#', '', $ret); return $ret . ");\n" . " }\n"; } /** * Generate sequenceKey Function - used generator_no_ini is set. * * @param array table and key definition. * @return string * @access public */ public function _generateSequenceKeyFunction($def) { //print_r($def); // DB_DataObject::debugLevel(5); global $_DB_DATAOBJECT; // print_r($def); $dbtype = $_DB_DATAOBJECT['CONNECTIONS'][$this->_database_dsn_md5]->dsn['phptype']; $realkeys = $def['keys']; $keys = array_keys($realkeys); $usekey = isset($keys[0]) ? $keys[0] : false; $table = $def['table']; $seqname = false; $ar = array(false, false, false); if ($usekey !== false) { if (!empty($_DB_DATAOBJECT['CONFIG']['sequence_' . $this->__table])) { $usekey = $_DB_DATAOBJECT['CONFIG']['sequence_' . $this->__table]; if (strpos($usekey, ':') !== false) { list($usekey, $seqname) = explode(':', $usekey); } } if (in_array($dbtype, array('mysql', 'mysqli', 'mssql', 'ifx')) && ($table[$usekey] & DB_DATAOBJECT_INT) && isset($realkeys[$usekey]) && ($realkeys[$usekey] == 'N') ) { // use native sequence keys. $ar = array($usekey, true, $seqname); } else { // use generated sequence keys.. if ($table[$usekey] & DB_DATAOBJECT_INT) { $ar = array($usekey, false, $seqname); } } } $ret = "\n" . " function sequenceKey() // keyname, use native, native name\n" . " {\n" . " return array("; foreach ($ar as $v) { switch (gettype($v)) { case 'boolean': $ret .= ($v ? 'true' : 'false') . ', '; break; case 'string': $ret .= "'" . $v . "', "; break; default: // eak $ret .= "null, "; } } $ret = preg_replace('#, $#', '', $ret); return $ret . ");\n" . " }\n"; } /** * Generate defaults Function - used generator_add_defaults or generator_no_ini is set. * Only supports mysql and mysqli ... welcome ideas for more.. * * * @param $table * @param $defs * @return string * @access public */ public function _generateDefaultsFunction($table, $defs) { $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; if (!in_array($__DB->phptype, array('mysql', 'mysqli'))) { return null; // cant handle non-mysql introspection for defaults. } $options = (new PEAR)->getStaticProperty('DB_DataObject', 'options'); $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; $method = $db_driver == 'DB' ? 'getAll' : 'queryAll'; if ($db_driver == 'DB') { $res = $__DB->$method('DESCRIBE ' . $table, DB_FETCHMODE_ASSOC); } else { $res = $__DB->$method('DESCRIBE ' . $table, MDB2_FETCHMODE_ASSOC); } $defaults = array(); foreach ($res as $ar) { // this is initially very dumb... -> and it may mess up.. $type = $defs[$ar['Field']]; switch (true) { case (is_null($ar['Default'])): $defaults[$ar['Field']] = 'null'; break; case ($type & DB_DATAOBJECT_DATE): case ($type & DB_DATAOBJECT_TIME): case ($type & DB_DATAOBJECT_MYSQLTIMESTAMP): // not supported yet.. break; case ($type & DB_DATAOBJECT_BOOL): $defaults[$ar['Field']] = (int)(boolean)$ar['Default']; break; case ($type & DB_DATAOBJECT_STR): $defaults[$ar['Field']] = "'" . addslashes($ar['Default']) . "'"; break; default: // hopefully eveything else... - numbers etc. if (!strlen($ar['Default'])) { continue; } if (is_numeric($ar['Default'])) { $defaults[$ar['Field']] = $ar['Default']; } break; } //var_dump(array($ar['Field'], $ar['Default'], $defaults[$ar['Field']])); } if (empty($defaults)) { return null; } $ret = "\n" . " function defaults() // column default values \n" . " {\n" . " return array(\n"; foreach ($defaults as $k => $v) { $ret .= ' \'' . addslashes($k) . '\' => ' . $v . ",\n"; } return $ret . " );\n" . " }\n"; } /** * hook to add extra methods to all classes * * called once for each class, use with $this->table and * $this->_definitions[$this->table], to get data out of the current table, * use it to add extra methods to the default classes. * * @access public * @param string $input * @return string added to class eg. functions. */ public function derivedHookFunctions($input = "") { // This is so derived generator classes can generate functions // It MUST NOT be changed here!!! return ""; } /** * * /** * getProxyFull - create a class definition on the fly and instantate it.. * * similar to generated files - but also evals the class definitoin code. * * * @param string database name * @param string table name of table to create proxy for. * * * @return object Instance of class. or PEAR Error * @access public */ public function getProxyFull($database, $table) { if ($err = $this->fillTableSchema($database, $table)) { return $err; } $options = &(new PEAR)->getStaticProperty('DB_DataObject', 'options'); $class_prefix = empty($options['class_prefix']) ? '' : $options['class_prefix']; $this->_extends = empty($options['extends']) ? $this->_extends : $options['extends']; $this->_extendsFile = !isset($options['extends_location']) ? $this->_extendsFile : $options['extends_location']; $classname = $this->classname = $this->getClassNameFromTableName($this->table); $out = $this->_generateClassTable(); //echo $out; eval('?>' . $out); return new $classname; } /** * fillTableSchema - set the database schema on the fly * * * * @param string database name * @param string table name of table to create schema info for * * @return none|object|PEAR * @access public */ public function fillTableSchema($database, $table) { global $_DB_DATAOBJECT; // a little bit of sanity testing. if ((false !== strpos($database, "'")) || (false !== strpos($database, ";"))) { return (new PEAR)->raiseError("Error: Database name contains a quote or semi-colon", null, PEAR_ERROR_DIE); } $this->_database = $database; $this->_connect(); $table = trim($table); // a little bit of sanity testing. if ((false !== strpos($table, "'")) || (false !== strpos($table, ";"))) { return (new PEAR)->raiseError("Error: Table contains a quote or semi-colon", null, PEAR_ERROR_DIE); } $__DB = &$GLOBALS['_DB_DATAOBJECT']['CONNECTIONS'][$this->_database_dsn_md5]; $options = (new PEAR)->getStaticProperty('DB_DataObject', 'options'); $db_driver = empty($options['db_driver']) ? 'DB' : $options['db_driver']; $is_MDB2 = ($db_driver != 'DB') ? true : false; if (!$is_MDB2) { // try getting a list of schema tables first. (postgres) $__DB->expectError(DB_ERROR_UNSUPPORTED); $this->tables = $__DB->getListOf('schema.tables'); $__DB->popExpect(); } else { /** * set portability and some modules to fetch the informations */ $__DB->setOption('portability', MDB2_PORTABILITY_ALL ^ MDB2_PORTABILITY_FIX_CASE); $__DB->loadModule('Manager'); $__DB->loadModule('Reverse'); } $quotedTable = !empty($options['quote_identifiers_tableinfo']) ? $__DB->quoteIdentifier($table) : $table; if (!$is_MDB2) { $defs = $__DB->tableInfo($quotedTable); } else { $defs = $__DB->reverse->tableInfo($quotedTable); if ((new PEAR)->isError($defs)) { return $defs; } foreach ($defs as $k => $v) { if (!isset($defs[$k]['length'])) { continue; } $defs[$k]['len'] = $defs[$k]['length']; } } if ((new PEAR)->isError($defs)) { return $defs; } if (@$_DB_DATAOBJECT['CONFIG']['debug'] > 2) { $this->debug("getting def for $database/$table", 'fillTable'); $this->debug(print_r($defs, true), 'defs'); } // cast all definitions to objects - as we deal with that better. foreach ($defs as $def) { if (is_array($def)) { $this->_definitions[$table][] = (object)$def; } } $this->table = trim($table); $ret = $this->_generateDefinitionsTable(); $_DB_DATAOBJECT['INI'][$database][$table] = $ret['table']; $_DB_DATAOBJECT['INI'][$database][$table . '__keys'] = $ret['keys']; return false; } }