*/ // dblayeroo.php /** Permit abstraction on the differents SQL databases available */ class dblayeroo { /** The table name to use */ private $table = null; /** The tableprefix text to prepend to table name (Should finish by _) * Just allow chars ! */ private $tableprefix = ""; /** The fields with the definition of type, and special parameters */ private $fields = array (); /** The primary field */ private $primary = null; /** An array to define the unique fields (or array of unique fields) */ private $unique = null; /** An array to define the foreign keys of the field */ private $foreign = array (); /** Debug of the SQL */ private $debug = FALSE; /** The connecting DSN */ private $dsn = null; /** The driver to use */ private $driver = null; /** The field group delimiter */ private $sep = ""; /** Titles */ private $titles = array (); /** Limit to one instance of the connection to the same database */ // Based on an idea of http://tonylandis.com/php/php5-pdo-singleton-class/ private static $instance = array (); /** Connection to the database engine * See http://fr2.php.net/manual/en/pdo.construct.php for the $dsn format * @param string $dsn PDO Data Source Name * @param string|null $username Username to connect * @param string|null $password Password to connect * @param string|null $driver_options Driver options to the database */ public function __construct ($dsn, $username=null, $password=null, $driver_options=null) /* {{{ */ { $driver = @explode (":", $dsn); if (! isset ($driver[0])) $this->DBException (dgettext ("domframework", "No valid DSN provided")); $driver[0] = strtolower ($driver[0]); if (! in_array ($driver[0], pdo_drivers ())) $this->DBException (sprintf (dgettext ("domframework", "Driver PDO '%s' not available in PHP"), $driver[0])); $this->driver = $driver[0]; // Force specifics initialisations $this->dsn = $dsn; switch ($driver[0]) { case "sqlite": // Look at the right to write in database and in the directory $file = substr ($dsn, 7); if (! is_writeable (dirname ($file))) $this->DBException (dgettext ("domframework", "The directory for SQLite database is write protected")); if (file_exists ($file) && ! is_writeable ($file)) $this->DBException (dgettext ("domframework", "The SQLite database file is write protected")); if (function_exists ("posix_getuid") && file_exists ($file) && fileowner ($file) === posix_getuid ()) chmod ($file, 0666); // Print the instances of PDO objects stored : // var_dump (self::$instance); if (! array_key_exists ($this->dsn, self::$instance)) { $this->debugLog ("CONNECT TO SQLite DATABASE"); try { self::$instance[$this->dsn] = new \PDO ($dsn, $username, $password, $driver_options); self::$instance[$this->dsn]->setAttribute (\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); } catch (Exception $e) { $this->DBException ("PDO error : ".$e->getMessage ()); } } // Force ForeignKeys support (disabled by default) self::$instance[$this->dsn]->exec ("PRAGMA foreign_keys = ON"); $this->sep = "`"; break; case "mysql": if (! array_key_exists ($this->dsn, self::$instance)) { $this->debugLog ("CONNECT TO MySQL DATABASE"); try { $driver_options[\PDO::MYSQL_ATTR_FOUND_ROWS] = 1; self::$instance[$this->dsn] = new \PDO ($dsn, $username, $password, $driver_options); self::$instance[$this->dsn]->setAttribute (\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); } catch (Exception $e) { $this->DBException ("PDO error : ".$e->getMessage ()); } } // Set the coding to UTF8 self::$instance[$this->dsn]->exec ("SET CHARACTER SET utf8"); $this->sep = "`"; break; case "pgsql": if (! array_key_exists ($this->dsn, self::$instance)) { $this->debugLog ("CONNECT TO PGSQL DATABASE"); try { self::$instance[$this->dsn] = new \PDO ($dsn, $username, $password, $driver_options); self::$instance[$this->dsn]->setAttribute (\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); } catch (Exception $e) { $this->DBException ("PDO error : ".$e->getMessage ()); } } // Set the coding to UTF8 self::$instance[$this->dsn]->exec ("SET NAMES 'utf8'"); $this->sep = "\""; break; default: $this->DBException (dgettext ("domframework", "Unknown PDO driver provided")); } return self::$instance[$this->dsn]; } /* }}} */ /** This function disconnect the database. It is normally only used in phpunit * unit tests */ public function disconnect () /* {{{ */ { unset (self::$instance[$this->dsn]); } /* }}} */ /** Start a new Transaction */ public function beginTransaction () /* {{{ */ { return self::$instance[$this->dsn]->beginTransaction (); } /* }}} */ /** Commit (validate) a transaction */ public function commit () /* {{{ */ { return self::$instance[$this->dsn]->commit (); } /* }}} */ /** RollBack a transaction */ public function rollback () /* {{{ */ { return self::$instance[$this->dsn]->rollback (); } /* }}} */ /** Return the connected database name from DSN used to connect */ public function databasename () /* {{{ */ { if ($this->sep === "") $this->DBException (dgettext ("domframework", "Database not connected")); $vals = explode (";", substr (strstr ($this->dsn, ":"), 1)); $dsnExplode = array (); foreach ($vals as $val) { @list ($k, $v) = explode ("=", $val); $dsnExplode[$k] = $v; } if (isset ($dsnExplode["dbname"])) return $dsnExplode["dbname"]; return NULL; } /* }}} */ /** Return all the tables available in the database */ public function listTables () /* {{{ */ { if ($this->sep === "") $this->DBException (dgettext ("domframework", "Database not connected")); switch (self::$instance[$this->dsn]->getAttribute (\PDO::ATTR_DRIVER_NAME)) { case "sqlite": $req = "SELECT name FROM sqlite_master WHERE type='table'"; $st = self::$instance[$this->dsn]->prepare ($req); $st->execute (); $res = array (); while ($d = $st->fetch (\PDO::FETCH_ASSOC)) $res[] = $d["name"]; break; case "mysql": $req = "SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_SCHEMA='".$this->databasename ()."'"; $st = self::$instance[$this->dsn]->prepare ($req); $st->execute (); $res = array (); while ($d = $st->fetch (\PDO::FETCH_ASSOC)) $res[] = $d["TABLE_NAME"]; break; case "pgsql": $req = "SELECT * FROM pg_tables WHERE schemaname = 'public'"; $st = self::$instance[$this->dsn]->prepare ($req); $st->execute (); $res = array (); while ($d = $st->fetch (\PDO::FETCH_ASSOC)) $res[] = $d["tablename"]; break; default: $this->DBException (dgettext ("domframework", "Unknown database driver in listTables")); } return $res; } /* }}} */ /** Create the table defined by the differents fields. * Define the SQL syntax based on SQL engines * $table = "dns zones"; * $fields = array ( * "id"=>array ("integer", "not null", "autoincrement"), * "zo ne"=>array ("varchar(255)", "not null"), * "vie wname"=>array ("varchar(255)"), * "view clients"=>array ("varchar(255)"), * "comme nt"=>array ("varchar(1024)"), * "opendate"=>array ("datetime", "not null"), * "closedate"=>array ("datetime"), * ); * $primary = "id"; * $unique = array ("id", array ("zo ne", "vie wname")); * $foreign = array ("zone"=>"table.field",...); */ public function createTable () /* {{{ */ { $this->debugLog ("Entering createTable"); if ($this->sep === "") throw new Exception (dgettext("domframework", "Database not connected"), 500); if (count ($this->fields) === 0) throw new Exception (dgettext("domframework", "No Field defined"), 500); if ($this->table === null) throw new \Exception (dgettext("domframework", "No table name defined to create the table"), 500); switch (self::$instance[$this->dsn]->getAttribute(\PDO::ATTR_DRIVER_NAME)) { case "sqlite": $sql = "CREATE TABLE IF NOT EXISTS ". "$this->sep$this->tableprefix$this->table$this->sep ". "(\n"; $i = 0; foreach ($this->fields as $field=>$params) { if ($i > 0) $sql .= ",\n"; // Name of field $sql .= "$this->sep$field$this->sep "; switch ($this->fieldTypeLight ($field)) { case "integer": $sql .= "INTEGER"; $params = array_slice ($params, 1); break; case "varchar": $length = $this->fieldLength ($field); $sql .= "VARCHAR($length)"; $params = array_slice ($params, 1); break; case "datetime": $sql .= "DATETIME"; $params = array_slice ($params, 1); break; case "date": $sql .= "DATE"; $params = array_slice ($params, 1); break; default: throw new Exception (sprintf ( dgettext("domframework", "Unknown type '%s' provided for field '%s'"), $this->fieldTypeLight ($field), $field), 500); } // Primary key if ($this->primary === $field) $sql .= " PRIMARY KEY"; // Others parameters for field // Sort to put the autoincrement field in front of params, if it is // present sort ($params); foreach ($params as $p) { switch ($p) { case "not null": $sql .= " NOT NULL"; break; case "autoincrement": $sql .= " AUTOINCREMENT";break; default: throw new Exception (sprintf (dgettext("domframework", "Unknown additionnal parameter '%s' for field '%s'"), $p, $field), 500); } } $i ++; } // Unique fields if ($this->unique !== null) { if (!is_array ($this->unique)) throw new Exception (dgettext("domframework", "The Unique field definition is not an array"), 500); foreach ($this->unique as $u) { $sql .= ",\n UNIQUE ($this->sep"; if (is_array ($u)) $sql .=implode ("$this->sep,$this->sep", $u); else $sql .= $u; $sql .="$this->sep)"; } } // Foreign keys $i = 0; foreach ($this->foreign as $field=>$k) { $sql .= ",\n FOREIGN KEY($this->sep$field$this->sep) ". "REFERENCES $this->sep".$k[0]."$this->sep($this->sep". $k[1]."$this->sep)"; if (isset ($k[2])) $sql .= " ".$k[2]; $i++; } $sql .=")"; break; case "mysql": $sql = "CREATE TABLE IF NOT EXISTS ". "$this->sep$this->tableprefix$this->table$this->sep ". "(\n"; $i = 0; foreach ($this->fields as $field=>$params) { if ($i > 0) $sql .= ",\n"; // Name of field $sql .= "$this->sep$field$this->sep "; switch ($this->fieldTypeLight ($field)) { case "integer": $sql .= "INTEGER"; $params = array_slice ($params, 1); break; case "varchar": $length = $this->fieldLength ($field); $sql .= "VARCHAR($length)"; $params = array_slice ($params, 1); break; case "datetime": $sql .= "DATETIME"; $params = array_slice ($params, 1); break; case "date": $sql .= "DATE"; $params = array_slice ($params, 1); break; default: throw new Exception (sprintf ( dgettext("domframework", "Unknown type provided for field '%s'"), $field), 500); } // Primary key if ($this->primary === $field) $sql .= " PRIMARY KEY"; // Others parameters for field // Sort to put the autoincrement field in front of params, if it is // present sort ($params); foreach ($params as $p) { switch ($p) { case "not null": $sql .= " NOT NULL"; break; case "autoincrement": $sql .= " AUTO_INCREMENT";break; default: throw new Exception (sprintf ( dgettext("domframework", "Unknown additionnal '%s' parameter for field '%s'"), $p, $field), 500); } } $i ++; } // Unique fields if ($this->unique !== null) { foreach ($this->unique as $u) { $sql .= ",\n UNIQUE ($this->sep"; if (is_array ($u)) $sql .=implode ("$this->sep,$this->sep", $u); else $sql .= $u; $sql .="$this->sep)"; } } // Foreign keys $i = 0; foreach ($this->foreign as $field=>$k) { $sql .= ",\n FOREIGN KEY($this->sep$field$this->sep) ". "REFERENCES $this->sep".$k[0]."$this->sep($this->sep". $k[1]."$this->sep)"; if (isset ($k[2])) $sql .= " ".$k[2]; if ($i > 0) $sql .= ","; $i++; } $sql .=") ENGINE=InnoDB DEFAULT CHARSET=utf8;"; break; case "pgsql": $sql = "CREATE TABLE IF NOT EXISTS ". "\"$this->tableprefix$this->table\" (\n"; $i = 0; foreach ($this->fields as $field=>$params) { if ($i > 0) $sql .= ",\n"; // Name of field $sql .= "\"$field\" "; if (in_array ("autoincrement", $params)) $sql .= "SERIAL"; else { switch ($this->fieldTypeLight ($field)) { case "integer": $sql .= "INTEGER"; $params = array_slice ($params, 1); break; case "varchar": $length = $this->fieldLength ($field); $sql .= "VARCHAR($length)"; $params = array_slice ($params, 1); break; case "datetime": $sql .= "timestamp with time zone"; $params = array_slice ($params, 1); break; case "date": $sql .= "DATE"; $params = array_slice ($params, 1); break; default: throw new Exception (sprintf ( dgettext("domframework", "Unknown type provided for field '%s'"), $field), 500); } // Primary key if ($this->primary === $field) $sql .= " PRIMARY KEY"; // Others parameters for field // Sort to put the autoincrement field in front of params, if it is // present sort ($params); foreach ($params as $p) { switch ($p) { case "not null": $sql .= " NOT NULL"; break; default: throw new Exception (sprintf ( dgettext("domframework", "Unknown additionnal parameter '%s' for field '%s'"), $p, $field), 500); } } } $i ++; } // Unique fields if ($this->unique !== null) { foreach ($this->unique as $u) { $sql .= ",\n UNIQUE (\""; if (is_array ($u)) $sql .=implode ("\",\"", $u); else $sql .= $u; $sql .="\")"; } } // Foreign keys $i = 0; foreach ($this->foreign as $field=>$k) { $sql .= ",\n FOREIGN KEY(\"$field\") REFERENCES \"".$k[0]."\"(\"". $k[1]."\")"; if (isset ($k[2])) $sql .= " ".$k[2]; if ($i > 0) $sql .= ","; $i++; } $sql .=")"; break; default: throw new Exception (dgettext("domframework", "PDO Engine not supported in dbLayeroo"), 500); } $this->debugLog ($sql); return self::$instance[$this->dsn]->exec ($sql); } /* }}} */ /** Drop the table */ public function dropTable () /* {{{ */ { $this->debugLog ("Entering dropTable ()"); if ($this->sep === "") throw new Exception (dgettext("domframework", "Database not connected")); if ($this->table === null) throw new \Exception (dgettext("domframework", "No table name defined to drop the table"), 500); $sql = "DROP TABLE $this->sep$this->tableprefix$this->table$this->sep"; $this->debugLog ($sql); return self::$instance[$this->dsn]->exec ($sql); } /* }}} */ /** Get the informations about a table * @param string $tableName The table to examine */ public function getTableSchema ($tableName) /* {{{ */ { $this->debugLog ("Entering getTableSchema (",$tableName,")"); switch (self::$instance[$this->dsn]->getAttribute(\PDO::ATTR_DRIVER_NAME)) { case "sqlite": $st = self::$instance[$this->dsn]->prepare ( "PRAGMA table_info($tableName)"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); $fields = array (); $unique = array (); $foreign = array (); $primary = ""; foreach ($content as $row) { $type = str_replace (" ", "", strtolower ($row["type"])); $fields[$row["name"]][] = $type; if ($row["notnull"] === "1") $fields[$row["name"]][] = "not null"; if ($row["pk"] === "1") $primary = $row["name"]; } $st = self::$instance[$this->dsn]->prepare ( "PRAGMA index_list($tableName)"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); foreach ($content as $c) { $st = self::$instance[$this->dsn]->prepare ( "PRAGMA index_info(".$c["name"].")"); $st->execute (); $content2 = $st->fetchAll (\PDO::FETCH_ASSOC); if (count ($content2) > 1) { $index = array (); foreach ($content2 as $c2) { $index[] = $c2["name"]; } $unique[$content2[0]["cid"]-1] = $index; } elseif (count ($content2) === 1) { $index = $content2[0]["name"]; $unique[$content2[0]["cid"]-1] = $index; } } ksort ($unique); $st = self::$instance[$this->dsn]->prepare ( "SELECT * FROM sqlite_sequence WHERE name='$tableName'"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); if (count ($content) > 0 && $primary !== "") $fields[$primary][] = "autoincrement"; $st = self::$instance[$this->dsn]->prepare ( "PRAGMA foreign_key_list('$tableName')"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); foreach ($content as $for) { $tmp = array ($for["table"], $for["to"]); $cascade = ""; if ($for["on_update"] !== "NO ACTION") $cascade .= "ON UPDATE ".$for["on_update"]; if ($for["on_delete"] !== "NO ACTION") $cascade .= "ON DELETE ".$for["on_delete"]; if ($cascade !== "") $tmp[] = $cascade; $foreign[$for["from"]] = $tmp; } return array ("fields" => $fields, "primary" => $primary, "unique" => $unique, "foreign" => $foreign); break; case "mysql": $st = self::$instance[$this->dsn]->prepare ( "SHOW COLUMNS FROM `$tableName`"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); $fields = array (); $unique = array (); $foreign = array ("TBD"); $primary = ""; foreach ($content as $col) { $tmp = array (); if ($col["Type"] === "int(11)") $tmp[] = "integer"; else $tmp[] = $col["Type"]; if ($col["Null"] === "NO") $tmp[] = "not null"; if ($col["Extra"] === "auto_increment") $tmp[] = "autoincrement"; $fields[$col["Field"]] = $tmp; } $st = self::$instance[$this->dsn]->prepare ( "SHOW INDEX FROM `$tableName`"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); foreach ($content as $col) { if ($col["Key_name"] === "PRIMARY") $primary = $col["Column_name"]; else { if ($col["Non_unique"] === "1") continue; if (array_key_exists ($col["Key_name"], $unique)) { if (!is_array ($unique[$col["Key_name"]])) $unique[$col["Key_name"]] = array ($unique[$col["Key_name"]]); $unique[$col["Key_name"]][] = $col["Column_name"]; } else { $unique[$col["Key_name"]] = $col["Column_name"]; } } } $unique = array_values ($unique); $st = self::$instance[$this->dsn]->prepare (" SELECT UPDATE_RULE,DELETE_RULE,COLUMN_NAME, kColUsage.REFERENCED_TABLE_NAME,REFERENCED_COLUMN_NAME FROM information_schema.REFERENTIAL_CONSTRAINTS AS rCons, information_schema.KEY_COLUMN_USAGE as kColUsage WHERE rCons.CONSTRAINT_SCHEMA=:dbname AND rCons.TABLE_NAME=:table AND rCons.CONSTRAINT_NAME=kColUsage.CONSTRAINT_NAME"); $st->execute (array(':dbname' => $this->databasename (), ':table' => $tableName)); $foreignTmp = $st->fetchAll (\PDO::FETCH_ASSOC); $foreign = array (); foreach ($foreignTmp as $f) { $tmp = array (); $tmp[] = $f["REFERENCED_TABLE_NAME"]; $tmp[] = $f["REFERENCED_COLUMN_NAME"]; $tmp[2] = ""; if ($f["UPDATE_RULE"] !== "NO ACTION" && $f["UPDATE_RULE"] !== "RESTRICT") $tmp[2] .= "ON UPDATE ".$f["UPDATE_RULE"]." "; if ($f["DELETE_RULE"] !== "NO ACTION" && $f["DELETE_RULE"] !== "RESTRICT") $tmp[2] .= "ON DELETE ".$f["DELETE_RULE"]; if ($tmp[2] !== "") $tmp[2] = trim ($tmp[2]); else unset ($tmp[2]); $foreign[$f["COLUMN_NAME"]] = $tmp; } return array ("fields" => $fields, "primary" => $primary, "unique" => $unique, "foreign" => $foreign); break; case "pgsql": $st = self::$instance[$this->dsn]->prepare ( "SELECT * FROM information_schema.columns WHERE table_schema='public' and table_name='$tableName'"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); $fields = array (); $unique = array (); $foreign = array (); $primary = ""; foreach ($content as $col) { $tmp = array (); if ($col["data_type"] === "character varying") $tmp[] = "varchar(".$col["character_maximum_length"].")"; else $tmp[] = $col["data_type"]; if ($col["is_nullable"] === "NO") $tmp[] = "not null"; if (substr ($col["column_default"], 0, 7) === "nextval") { $tmp[] = "autoincrement"; $primary = $col["column_name"]; } $fields[$col["column_name"]] = $tmp; } $st = self::$instance[$this->dsn]->prepare ( "select * from information_schema.constraint_column_usage where table_name='$tableName'"); $st->execute (); $content = $st->fetchAll (\PDO::FETCH_ASSOC); foreach ($content as $col) { if (array_key_exists ($col["constraint_name"], $unique)) { if (! is_array ($unique[$col["constraint_name"]])) $unique[$col["constraint_name"]] = array ($unique[$col["constraint_name"]]); $unique[$col["constraint_name"]][] = $col["column_name"]; } else { $unique[$col["constraint_name"]] = $col["column_name"]; } } $unique = array_values ($unique); $st = self::$instance[$this->dsn]->prepare (" SELECT kColUsage1.column_name COLUMN_NAME, kColUsage2.table_name REFERENCED_TABLE_NAME, kColUsage2.column_name REFERENCED_COLUMN_NAME, update_rule,delete_rule FROM information_schema.KEY_COLUMN_USAGE AS kColUsage1, information_schema.KEY_COLUMN_USAGE AS kColUsage2, information_schema.REFERENTIAL_CONSTRAINTS AS rCons WHERE kColUsage1.table_catalog=:dbname AND kColUsage1.table_name=:table AND rCons.constraint_name=kColUsage1.constraint_name AND rCons.unique_constraint_name=kColUsage2.constraint_name "); $st->execute (array(':dbname' => $this->databasename (), ':table' => $tableName)); $foreignTmp = $st->fetchAll (\PDO::FETCH_ASSOC); $foreign = array (); foreach ($foreignTmp as $f) { $tmp = array (); $tmp[] = $f["referenced_table_name"]; $tmp[] = $f["referenced_column_name"]; $tmp[2] = ""; if ($f["update_rule"] !== "NO ACTION" && $f["update_rule"] !== "RESTRICT") $tmp[2] .= "ON UPDATE ".$f["update_rule"]." "; if ($f["delete_rule"] !== "NO ACTION" && $f["delete_rule"] !== "RESTRICT") $tmp[2] .= "ON DELETE ".$f["delete_rule"]; if ($tmp[2] !== "") $tmp[2] = trim ($tmp[2]); else unset ($tmp[2]); $foreign[$f["column_name"]] = $tmp; } return array ("fields" => $fields, "primary" => $primary, "unique" => $unique, "foreign" => $foreign); break; default: throw new Exception (dgettext("domframework", "PDO Engine not supported in dbLayeroo"), 500); } } /* }}} */ /** Return the type of the provided field * @param string $field The field to get the type */ private function fieldTypeComplete ($field) /* {{{ */ { // $this->debugLog ("Entering fieldTypeComplete (",$field,")"); if (! array_key_exists ($field, $this->fields)) $this->DBException (sprintf ( "fieldType : can't find the definition for field '%s'", $field)); if (! array_key_exists (0, $this->fields[$field])) $this->DBException (sprintf ( "fieldType : can't find the type for field '%s'", $field)); if (! is_string ($this->fields[$field][0])) $this->DBException (sprintf ( "fieldType : The type of field '%s' is not a string", $field)); $type = strtolower ($this->fields[$field][0]); $type = str_replace (" ", "", $type); return $type; } /* }}} */ /** Return the type of the provided field. For varchar(255), return only * varchar * @param string $field The field to get the type */ private function fieldTypeLight ($field) /* {{{ */ { $type = $this->fieldTypeComplete ($field); list ($type, ) = explode ("(", $type); return $type; } /* }}} */ /** Return the length of a field (generally a varchar) * @param string $field The field to get the type */ private function fieldLength ($field) /* {{{ */ { $type = $this->fieldTypeComplete ($field); $pos = strpos ($type, "("); if ($pos === false) $this->DBException (sprintf ( "fieldLength : no length defined for field '%s'", $field)); $length = intval (substr ($type, 1+$pos, -1)); if ($length === 0) $this->DBException (sprintf ( "fieldLength : Length equal to Zero for field '%s'", $field)); return $length; } /* }}} */ ///////////////////////////// /// GETTERS / SETTERS /// ///////////////////////////// /** Get/Set the table property * @param string|null $table The table to use */ public function table ($table=null) /* {{{ */ { $this->debugLog ("Entering table (",$table,")"); if ($table === null) return $this->table; if (! is_string ($table)) $this->DBException ("Parameter table invalid: not a string"); if (mb_strlen ($table) > 63) $this->DBException ("Parameter table invalid: too long"); $this->table = $table; return $this; } /* }}} */ /** Get/Set the tableprefix property * @param string|null $tableprefix The prefix to append */ public function tableprefix ($tableprefix=null) /* {{{ */ { $this->debugLog ("Entering tableprefix (",$tableprefix,")"); if ($tableprefix === null) return $this->tableprefix; if (! is_string ($tableprefix)) $this->DBException ("Parameter tableprefix invalid: not a string"); // 64 - at least one char for the table name if (mb_strlen ($tableprefix) > 63) $this->DBException ("Parameter tableprefix invalid: too long"); $this->tableprefix = $tableprefix; return $this; } /* }}} */ /** Get/Set the fields property * The fields to define are in the format: * array ("fieldName"=>array ("type"[, "not null"[, "autoincrement"]])) * @param array|null $fields The fields to define */ public function fields ($fields=null) /* {{{ */ { $this->debugLog ("Entering fields (VALUE)"); if ($fields === null) return $this->fields; if (! is_array ($fields)) $this->DBException ("Parameter fields invalid: not an array"); foreach ($fields as $field=>$params) { if (mb_strlen ($field) > 64) $this->DBException ("Parameter fields invalid: column name too long"); if (! is_array ($params)) $this->DBException ("Parameter fields invalid: ". "param not an array for '$field'"); if (! array_key_exists (0, $params)) $this->DBException ("Parameter fields invalid: ". "No type of column provided for '$field'"); if (preg_match ("#^(data|datetime|integer|time|varchar\(\d+\))$#i", $params[0]) !== 1) $this->DBException ("Parameter fields invalid: ". "Unknown column type provided for '$field'"); if (array_key_exists (1, $params) && $params[1] !== "not null" && $params[1] !== "autoincrement") $this->DBException ("Parameter fields invalid: ". "Second parameter invalid for '$field'"); if (array_key_exists (2, $params) && $params[2] !== "autoincrement") $this->DBException ("Parameter fields invalid: ". "Third parameter invalid for '$field'"); } $this->fields = $fields; return $this; } /* }}} */ /** Get/Set the primary property * @param string|null $primary The primary key to use */ public function primary ($primary=null) /* {{{ */ { $this->debugLog ("Entering primary (",$primary,")"); if ($primary === null) return $this->primary; if (! is_string ($primary)) $this->DBException ("Parameter primary invalid: not a string"); if (mb_strlen ($primary) > 64) $this->DBException ("Parameter primary invalid: too long"); if (! array_key_exists ($primary, $this->fields)) $this->DBException ("Parameter primary invalid: column doesn't exists"); $this->primary = $primary; return $this; } /* }}} */ /** Get/Set the unique property * @param array|null $unique The unique fields constraint to add */ public function unique ($unique=null) /* {{{ */ { $this->debugLog ("Entering unique (VALUE)"); if ($unique === null) return $this->unique; if (! is_array ($unique)) $this->DBException ("Parameter unique invalid: not an array"); foreach ($unique as $u1) { if (is_string ($u1)) { if (mb_strlen ($u1) > 64) $this->DBException ("Parameter unique invalid: too long '$u1'"); } elseif (is_array ($u1)) { foreach ($u1 as $u2) { if (is_string ($u2)) { if (mb_strlen ($u2) > 64) $this->DBException ("Parameter unique invalid: too long '$u2'"); } else $this->DBException ("Parameter unique invalid: Not string ". "in array"); } } else $this->DBException ("Parameter unique invalid: Not string nor array: ". gettype ($u1)); } $this->unique = $unique; return $this; } /* }}} */ /** Get/Set the foreign property * @param array|null $foreign The definition of the foreign constraint */ public function foreign ($foreign=null) /* {{{ */ { $this->debugLog ("Entering foreign (VALUE)"); if ($foreign === null) return $this->foreign; if (! is_array ($foreign)) $this->DBException ("Parameter foreign invalid: not an array"); foreach ($foreign as $col=>$params) { if (! array_key_exists ($col, $this->fields)) $this->DBException ("Parameter foreign invalid: column doesn't exists"); if (! is_array ($params)) $this->DBException ("Parameter foreign invalid: ". "parameters not in array"); if (! array_key_exists (0, $params)) $this->DBException ("Parameter foreign invalid: ". "parameter 0 doesn't exists"); if (! array_key_exists (1, $params)) $this->DBException ("Parameter foreign invalid: ". "parameter 1 doesn't exists"); if (! is_string ($params[0])) $this->DBException ("Parameter foreign invalid: ". "parameter 0 is not a string"); if (! is_string ($params[1])) $this->DBException ("Parameter foreign invalid: ". "parameter 1 is not a string"); if (mb_strlen ($params[0] > 64)) $this->DBException ("Parameter foreign invalid: ". "parameter 0 is too long"); if (mb_strlen ($params[1] > 64)) $this->DBException ("Parameter foreign invalid: ". "parameter 1 is too long"); if (array_key_exists (2, $params)) { preg_match_all ("#^(ON UPDATE ". "(CASCADE|RESTRICT|NO ACTION|SET DEFAULT|SET NULL) ?)?". "(ON DELETE ". "(CASCADE|RESTRICT|NO ACTION|SET DEFAULT|SET NULL))?$#", $params[2], $matches); if ($matches[1] === "" && $matches[3] === "") $this->DBException ("Parameter foreign invalid: ". "Unknown action provided"); } } $this->foreign = $foreign; return $this; } /* }}} */ /** Get/Set the debug property * @param integer|null $debug Set the debug value */ public function debug ($debug=null) /* {{{ */ { $this->debugLog ("Entering debug (",$debug,")"); if ($debug === null) return $this->debug; if (! is_int ($debug)) $this->DBException ("Parameter debug invalid: not an integer"); $this->debug = $debug; return $this; } /* }}} */ /** Get/Set the dsn property * @param string|null $dsn Set the DSN property of PDO */ public function dsn ($dsn=null) /* {{{ */ { $this->debugLog ("Entering dsn (",$dsn,")"); if ($dsn === null) return $this->dsn; if (! is_string ($dsn)) $this->DBException ("Parameter dsn invalid : not a string"); $this->dsn = $dsn; return $this; } /* }}} */ /** Get/Set the titles property * @param array|null $titles The titles of the fields */ public function titles ($titles=null) /* {{{ */ { $this->debugLog ("Entering titles (VALUE)"); if ($titles === null) return $this->titles; if (! is_array ($titles)) $this->DBException ("Parameter titles invalid: not an array"); foreach ($titles as $title=>$translation) { if (! is_string ($title)) $this->DBException ("Parameter titles invalid: title not a string"); if (mb_strlen ($title) > 64) $this->DBException ("Parameter titles invalid: title too long"); if (! is_string ($translation)) $this->DBException ("Parameter titles invalid: ". "translation not a string"); if (mb_strlen ($translation) > 64) $this->DBException ("Parameter titles invalid: translation too long"); } $this->titles = $titles; return $this; } /* }}} */ ///////////////////////////////////// /// MANAGE THE REQUEST BY OOP /// ///////////////////////////////////// /** The command to use */ private $command = ""; /** The DISTINCT option */ private $distinct = ""; /** The columns to display in SELECT, with the tables names and the separators * correctely defined */ private $displayColumn = array (); /** Manage the joins */ private $joins = array (); /** The WHERE expression */ private $whereExpression = array (); /** The values for each parameter for the WHERE condition */ private $whereValues = array (); /** The ORDER expression */ private $orderExpression = array (); /** The LIMIT expression */ private $limitExpression = ""; /** The values to SET in INSERT/UPDATE */ private $setValues = array (); /** The types to SET in INSERT/UPDATE */ private $setType = array (); /** The dblayeroo object of the foreign keys tables to check */ private $setForeignObj = array (); /** The debug depth (as we clone object, the depth is increased to debug * easily the functions */ private $debugDepth = 1; /** Reinit the SQL request */ public function clearRequest () /* {{{ */ { $this->debugLog ("Entering clearRequest ()"); $this->command = ""; $this->distinct = ""; $this->displayColumn = array (); $this->joins = array (); $this->whereExpression = array (); $this->whereValues = array (); $this->orderExpression = array (); $this->limitExpression = ""; $this->setValues = array (); $this->setType = array (); $this->setForeignObj = array (); unset ($this->fieldsTmp); } /* }}} */ /** Define a new foreign object * @param object $object The dblayeroo object to use for foreign constraint * checks */ public function setForeignObj ($object) /* {{{ */ { $this->debugLog ("Entering setForeignObj (OBJECT)"); if (! is_object ($object)) $this->DBException ("Invalid setForeignObj parameter: not an object"); if (get_class ($object) !== __CLASS__) $this->DBException ( "Invalid object provided to setForeignObj (not dblayeroo object)"); if (! isset ($object->table)) $this->DBException ( "Invalid object provided to setForeignObj (no table defined)"); $this->setForeignObj[$object->tableprefix.$object->table] = $object; } /* }}} */ /** Define the command to execute. Can be * "SELECT", "INSERT", "DELETE", "UPDATE". * @param string $command The command to execute */ public function command ($command) /* {{{ */ { $this->debugLog ("Entering command (",$command,")"); $allowed = array ("SELECT", "INSERT", "DELETE", "UPDATE"); if (! is_string ($command)) $this->DBException ("Invalid command provided (not string)"); $command = strtoupper ($command); if (! in_array ($command, $allowed)) $this->DBException ("Invalid command provided (unknown command)"); $this->command = $command; return $this; } /* }}} */ /** Alias of command ("SELECT") */ public function select () /* {{{ */ { $this->command = "SELECT"; return $this; } /* }}} */ /** Alias of command ("INSERT") */ public function insert () /* {{{ */ { $this->command = "INSERT"; return $this; } /* }}} */ /** Alias of command ("DELETE") */ public function delete () /* {{{ */ { $this->command = "DELETE"; return $this; } /* }}} */ /** Alias of command ("UPDATE") */ public function update () /* {{{ */ { $this->command = "UPDATE"; return $this; } /* }}} */ /** Set the DISTINCT option */ public function setDistinct () /* {{{ */ { $this->distinct = "DISTINCT"; return $this; } /* }}} */ /** Set the columns to display for the next SELECT request * @param array|string $columnNames The columns name, separated by comma * By default, display all the columns */ public function displayColumn ($columnNames) /* {{{ */ { $this->debugLog ("Entering displayColumn (",$columnNames,")"); if (! is_string ($columnNames) && ! is_array ($columnNames)) $this->DBException ( "Invalid columnNames provided (not string and not array)"); if (is_string ($columnNames)) $columnNames = explode (",", $columnNames); foreach ($columnNames as $name) { if (! array_key_exists ($name, $this->fields)) $this->DBException (sprintf ( "Invalid field to display '%s' : not defined in table", $name)); $this->displayColumn[] = $this->sep.$this->tableprefix.$this->table. $this->sep.".". $this->sep.$name.$this->sep; } return $this; } /* }}} */ /** Do a inner join between two dblayer objects * The join array is a associated array with local field as key and distant * field as value * @param object $object The dblayeroo object to use for searching the join * data * @param array $joinArray The values to search for join */ public function joinInner ($object, $joinArray) /* {{{ */ { $this->debugLog ("Entering joinInner (OBJECT, JOINARRAY)"); return $this->joinReal ("INNER", $object, $joinArray); } /* }}} */ /** Do a left join between two dblayer objects * The join array is a associated array with local field as key and distant * field as value * @param object $object The dblayeroo object to use for searching the join * data * @param array $joinArray The values to search for join */ public function joinLeft ($object, $joinArray) /* {{{ */ { $this->debugLog ("Entering joinLeft (OBJECT, JOINARRAY)"); return $this->joinReal ("LEFT", $object, $joinArray); } /* }}} */ /** Do a right join between two dblayer objects * The join array is a associated array with local field as key and distant * field as value * @param object $object The dblayeroo object to use for searching the join * data * @param array $joinArray The values to search for join */ public function joinRight ($object, $joinArray) /* {{{ */ { $this->debugLog ("Entering joinRight (OBJECT, JOINARRAY)"); return $this->joinReal ("RIGHT", $object, $joinArray); } /* }}} */ /** Do the real join * @param string $joinType The join type to use ("INNER", "LEFT", "RIGHT") * @param object $object The dblayeroo object to use for searching the join * data * @param array $joinArray The values to search for join */ private function joinReal ($joinType, $object, $joinArray) /* {{{ */ { $this->debugLog ("Entering joinReal (",$joinType,", OBJECT, JOINARRAY)"); if (! is_string ($joinType)) $this->DBException ("Invalid joinType provided to join (not string)"); if (! in_array ($joinType, array ("INNER", "LEFT", "RIGHT"))) $this->DBException ("Invalid joinType provided to join (not known)"); if (! is_object ($object)) $this->DBException ("Invalid object provided to join (not object)"); if (get_class ($object) !== __CLASS__) $this->DBException ( "Invalid object provided to join (not dblayeroo object)"); if ($this->dsn !== $object->dsn) $this->DBException ( "DSN different : don't support JOIN between databases"); if (! is_array ($joinArray)) $this->DBException ("Invalid joinArray provided (not array)"); if (empty ($joinArray)) $this->DBException ("Invalid joinArray provided (empty array)"); foreach ($joinArray as $fieldLocal=>$fieldToJoin) { if (! array_key_exists ($fieldLocal, $this->fields)) $this->DBException (sprintf ( "Invalid field to join '%s' : not defined in Local table", $fieldLocal)); if (! array_key_exists ($fieldToJoin, $object->fields)) $this->DBException (sprintf ( "Invalid field to join '%s' : not defined in Distant table", $fieldToJoin)); } if (! isset ($object->table) || $object->table === null || trim ($object->table) === "") $this->DBException ("No table defined in the Join object"); if (! isset ($this->table) || $this->table === null || trim ($this->table) === "") $this->DBException ("No table defined in the local object"); if (! isset ($object->tableprefix) || $object->tableprefix === null) $this->DBException ("No tableprefix defined in the Join object"); $tmp = ""; foreach ($joinArray as $fieldLocal=>$fieldToJoin) { if ($tmp !== "") $tmp .= " AND "; $tmp .= $this->sep.$this->tableprefix.$this->table.$this->sep.".". $this->sep.$fieldLocal.$this->sep. "=". $this->sep.$object->tableprefix.$object->table.$this->sep.".". $this->sep.$fieldToJoin.$this->sep; } // Correct the displayQuery in the main display fields with the display // fields of object $this->joins[] = "$joinType JOIN ". $this->sep.$object->tableprefix.$object->table.$this->sep." ON $tmp"; foreach ($object->displayColumn as $col) { array_unshift ($this->displayColumn, $col); } // Correct the WHERE in the main with the object WHERE $this->whereExpression = array_merge ($object->whereExpression, $this->whereExpression); $this->whereValues = array_merge ($object->whereValues, $this->whereValues); // Add the new object fields to the this. The fields must be available to // be normalized at the end $tmp = array (); foreach ($object->fields as $key=>$data) { $tmp[$this->sep.$object->tableprefix.$object->table.$this->sep.".". $this->sep.$key.$this->sep] = $data; } if (!isset ($this->fieldsTmp)) { $tmpThis = array (); foreach ($this->fields as $key=>$data) { $tmpThis[$this->sep.$this->tableprefix.$this->table.$this->sep.".". $this->sep.$key.$this->sep] = $data; } $this->fieldsTmp = $tmpThis; } $this->fieldsTmp = array_merge ($this->fieldsTmp, $tmp); return $this; } /* }}} */ /** Set a new WHERE expression value * @param string $field The field to check * @param string $operator The operator ("=", "<=", ">=", "!=", "NOT LIKE", * "LIKE", "IS NULL", "REGEXP", "NOT REGEXP") * @param string|null $value The value to search ("" if not provided) */ public function whereAdd ($field, $operator, $value = "") /* {{{ */ { $this->debugLog ("Entering whereAdd (",$field,", ",$operator,", ",$value, ")"); if (! is_string ($field)) $this->DBException ("Invalid field provided (not string)"); if (! is_string ($operator)) $this->DBException ("Invalid operator provided (not string)"); if (! is_string ($value) && ! is_null ($value) && ! is_integer ($value)) $this->DBException ("Invalid value provided (not string nor null ". "nor integer)"); if (! array_key_exists ($field, $this->fields)) $this->DBException (sprintf ( "Invalid field to whereAdd '%s' : not defined in table", $field)); $operator = strtoupper ($operator); $allowed = array ("=", "<=", ">=", "!=", "LIKE", "NOT LIKE", "IS NULL", "REGEXP", "NOT REGEXP"); if (! in_array ($operator, $allowed)) $this->DBException ("Invalid operator provided (unknown operator)"); // TODO : Check if the value is corresponding to the type of the column if (count ($this->whereExpression) && end ($this->whereExpression) !== "AND" && end ($this->whereExpression) !== "OR" && end ($this->whereExpression) !== "(") $this->whereExpression[] = "AND"; if ($this->driver === "pgsql" && $operator === "REGEXP") $operator = "~"; if ($this->driver === "pgsql" && $operator === "NOT REGEXP") $operator = "!~"; $hash = md5 ("$field, $operator, $value"); $this->whereExpression[] = $this->sep.$this->tableprefix.$this->table.$this->sep.".". $this->sep.$field.$this->sep." ".$operator." :$hash"; $this->whereValues[$hash] = array ( "field"=>$field, "fieldfull"=> $this->tableprefix.$this->table.".".$field, "operator"=>$operator, "value"=>$value, "hash"=>$hash, "type"=>$this->fieldTypeLight ($field)); return $this; } /* }}} */ /** Add a new AND to the WHERE expression */ public function whereAddAND () /* {{{ */ { $this->debugLog ("Entering whereAddAND ()"); if (count ($this->whereExpression) === 0) $this->DBException ("Can not add AND as there is no previous expression"); $this->whereExpression[] = "AND"; return $this; } /* }}} */ /** Add a new OR to the WHERE expression */ public function whereAddOR () /* {{{ */ { $this->debugLog ("Entering whereAddOR ()"); if (count ($this->whereExpression) === 0) $this->DBException ("Can not add OR as there is no previous expression"); $this->whereExpression[] = "OR"; return $this; } /* }}} */ /** Add a new Open Parenthesis to the WHERE expression */ public function whereAddParenthesisOpen () /* {{{ */ { $this->debugLog ("Entering whereAddParenthesisOpen ()"); $this->whereExpression[] = "("; return $this; } /* }}} */ /** Add a new Close Parenthesis to the WHERE expression */ public function whereAddParenthesisClose () /* {{{ */ { $this->debugLog ("Entering whereAddParenthesisClose ()"); $this->whereExpression[] = ")"; return $this; } /* }}} */ /** Add a new ORDER sort * @param string $field The field to sort * @param string|null $sort The sort order ("ASC", "DESC"); */ public function orderAdd ($field, $sort = "ASC") /* {{{ */ { $this->debugLog ("Entering orderAdd (",$field,", ",$sort,")"); if (! is_string ($field)) $this->DBException ("Invalid field provided (not string)"); if (! is_string ($sort)) $this->DBException ("Invalid sort provided (not string)"); $sort = strtoupper ($sort); if (! in_array ($sort, array ("ASC", "DESC"))) $this->DBException ("Invalid sort provided (not ASC nor DESC)"); if (! array_key_exists ($field, $this->fields)) $this->DBException (sprintf ( "Invalid field to orderAdd '%s' : not defined in table", $field)); $this->orderExpression[] = $this->sep.$field.$this->sep." ".$sort; return $this; } /* }}} */ /** Define a LIMIT for the request. * To use only the nbLines, put a 0 on startLine * @param integer $startLine The starting line in the result list * @param integer $nbLines The number of lines to return */ public function limit ($startLine, $nbLines) /* {{{ */ { $this->debugLog ("Entering limit (",$startLine,", ",$nbLines,")"); if (! preg_match ("/^\d+$/", $startLine)) $this->DBException (sprintf ( "Invalid startLine to limit '%d': not numerical", $startLine)); if (! preg_match ("/^\d+$/", $nbLines)) $this->DBException (sprintf ( "Invalid nbLines to limit '%d': not numerical", $nbLines)); $startLine = intval ($startLine); $nbLines = intval ($nbLines); if ($startLine !== 0) $this->limitExpression = "$startLine,$nbLines"; else $this->limitExpression = "$nbLines"; return $this; } /* }}} */ /** Define a LIMIT for the request. * @param integer $nbLines The number of lines to return */ public function limitLines ($nbLines) /* {{{ */ { $this->debugLog ("Entering limitLines (",$nbLines,")"); if (! preg_match ("/^\d+$/", $nbLines)) $this->DBException (sprintf ( "Invalid nbLines to limit '%d': not numerical", $nbLines)); $nbLines = intval ($nbLines); $this->limitExpression = "$nbLines"; return $this; } /* }}} */ /** Set INSERT/UPDATE values * - The provided array must be an associative array, the field name must be * provided as key and the value as value. Each field=>val will be updated * @param array $values The values to INSERT or UPDATE */ public function setValues ($values) /* {{{ */ { $this->debugLog ("Entering setValues (",$values,")"); if (! is_array ($values)) $this->DBException ("Invalid values to setValues : not an array"); $associative = null; $tmpValues = array (); $tmpType = array (); foreach ($values as $key=>$val) { if (! array_key_exists ($key, $this->fields)) $this->DBException (sprintf ( "Invalid field to setValues '%s' : not defined in table", $key)); $tmpValues[$key] = $val; $tmpType[md5 ("$key, $val")] = $this->fieldTypeLight ($key); $this->debugLog ("setValues : Type for $key = ". $this->fieldTypeLight ($key)); } $this->setValues = $tmpValues; $this->setType = $tmpType; return $this; } /* }}} */ /** Create the SQL request */ private function createRequest () /* {{{ */ { if ($this->sep === "") $this->DBException (dgettext ("domframework", "Database not connected")); if ($this->table === null) $this->DBException (dgettext ("domframework", "No table name defined to insert in the table")); if ($this->unique === null) $this->DBException (dgettext ("domframework", "Unique fields of table are not defined")); if (! is_array ($this->unique)) $this->DBException (dgettext ("domframework", "The unique configuration is not an array")); switch ($this->command) { case "SELECT": $sql = "SELECT"; if ($this->distinct !== "") $sql .= " ".$this->distinct; $displayColumns = implode (",", $this->displayColumn); if ($displayColumns === "") { if (isset ($this->fieldsTmp)) $displayColumns = implode (",", array_keys ($this->fieldsTmp)); else $displayColumns = "*"; } $sql .= " $displayColumns FROM $this->sep$this->tableprefix". "$this->table$this->sep"; if (! empty ($this->joins)) $sql .= " ". implode (" ", $this->joins); if (! empty ($this->whereExpression)) $sql .= " WHERE ". implode (" ", $this->whereExpression); if (! empty ($this->orderExpression)) $sql .= " ORDER BY ". implode (",", $this->orderExpression); if (! empty ($this->limitExpression)) $sql .= " LIMIT $this->limitExpression"; // No set Values for SELECT $this->setValues = array (); break; case "INSERT": $sql = "INSERT INTO $this->sep$this->tableprefix$this->table$this->sep ("; if (empty ($this->setValues)) $this->DBException ("No values set to add in INSERT"); $i = 0; foreach ($this->setValues as $key=>$val) { if ($i > 0) $sql .= ","; $sql .= $this->sep.$key.$this->sep; $i++; } $sql .= ") VALUES ("; $i = 0; foreach ($this->setValues as $key=>$val) { if ($i > 0) $sql .= ","; $sql .= ":".md5 ("$key, $val"); $i++; } $sql .= ")"; // No WHERE in INSERT : remove the WHERE parameters $this->whereExpression = array (); $this->whereValues = array (); break; case "DELETE": $sql = "DELETE FROM $this->sep$this->tableprefix$this->table$this->sep"; if (! empty ($this->whereExpression)) $sql .= " WHERE ". implode (" ", $this->whereExpression); if (! empty ($this->orderExpression)) $sql .= " ORDER BY ". implode (",", $this->orderExpression); if (! empty ($this->limitExpression)) $sql .= " LIMIT $this->limitExpression"; // No set Values for DELETE $this->setValues = array (); break; case "UPDATE": $sql = "UPDATE $this->sep$this->tableprefix$this->table$this->sep"; if (empty ($this->setValues)) $this-> DBException ("No values to set in UPDATE"); $sql .= " SET "; $i = 0; foreach ($this->setValues as $key=>$val) { if ($i > 0) $sql .= ","; $hash = md5 ("$key, $val"); $sql .= $this->sep.$key.$this->sep."=:".$hash; $i++; } if (! empty ($this->whereExpression)) $sql .= " WHERE ". implode (" ", $this->whereExpression); if (! empty ($this->orderExpression)) $sql .= " ORDER BY ". implode (",", $this->orderExpression); if (! empty ($this->limitExpression)) $sql .= " LIMIT $this->limitExpression"; break; default: $this->DBException ("No command specified"); } return $sql; } /* }}} */ /** Prepare the request with the associated entries. * If textForm is true, return a string to display what will be done * If textForm is false, return a statement * @param string $sql The SQL request to prepare * @param boolean $textForm If true, return the result. If false prepare * really the request */ private function prepareRequest ($sql, $textForm) /* {{{ */ { $text = ""; if (!$textForm) $st = self::$instance[$this->dsn]->prepare ($sql); foreach ($this->whereValues as $hash=>$val) { $field = $val["field"]; $value = $val["value"]; $type = $val["type"]; $text .= "DEBUG BIND WHERE : $hash ($field)->$value "; if ($value === null) $text .= "NULL (null)\n"; elseif ($type === "integer") $text .= "(integer)\n"; elseif ($type === "varchar") $text .= "(varchar)\n"; elseif ($type === "datetime") $text .= "(datetime)\n"; elseif ($type === "date") $text .= "(date)\n"; else { $text .= "(UNKNOWN)\n"; $this->DBException ("TO BE DEVELOPPED : type=".$type); } if (!$textForm) { if ($value === null) $st->bindValue (":$hash", $value, \PDO::PARAM_NULL); elseif ($type === "integer") $st->bindValue (":$hash", $value, \PDO::PARAM_INT); elseif ($type === "varchar") $st->bindValue (":$hash", "$value", \PDO::PARAM_STR); elseif ($type === "datetime") $st->bindValue (":$hash", $value, \PDO::PARAM_STR); elseif ($type === "date") $st->bindValue (":$hash", $value, \PDO::PARAM_STR); else $this->DBException ("prepareRequest:whereValues TO BE DEVELOPPED : ". "type=$type"); } } foreach ($this->setValues as $field=>$value) { $hash = md5 ("$field, $value"); if (! array_key_exists ($hash, $this->setType)) $this->DBException (sprintf ("Field '%s' not found in Type list", $field)); $type = $this->setType[$hash]; $text .= "DEBUG BIND SET : $hash ($field)->$value "; if ($value === null) $text .= "NULL (null)\n"; elseif ($type === "integer") $text .= "(integer)\n"; elseif ($type === "varchar") $text .= "(varchar)\n"; elseif ($type === "datetime") $text .= "(datetime)\n"; elseif ($type === "date") $text .= "(date)\n"; else { $text .= "(UNKNOWN)\n"; $this->DBException ("TO BE DEVELOPPED : ".$fields[$field][0]); } if (!$textForm) { if ($value === null) $st->bindValue (":$hash", $value, \PDO::PARAM_NULL); elseif ($this->setType[$hash] === "integer") $st->bindValue (":$hash", $value, \PDO::PARAM_INT); elseif ($this->setType[$hash] === "varchar") $st->bindValue (":$hash", "$value", \PDO::PARAM_STR); elseif ($this->setType[$hash] === "datetime") $st->bindValue (":$hash", $value, \PDO::PARAM_STR); elseif ($this->setType[$hash] === "date") $st->bindValue (":$hash", $value, \PDO::PARAM_STR); else $this->DBException ("prepareRequest:setValues TO BE DEVELOPPED : ". $this->fields[$field][0]); } } if ($textForm) return $text; else return $st; } /* }}} */ /** Return the query that will be executed */ public function getDisplayQuery () /* {{{ */ { $text = ""; $sql = $this->createRequest (); $text .= "$sql"; $prep = $this->prepareRequest ($sql, true); if ($prep !== "") $text .= "\n$prep"; return $text; } /* }}} */ /** Check the provided values which will be inserted or updated against the * database structure. * In update, do not forget to define the whereAdd parameters ! * @param array $values The values to test * @param boolean|null $update if true UPDATE request, else INSERT request * @return array The errors found by field */ public function verify ($values, $update = false) /* {{{ */ { $update = !! $update; $errors = array (); if ($update === false) { // INSERT mode if (! array_key_exists ($this->primary, $values)) $values[$this->primary] = null; // - Look if all the NOT NULL fields are filled foreach ($this->fields as $field=>$params) { if (in_array ("not null", $params) && ! array_key_exists ($field, $values)) $errors[$field] = sprintf (dgettext ("domframework", "Mandatory field '%s' not provided"), $field); elseif (! in_array ("not null", $params) && ! array_key_exists ($field, $values)) continue; } } else { // UPDATE mode } // INSERT or UPDATE mode // - Check the validity of the content of the fields. Should be already done // by the application, so just throw an Exception if the error is raised foreach ($this->fields as $field=>$params) { $this->debugLog (" verify the field validity [$field]"); if ($update !== false && ! array_key_exists ($field, $values)) // In case of UPDATE, the values are already stored. We can skip the // test if the user don't want to modify a field (and do not provide the // value continue; if (! in_array ("not null", $params) && ! array_key_exists ($field, $values)) continue; if (in_array ("not null", $params) && ! array_key_exists ($field, $values)) { $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' mandatory and not provided"), $field); continue; } if (! is_string ($values[$field]) && ! is_integer ($values[$field]) && ! is_null ($values[$field])) $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' not a string nor a integer"), $field); switch ($this->fieldTypeLight ($field)) { case "integer": if (strspn ($values[$field], "0123456789") !== strlen ($values[$field])) $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' not in integer format"), $field); break; case "varchar": $length = $this->fieldLength ($field); if (mb_strlen ($values[$field]) > $length) $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' : Data field too long"), $field); break; case "date": if (! preg_match ("#^\d{4}-\d{2}-\d{2}$", $values[$field])) $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' not in date format"), $field); break; case "datetime": if (! preg_match ("#^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$#", $values[$field])) $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' not in datetime format"), $field); break; case "time": if (! preg_match ("#^\d{2}:\d{2}:\d{2}$#", $values[$field])) $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' not in time format"), $field); break; default: $errors[$field] = sprintf (dgettext ("domframework", "Field '%s' : invalid SQL type (%s) in ". "\$this->fields "), $field, $this->fieldTypeLight ($field)); } } // - Check the unique entries (if defined) // The primary key is always unique : add it if not set by the user $this->debugLog (" verify the unique constraint"); if (! in_array ($this->primary, $this->unique)) $uniques = array_merge (array ($this->primary), $this->unique); else $uniques = $this->unique; $setValues = $values; if (! array_key_exists ($this->primary, $setValues)) $setValues[$this->primary] = null; foreach ($uniques as $k=>$columns) { if ($update !== false && ! isset ($resUpdate)) { // Can not update multiple UNIQUE rows with the same value $this->debugLog ("CLONE because of update"); $objTmp = clone $this; $objTmp->debugDepth++; $objTmp->clearRequest (); $objTmp->Select (); $objTmp->displayColumn ($this->primary); $objTmp->displayColumn ($columns); $objTmp->whereValues = $this->whereValues; $objTmp->whereExpression = $this->whereExpression; $objTmp->limitLines (3); $resUpdate = $objTmp->execute (); unset ($objTmp); if (count ($resUpdate) > 1) $this->DBException (sprintf (dgettext ("domframework", "Can't update multiple rows with unique value"), $columns)); if (count ($resUpdate) === 0) { // There is no row available with the WHERE clause provided // Skip all the UNIQUE tests as there will not have any modification break; } } $this->debugLog ("CLONE to check primary and unique constraint"); $objTmp = clone $this; $objTmp->debugDepth++; $objTmp->clearRequest (); $objTmp->Select (); $objTmp->displayColumn ($this->primary); if (is_array ($columns)) { // Multiple columns in unique $objTmp->debugLog (" verify the unique multiple [", implode (",", $columns),"]"); foreach ($columns as $column) { if (! array_key_exists ($column, $setValues)) { if (array_key_exists (0, $resUpdate)) { // In UPDATE, if a column is not modified (doesn't appears in // setValues), use the old value to search $valTest = $resUpdate[0][$column]; $objTmp->whereAdd ($column, "=", $valTest); } else $errors[$column] = sprintf (dgettext ("domframework", "No column '%s' defined but must be unique !"), $column); } else $objTmp->whereAdd ($column, "=", $setValues[$column]); } } else { // One column in unique $objTmp->debugLog (" verify the unique one column [$columns]"); if (! array_key_exists ($columns, $setValues)) $errors[$columns] = sprintf (dgettext ("domframework", "No column '%s' defined but must be unique !"), $columns); else $objTmp->whereAdd ($columns, "=", $setValues[$columns]); } if ($update && array_key_exists (0, $resUpdate)) { // If the value already exists, check if it is the same (the SQL can // overwrite with the same value) // If it is not the same, produce an error $objTmp->whereAdd ($this->primary, "!=", $resUpdate[0][$this->primary]); } if (count ($errors) == 0 && count ($objTmp->execute ())) $this->DBException (dgettext ("domframework", "An entry with these values already exists")); unset ($objTmp); } // - If foreign keys, check if the value is set in the constraint foreach ($this->foreign as $field=>$params) { if (! array_key_exists ($field, $this->setValues)) $errors[$field] = dgettext ("domframework", "The field '%s' must be test on foreign, but is not provided"); if (! array_key_exists ($params[0], $this->setForeignObj)) $this->DBException (dgettext ("domframework", "No foreign object configured to test the foreign key")); $this->debugLog ("CLONE to check foreign constraint [$field]"); $objTmp = clone $this->setForeignObj[$params[0]]; $objTmp->debug = $this->debug; $objTmp->debugDepth++; $objTmp->clearRequest (); $objTmp->Select (); $objTmp->displayColumn ($objTmp->primary); $objTmp->whereAdd ($params[1], "=", $this->setValues[$field]); if (count ($objTmp->execute ()) === 0) $errors[$field] = sprintf (dgettext ("domframework", "The value of the foreign key '%s' doesn't exists in foreign table"), $field); } return $errors; } /* }}} */ /** Check the values before doing really the modification of the database * @param boolean|null $update if true UPDATE request, else INSERT request */ public function checkValues ($update = false) /* {{{ */ { $this->debugLog ("Entering checkValues (",$update,")"); $update = !! $update; $values = $this->setValues; $errors = $this->verify ($values, $update); if (count ($errors)) $this->DBException (reset ($errors)); $this->debugLog ("End of checkValues (",$update,") : Nothing in error"); } /* }}} */ /** Execute the pre-defined query * Return the content array if SELECT command is choosed * Return the Last ID if INSERT command is choosed * Return the number of modified lines for UPDATE/DELETE command */ public function execute () /* {{{ */ { $this->debugLog ("Entering execute ()"); switch ($this->command) { case "SELECT": break; case "INSERT": $this->checkValues (false); break; case "UPDATE": $this->checkValues (true); break; case "DELETE": break; default: $this->DBException ("execute : command not defined : no check"); } $this->debugLog ("Entering createRequest ()"); $sql = $this->createRequest (); $this->debugLog ("Entering prepareRequest (",$sql,", ",false,")"); $st = $this->prepareRequest ($sql, false); $this->debugLog ("'",$this->getDisplayQuery (),"'"); $st->execute (); switch ($this->command) { case "SELECT": $result = $st->fetchAll (\PDO::FETCH_NUM); // There is no fetchAll corresponding to the columnName->value. Assign the // name to the value by index. // FETCH_ASSOC doesn't work in empty left join (return NULL instead of // the filled value) if (count ($this->displayColumn)) { if (isset ($this->fieldsTmp)) { $columns = $this->displayColumn; } else { // Remove the columns names as there is no collisions risk $columns = array (); foreach ($this->displayColumn as $col) { $columns[] = substr ($col, 1 + strrpos ($col, $this->sep, -2), -1); } } } elseif (isset ($this->fieldsTmp)) { $columns = array_keys ($this->fieldsTmp); } else { $columns = array_keys ($this->fields); } foreach ($result as $rownb=>$row) { foreach ($row as $colNb=>$val) { // Harmonize the fetchAll result between all the databases drivers if (isset ($this->fieldsTmp)) { if (strtolower ($this->fieldsTmp[$columns[$colNb]][0]) === "integer") $val = intval ($val); } elseif (isset ($this->fields)) { if (strtolower ($this->fields[$columns[$colNb]][0]) === "integer") $val = intval ($val); } $colName = str_replace ($this->sep, "", $columns[$colNb]); $result[$rownb][$colName] = $val; unset ($result[$rownb][$colNb]); } } return $result; case "INSERT": // PostGres need the name of the column autoincrement to return something // If there is no autoincrement column, do not request the lastInsertId $autoInc = null; if ($this->driver === "pgsql") { foreach ($this->fields as $col=>$params) { if (in_array ("autoincrement", $params)) { $autoInc = $col; break; } } if ($autoInc !== null) $autoInc = $this->table."_${col}_seq"; } $this->debugLog ("INSERT: lastInsertId=", self::$instance[$this->dsn]->lastInsertId ($autoInc)); return self::$instance[$this->dsn]->lastInsertId ($autoInc); case "UPDATE": case "DELETE": return $st->rowCount (); default: $this->DBException ("execute : command not defined : no RC"); } } /* }}} */ /** Error management * @param string $message The message to throw in the exception */ public function DBException ($message) /* {{{ */ { $backtrace = debug_backtrace (); if (! array_key_exists (1, $backtrace)) unset ($backtrace); else { $backtrace = end ($backtrace); $filename = basename ($backtrace["file"]); $line = $backtrace["line"]; $method = $backtrace["function"]; $message .= " ($filename:$line [$method])"; } throw new \Exception ($message, 500); } /* }}} */ /** Debug function * @param mixed ...$message The message to display in debug */ private function debugLog ($message) /* {{{ */ { if ((!!$this->debug) === false) return; echo str_repeat ("=", $this->debugDepth * 2)." "; foreach (func_get_args() as $nb=>$arg) { if (is_string ($arg) || is_int ($arg)) echo $arg; elseif (is_bool ($arg) && $arg === true) echo "true"; elseif (is_bool ($arg) && $arg === false) echo "false"; elseif (is_array ($arg)) print_r ($arg); elseif (is_null ($arg)) echo "NULL"; else die ("DEBUG TYPE UNKNOWN ".gettype ($arg)."\n"); } echo "\n"; } /* }}} */ }