PDO/DataObject/Generator.php
authorAlan Knowles <alan@roojs.com>
Wed, 18 Jan 2017 02:21:22 +0000 (10:21 +0800)
committerAlan Knowles <alan@roojs.com>
Wed, 18 Jan 2017 02:21:22 +0000 (10:21 +0800)
PDO/DataObject/Generator.php [new file with mode: 0755]

diff --git a/PDO/DataObject/Generator.php b/PDO/DataObject/Generator.php
new file mode 100755 (executable)
index 0000000..cef198e
--- /dev/null
@@ -0,0 +1,745 @@
+<?php
+/**
+ * Generation tools for PDO_DataObject
+ *
+ * For PHP versions  5 and 7
+ * 
+ * 
+ * Copyright (c) 2016 Alan Knowles
+ * 
+ * This program is free software: you can redistribute it and/or modify  
+ * it under the terms of the GNU Lesser General Public License as   
+ * published by the Free Software Foundation, version 3.
+ *
+ * This program is distributed in the hope that it will be useful, but 
+ * WITHOUT ANY WARRANTY; without even the implied warranty of 
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 
+ * Lesser General Lesser Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *  
+ * @category   Database
+ * @package    PDO_DataObject
+ * @author     Alan Knowles <alan@roojs.com>
+ * @copyright  2016 Alan Knowles
+ * @license    https://www.gnu.org/licenses/lgpl-3.0.en.html  LGPL 3
+ * @version    1.0
+ * @link       https://github.com/roojs/PDO_DataObject
+ */
+  
+ /*
+ * Security Notes:
+ *   This class may use 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 _$options
+ * [PDO_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 PDO_DataObject)
+ * generator_class_rewrite = ANY|specific_name   // default is PDO_DataObject
+ *
+ */
+
+/**
+ * Needed classes
+ * We lazy load here, due to problems with the tests not setting up include path correctly.
+ * FIXME!
+ */
+class_exists('PDO_DataObject') ? '' : require_once 'PDO/DataObject.php';
+/**
+ * Generator class
+ *
+ * @package PDO_DataObject
+ */
+class PDO_DataObject_Generator extends PDO_DataObject
+{
+    
+    
+    
+     /* ---------------- ---------------- static  -------------------------------- */
+    /**
+     * Configuration - use config() to access this.
+     *
+     * @access  private
+     * @static
+     * @var     array
+     */
+    private static $config = array(
+            
+         
+        
+        // ---- Generator
+              
+            'build_views' => false,
+                // for postgres, you can build dataobjects for views as well
+                // you can set this to 'schema.views' to extract views with schema information
+                // I believe  postgres also supports updating on views (nice feature)
+                // *** NOTE *** You will have to manually define keys() / sequenceKeys()
+                // As the generator can not recognize these automatically
+                
+            'strip_schema' => true,
+                //     postgres has a wierd concept of schema's which end up prefixed to
+                //     the list of tables. - this makes a mess of class/schema generation
+                //     setting this to '', makes the generator strip the schema from the table name.
+                //  now supports regex (if you set it to a regex it will strip schema of matched names)
+                //  for example '/^public\./'
+            'embed_schema' => false,
+                // (True) will generate the methods tableColumns() ,keys(), sequenceKeys() and defaults()
+                // methods in the generated classes 
+                // and not generate any ini file to describe the table.
+                
+            'extends_class' => 'PDO_DataObject',
+                // what class do the generated classes extend?
+            'extends_class_location' => 'PDO/DataObject.php',
+                // what file is the extended class in.                
+        
+            'generate_links' => false,
+                // generate .link.ini files based on introspecting the database.
+        
+        // advanced customization..
+            'hook' => 'PDO_DataObject_Generator_Hooks',
+                // class for hooks code (used to be derivedHook****)
+                // allows custom generation of PHP code.
+                // can be class name or object
+            'table_gen_class' => 'PDO_DataObject_Generator_Table',
+                // class for table parsing/ generator
+                // allows for more custom generaton.
+                // if you use a custom class - you must include it before loading..
+            'var_keyword' => 'public',
+                // var|public  - (or private if you want to break things)
+                // The variable prefix that is used when class properties are created
+                //  the default is public 
+            'add_database_nickname' => false,
+                // add the line public $_database_nickname = .... (disabled by default)
+            'no_column_vars' => false,
+                // (True) prevents writing of private/var's so you can overload get/set 
+                // note: this has the downside of making code less clear... (alot of magic!!)
+            'setters' => false,
+               // (true) will generate setXXXX() methods for you.
+            'getters' => false,
+               // (true) will generate getXXXX() methods for you.
+            'add_defaults' => false,
+                // add a method defaults() - that returns the default value for each column.
+            'link_methods'  =>false,
+                // (true|callable) will create the wrappers around link()
+                // => function($k) { return $k; } // to munge the column name into a method name.
+                // Only likely to work with with mysql / mysqli / postgres  at present. ?? maybe sqlite?
+            'secondary_key_match' => 'primary|unique',
+                // if a column is auto-increment or nextval() - then it's determined to be a sequence key
+                // if it's only primary or unique - then it's assumed to be an index, but using emulated sequences keys.
+            'include_regex' => false,
+                // regex to match table names = if set, then only table names matching will be generated
+            'exclude_regex' => false,
+                // regex to match table names = if set, then matching tables will not be generated
+                
+    );
+      /**
+     * Set/get the generator configuration...
+     * Usage:
+     *
+     * Fetch the current config.
+     * $cfg = PDO_DataObject_Generator::config(); 
+     *
+     * SET a configuration value. (returns old value.)
+     * $old = PDO_DataObject_Generator::config('schema_location', '');  
+     *
+     * GET a specific value ** does not do this directly to stop errors...
+     * somevar = PDO_DataObject_Generator::config()['schema_location'];  
+     *
+     * SET multiple values (returns 'old' configuration)
+     * $old_array = PDO_DataObject_Generator::config( array( 'schema_location' => '' ));
+     * 
+     * 
+     * @param   array  key/value 
+     * @param   mixed value 
+     * @static
+     * @access  public
+     * @return - the current config (or previous value/config) 
+     */
+     
+    static function config($cfg_in = array(), $value=false) 
+    {
+         
+        if (!func_num_args()) {
+            return self::$config;
+        }
+        
+        if (!is_array($cfg_in) && func_num_args() < 2) {
+            // one arg = not an array..
+            (new PDO_DataObject())->raise("Invalid Call to config should be string+anther value or array",
+                              self::ERROR_INVALIDARGS);
+        }
+        
+        $old = self::$config;
+        
+        $cfg = $cfg_in;
+        
+        if (func_num_args() > 1) {
+            // two args..
+            if (!is_string($cfg_in)) {
+                (new PDO_DataObject())->raise("Invalid Call to config should be string+anther value or array",
+                              self::ERROR_INVALIDARGS);    
+            }
+            
+            $k = $cfg_in;
+            $cfg = array();
+            $cfg[$k] = $value;
+        } 
+          
+        foreach ($cfg as $k=>$v) {
+            if (!isset(self::$config[$k])) {
+                (new PDO_DataObject())->raise("Invalid Configuration setting : $k",
+                        self::ERROR_INVALIDCONFIG);
+            }
+            self::$config[$k] = $v;
+        }
+        
+        
+        return is_array($cfg_in) ? $old : $old[$cfg_in];
+    }
+    /**
+     * Associate Array of table names => Table Objects
+     *
+     * @var array
+     * @access private
+     */
+    var $tables = array();
+    
+    
+    /**
+     * Constructor
+     *  - although it extends PDO_DataObject, the ctor behavior is slightly different
+     *    it expects a database nickname (optiontally)
+     * @param string database nickname
+     */
+    function __construct($cfg = false)
+    {
+        if ($cfg !== false) {
+            $this->databaseNickname($cfg);
+        }
+        
+        $hook = self::$config['hook'];
+        if (is_object($hook)) {
+            $this->hook = $hook;
+            return;
+        }
+        if ($hook == 'PDO_DataObject_Generator_Hooks') {
+            class_exists($hook) ? '' :
+                require_once 'PDO/DataObject/Generator/Hooks.php';
+        }
+        if (!class_exists($hook)) {
+            $this->raise("Hook class '{$hook}' does not exist - please include it or use an autoloader");
+        }
+        $this->hook = new $hook($this);
+    }
+    
+    
+    /**
+     * The 'starter' = call this to start the process
+     *
+     * @access  public
+     * @return  none
+     */
+    function start()
+    {
+        $options = PDO_DataObject::config();
+        
+        $databases = array();
+        if (!empty($options['databases'])) {
+            $databases  = $options['databases'];
+        }
+        
+        
+        if (!empty($options['database'])) {
+            // ctor without table...
+            $do = new PDO_DataObject();
+            $dname = $do->PDO()->database_nickname;
+            print_R($do->PDO());exit;
+            
+            if (!isset($database[$dname])){
+                $databases[$dname] = $options['database'];
+            }
+        }
+
+        
+        foreach($databases as $databasename => $database) {
+            if (!$database) {
+                continue;
+            }
+            $this->debug("CREATING FOR $databasename\n",__FUNCTION__,1);
+            $class = get_class($this);
+            $t = new $class();
+            $t->_database_dsn = $database;
+            $t->databaseNickname( $databasename);
+            
+            $t->readTableList();
+
+            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");
+    }
+
+  /**
+     *  
+     * 'proxy' version of databaseStructure - this is not so 'speed sensitive'
+     * only used when
+     * b) proxy is set..
+     
+    *
+     *  - set's the structure.. and the links data..
+          
+     *
+     * obviously you dont have to use ini files.. (just return array similar to ini files..)
+     *  
+     * It should append to the table structure array 
+     *
+     *     
+     * @param optional string  name of database to assign / read
+     * @param optional array   structure of database, and keys
+     * @param optional array  table links
+     * @return (varies) - depends if you are setting or getting...
+     */
+    
+    function databaseStructureProxy($database, $table = false)
+    {
+        
+        $this->databaseNickname( $database );
+        $this->PDO();
+        $this->readTableList();
+         
+            // prevent recursion...
+            
+        $old = PDO_DataObject::config('proxy', false);
+        $ret = $this->databaseStructure(); 
+        PDO_DataObject::config('proxy', $old);
+        return $ret;
+            // databaseStructure('mydb',   array(.... schema....), array( ... links')
+         
+            // will not get here....
+    }    
+    
+     /**
+     * create an instance of introspection. 
+     * - manual set
+     * - proxy
+     * - ini_****
+     */
+    function introspection()
+    {
+        
+        $type  = $this->PDO()->getAttribute(PDO::ATTR_DRIVER_NAME);
+        if (empty($type)) {
+            throw new Exception("could not work out database type");
+        }
+        $class = 'PDO_DataObject_Introspection_'. $type;
+        class_exists($class)  ? '' : require_once 'PDO/DataObject/Introspection/'. $type. '.php';
+        $this->debug("Creating Introspection for $class", "_introspection");
+        return new $class( clone($this) ); /// clone so we can run multipel queries?
+       
+    }
+
+    
+   
+    /**
+     * Build a list of tables and definitions.;
+     * and store it in $this->tables and $this->_definitions[tablename];
+     *
+     * @access  public
+     * @return  Array of tables
+     */
+    function readTableList()
+    {
+        $options = self::config();
+        
+        $pdo = $this->PDO();
+        $io  = $this->introspection();
+        
+        $tables = array();
+        
+        // try schema first...
+        try {
+            $tables = $io->getListOf('schema.tables');
+        } catch (Exception $e) {     
+        }
+        
+        if (empty($this->tables)) {
+            $tables = $io->getListOf('tables');
+        }
+        
+        // build views as well if asked to.
+        if (!empty($options['build_views'])) {
+            $views =$io->getListOf(
+                    is_string($options['build_views']) ?
+                                $options['build_views'] : 'views'
+            );
+            
+            $tables = array_merge ($tables, $views);
+        }
+       
+        
+
+        // declare a temporary table to be filled with matching tables names
+        $tmp_table = array();
+
+
+        foreach($tables as $table) {
+            if ($options['include_regex'] &&
+                    !preg_match($options['include_regex'],$table)) {
+                $this->debug("SKIPPING (include_regex) : $table", __FUNCTION__,1);
+                continue;
+            } 
+            
+            if ($options['exclude_regex'] &&
+                    preg_match($options['exclude_regex'],$table)) {
+                $this->debug("SKIPPING (exclude_regex) : $table", __FUNCTION__,1);
+                continue;
+            }
+            
+            $strip = $options['strip_schema'];
+            $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");
+            
+            // we do not quote table - as these are now internal methods - and it is done by the introspection classes 
+            $this->tables[$table] = $this->newTable($table);
+            
+            
+             
+
+
+        }
+        return array_keys($this->tables);
+         
+        //print_r($this->_definitions);
+    }
+    
+    
+    /**
+     * Create an instance of the table class (which can be specified in config[table_gen_class])
+     *
+     * 
+     *
+     */
+    function newTable($name)
+    {
+        $tcls = self::$config['table_gen_class'];
+        
+        if ($tcls == 'PDO_DataObject_Generator_Table') {
+            class_exists($tcls) ? '' :
+                require_once 'PDO/DataObject/Generator/Table.php';
+        }
+        if (!class_exists($tcls)) {
+            $this->raise("Table class '{$tcls}' does not exist - please include it or use an autoloader");
+        }
+        return new $tcls($this,$name);
+    }
+    /**
+     * fetch the content of '.ini' files with database schema in them.
+     *
+     * @access  public
+     * @return  none
+     */    
+
+    function toIni()
+    {
+        $out = '';
+        foreach($this->tables as $table) {
+            $out .= $table->toIniString();
+        }
+        return $out;
+    }
+
+    /**
+     * Create '.ini' files with database schema in them.
+     *
+     * @access  public
+     * @return  none
+     */
+    function generateIni()
+    {
+        $this->debug("Generating Definitions INI file:        ");
+        if (!$this->tables) {
+            $this->debug("-- NO TABLES -- \n");
+            return;
+        }
+
+        $options = PDO_DataObject::config();
+
+        
+        if (empty($options['schema_location']) ) {
+            return;
+        }
+        
+        if (!empty($options['generator_no_ini'])) { // built in ini files..
+            return;
+        }
+        
+        $out = $this->toIni();
+
+        $this->PDO();
+        // dont generate a schema if location is not set
+        // it's created on the fly!
+
+        // where to generate the schema...
+        $base = $options['schema_location'];
+        
+        if (is_array($base)) {
+            if (!isset($base[$this->_database_nickname])) {
+                $this->raise("Could not find schema location from config[schema_location] - array but no matching database",
+                    PDO_DataObject::ERROR_INVALIDCONFIG);
+            }
+            $base =  explode(PATH_SEPARATOR, $base[$this->_database_nickname])[0]; // get the first path...
+
+            $file = $base[$this->_database_nickname];
+        } else {
+            $base =  explode(PATH_SEPARATOR, $options['schema_location'])[0]; // get the first path...
+
+            $file = "{$base}/{$this->_database_nickname}.ini";
+        }
+        
+       
+        $this->debug("Writing ini as {$file}\n");
+        //touch($file);
+        $tmpname = tempnam(session_save_path(),'PDO_DataObject_');
+        //print_r($this->_newConfig);
+        $fh = fopen($tmpname,'w');
+        if (!$fh) {
+            return $this->raise(
+                "Failed to create temporary file: $tmpname\n".
+                "make sure session.save_path is set and is writable\n"
+                ,null);
+        }
+        fwrite($fh,$out);
+        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 (!file_exists(dirname($file))) {
+            mkdir(dirname($file), $perms, true);
+        }
+        if (!@rename($tmpname, $file)) { 
+            unlink($file); 
+            rename($tmpname, $file);
+        }
+        chmod($file,$perms);
+
+
+    }
+    
+    function toLinksIni()
+    {
+        $out = '';
+        foreach($this->tables as $tn=>$table) {
+            $out .= $table->toLinksIni();
+        }
+        return $out;
+
+        
+    }
+    
+    /**
+     * 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 
+     */
+    function generateForeignKeys() 
+    {
+        
+        if (!self::$config['generate_links']) {
+            return false;
+        }
+        $type  = $this->PDO()->getAttribute(PDO::ATTR_DRIVER_NAME);
+
+        
+        if (!in_array($type  , array('mysql', 'mysqli', 'pgsql'))) {
+            return $this->raise("config[generate_links] only works currently with pgsql and mysql");
+            
+        }
+        $this->debug("generateForeignKeys: Start");
+        
+        $out = $this->toLinksIni(); 
+
+        $options = PDO_DataObject::config();
+
+ // where to generate the schema...
+        $base = $options['schema_location'];
+        
+        if (is_array($base)) {
+            if (!isset($base[$this->_database_nickname])) {
+                $this->raise("Could not find schema location from config[schema_location] - array but no matching database",
+                    PDO_DataObject::ERROR_INVALIDCONFIG);
+            }
+            $base =  explode(PATH_SEPARATOR, $base[$this->_database_nickname])[0]; // get the first path...
+
+            $file =  preg_replace('/\.ini/','.links.ini' , $base[$this->_database_nickname]);
+        } else {
+            $base =  explode(PATH_SEPARATOR, $options['schema_location'])[0]; // get the first path...
+
+            $file = "{$base}/{$this->_database_nickname}.links.ini";
+        }
+
+      
+
+        $this->debug("Writing ini as {$file}\n");
+        
+        //touch($file); // not sure why this is needed?
+        $tmpname = tempnam(session_save_path(),'PDO_DataObject_');
+       
+        $fh = fopen($tmpname,'w');
+        if (!$fh) {
+            return $this->raise(
+                "Failed to create temporary file: $tmpname\n".
+                "make sure session.save_path is set and is writable\n"
+                ,PDO_DataObject::ERROR_INVALIDCONFIG);
+        }
+        fwrite($fh,$out);
+        fclose($fh);
+        
+        $perms = file_exists($file) ? fileperms($file) : 0755;
+
+        if (!file_exists(dirname($file))) {
+            mkdir(dirname($file),$perms, true);
+        }
+        
+        // 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);
+    }
+
+      
+    function toPhp($tablename)
+    {
+        return  $this->tables[$tablename]->toPhp('');
+    
+        
+    }
+    
+    
+    
+    /*
+     * building the class files
+     * for each of the tables output a file!
+     */
+    function generatePhp()
+    {
+        //echo "Generating Class files:        \n";
+        $options = self::config();
+        
+
+        foreach($this->tables as $table) {
+            
+            
+            $cn = $table->toPhpClassName();
+            $fn = $table->toPhpFileName();
+            
+            $oldcontents = '';
+            if (file_exists($fn)) {
+                $oldcontents = file_get_contents($fn);
+            }
+            
+            $out = $table->toPhp($oldcontents);
+            
+            $this->debug( "writing $cn\n");
+            $tmpname = tempnam(session_save_path(),'PDO_DataObject_');
+       
+            $fh = fopen($tmpname, "w");
+            if (!$fh) {
+                return $this->raise(
+                    "Failed to create temporary file: $tmpname\n".
+                    "make sure session.save_path is set and is writable\n"
+                    ,null);
+            }
+            fputs($fh,$out);
+            fclose($fh);
+            $perms = file_exists($fn) ? fileperms($fn) : 0755;
+            if (!file_exists(dirname($fn))) {
+                mkdir(dirname($fn),$perms, true);
+            }
+            
+            // windows can fail doing this. - not a perfect solution but otherwise it's getting really kludgy..
+            if (!@rename($tmpname, $fn)) {
+                unlink($fn); 
+                rename($tmpname, $fn);
+            }
+            
+            chmod($fn, $perms);
+        }
+        //echo $out;
+    }
+   
+    
+    /**
+
+    /**
+    * 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
+    */
+    function getProxyFull($database,$table) 
+    {
+        
+        $this->_database = $database;
+        // might produce error??
+        if (!isset($this->tables[$table])) {
+            $this->tables[$table] = $this->newTable($table);
+        }
+        $tbl  = $this->tables[$table] ;
+        
+        $classname = $tbl->toPhpClassName();
+        $out = $tbl->toPhp('');
+        //echo $out;
+        eval('?>'.$out);
+        return new $classname;
+        
+    }
+     
+     
+    
+}