Files
DomFramework/dblayer.php
2020-09-07 14:13:56 +00:00

1666 lines
56 KiB
PHP

<?php
/** DomFramework
* @package domframework
* @author Dominique Fournier <dominique@fournier38.fr>
* @license BSD
*/
// dblayer.php
/* Documentation :
The dbLayer provide an abstraction layer on PDO to be easier on all the CRUD
(Create, Read, Update, Delete) operations, accross all the databases engines.
To use it, extends in your code this class, and define the attributes :
- public $table : name of the table you want to use
- public $fields : description of all the fields in the database like :
public $fields = array (
"id"=>array ("integer", "not null", "autoincrement"),
"zone"=>array ("varchar", "255", "not null"),
"viewname"=>array ("varchar", "255"),
"viewclients"=>array ("varchar", "255"),
"comment"=>array ("varchar", "1024"),
"opendate"=>array ("datetime", "not null"),
"closedate"=>array ("datetime"),
);
- public $primary = "id" ; the primary key of the table (in text). Actually
the dbLayer abstraction don't supports primary key on multiples columns
- public $unique = array ("column", array ("column1", "column2");)
- public $foreign : Add the support of foreign keys. Must be defined like :
public $foreign = array (
"localfield"=>array ("foreign Table", "foreign key",
"ON UPDATE CASCADE ON DELETE CASCADE"),
);
Optionnaly, you can add the
- public $debug = TRUE : enable the debug on screen (NOT FOR PROD !!)
*/
require_once ("domframework/verify.php");
/** Permit abstraction on the differents SQL databases available */
class dblayer
{
/** The table name to use */
public $table = null;
/** The tableprefix text to prepend to table name (Should finish by _)
Just allow chars ! */
public $tableprefix = "";
/** The fields with the definition of type, and special parameters */
public $fields = array ();
/** The primary field */
public $primary = null;
/** An array to define the unique fields (or array of unique fields) */
public $unique = null;
/** An array to define the foreign keys of the field */
public $foreign = array ();
/** Debug of the SQL */
public $debug = FALSE;
/** The connecting DSN */
private $dsn = null;
/** The field group delimiter */
private $sep = "";
/** Titles */
public $titles = array ();
/** Define the name of the method to use to verify each entry */
public $verifyOneFunc;
/** Define the name of the method to use to verify all entries */
public $verifyAllFunc;
/** Define the name of the method of hook before read */
public $hookprereadFunc;
/** Define the name of the method of hook after read */
public $hookpostreadFunc;
/** Define the name of the method of hook before insert */
public $hookpreinsertFunc;
/** Define the name of the method of hook after insert */
public $hookpostinsertFunc;
/** Define the name of the method of hook before update */
public $hookpreupdateFunc;
/** Define the name of the method of hook after update */
public $hookpostupdateFunc;
/** Define the name of the method of hook before delete */
public $hookpredeleteFunc;
/** Define the name of the method of hook after delete */
public $hookpostdeleteFunc;
/** The verify unitary stack
@param string $field The name of the field to test
@param string $val The value of the field to test */
public function verifyOne ($field, $val) {}
/** The verify global stack
@param array $data The associative array of contents */
public function verifyAll ($data) {}
// TODO !!
/** Create Table creation from $this->fields with engine abstraction
Example in sqlite3 id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
in MySQL id INTEGER NOT NULL PRIMARY KEY AUTO_INCREMENT, */
// TODO !!
/** Create automatic creation of $fields from .schema of sqlite3/
show create table `NomTable`; for MySQL
SQLite3 : PRAGMA TABLE_INFO('yourtable');
MYSQL : SHOW COLUMNS FROM yourtable;*/
// TODO !!
/** Allow to modify tables if the definition is changed
Attention : SQLite don't supports adding Foreign keys without deleting all
the table, and re-import the data (http://www.sqlite.org/omitted.html) */
/** 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)
{
$this->verifyOneFunc = array ($this, "verifyOne");
$this->verifyAllFunc = array ($this, "verifyAll");
$this->hookprereadFunc = array ($this, "hookpreread");
$this->hookpostreadFunc = array ($this, "hookpostread");
$this->hookpreinsertFunc = array ($this, "hookpreinsert");
$this->hookpostinsertFunc = array ($this, "hookpostinsert");
$this->hookpreupdateFunc = array ($this, "hookpreupdate");
$this->hookpostupdateFunc = array ($this, "hookpostupdate");
$this->hookpredeleteFunc = array ($this, "hookpredelete");
$this->hookpostdeleteFunc = array ($this, "hookpostdelete");
$driver = @explode (":", $dsn);
if (! isset ($driver[0]))
throw new \Exception (dgettext ("domframework", "No valid DSN provided"),
500);
if (! in_array ($driver[0], pdo_drivers ()))
throw new \Exception (sprintf (dgettext ("domframework",
"Driver PDO '%s' not available in PHP"),
$driver[0]), 500);
// 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)))
throw new \Exception (dgettext ("domframework",
"The directory for SQLite database is write protected"),
500);
if (file_exists ($file) && ! is_writeable ($file))
throw new \Exception (dgettext ("domframework",
"The SQLite database file is write protected"),
500);
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))
{
if ($this->debug) echo "CONNECT TO DATABASE\n";
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)
{
throw new \Exception ("PDO error : ".$e->getMessage(), 500);
}
}
// 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))
{
if ($this->debug) echo "CONNECT TO DATABASE\n";
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)
{
throw new \Exception ("PDO error : ".$e->getMessage(), 500);
}
}
// 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))
{
if ($this->debug) echo "CONNECT TO DATABASE\n";
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)
{
throw new \Exception ("PDO error : ".$e->getMessage(), 500);
}
}
// Set the coding to UTF8
self::$instance[$this->dsn]->exec ("SET NAMES 'utf8'");
$this->sep = "\"";
break;
default:
throw new \Exception (dgettext ("domframework",
"Unknown PDO driver provided"), 500);
}
return self::$instance[$this->dsn];
}
/** Return the connected database name from DSN used to connect
*/
public function databasename ()
{
if ($this->sep === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
$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 === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
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:
throw new \Exception (dgettext ("domframework",
"Unknown database driver in listTables"), 500);
}
return $res;
}
/** Verify if the provided data can be inserted/updated in the database.
* @param array $data An array containing the data to verify with keys
* @param mixed|null $updatekey the key to update
* @return an array with the errors in array($key=>array($priority,$message))
*/
public function verify ($data, $updatekey=false)
{
if ($this->debug) echo "== Entering verify\n";
$errors = array ();
foreach ($this->fields as $key=>$params)
{
if ($this->debug) echo " verify ($key)\n";
if ($updatekey === false)
{
// Don't check if there is an update : the database is already filled
// For autoincrement, in INSERT mode, force the value to null
if (in_array ("autoincrement", $params))
$data[$key] = null;
if (in_array ("not null", $params) && !array_key_exists ($key, $data))
{
$errors[$key] = array ("error", sprintf (dgettext ("domframework",
"Mandatory field '%s' not provided"),
$key));
continue;
}
if (in_array ("not null", $params) && $data[$key] === "")
{
$errors[$key] = array ("error", sprintf (dgettext ("domframework",
"Mandatory field '%s' is empty"),
$key));
continue;
}
}
// Do not verify the non provided data (if they are not mandatory)
if (!array_key_exists ($key, $data))
continue;
// Verify the fields, if $verify is defined, before doing insertion
$verify = call_user_func ($this->verifyOneFunc, $key, $data[$key]);
if (is_array ($verify) && count ($verify))
{
$errors[$key] = array ($verify[0], $verify[1]);
//." ". dgettext ("domframework","in")." ".$key);
continue;
}
// Check for type inconsistencies if the value is provided
if (is_null ($data[$key]))
{
// Skipped the removed autoincrement keys
continue;
}
elseif (! is_string ($data[$key]) && ! is_integer ($data[$key]))
{
$errors[$key] = array ("error", sprintf (
dgettext ("domframework",
"Errors in consistency : '%s' is not an integer or a string [is %s]"),
$key, gettype ($data[$key])));
continue;
}
elseif ($data[$key] !== "" && $params[0] === "integer")
{
if (strspn ($data[$key], "0123456789") !== strlen ($data[$key]))
{
$errors[$key] = array ("error", sprintf (
dgettext ("domframework",
"Errors in consistency : '%s' is not an integer"),
$key));
continue;
}
}
elseif ($data[$key] !== "" && $params[0] === "varchar")
{
if (! isset ($params[1]))
{
$errors[$key] = array ("error", sprintf (
dgettext ("domframework",
"The length of varchar field '%s' is not provided"),
$key));
continue;
}
if (mb_strlen ($data[$key]) > $params[1])
{
$errors[$key] = array ("error", sprintf (
dgettext ("domframework",
"Errors in consistency : '%s' data is too long"),
$key));
continue;
}
}
elseif ($data[$key] !== "" && $params[0] === "datetime")
{
// The date format must be in ANSI SQL : YYYY-MM-DD HH:MM:SS
$d = DateTime::createFromFormat("Y-m-d H:i:s", $data[$key]);
if (!$d || $d->format("Y-m-d H:i:s") !== $data[$key])
{
$errors[$key] = array ("error", sprintf (
dgettext ("domframework",
"Incorrect datetime provided for field '%s'"),
$key));
continue;
}
}
elseif ($data[$key] !== "" && $params[0] === "date")
{
// The date format must be in ANSI SQL : YYYY-MM-DD
$d = DateTime::createFromFormat("Y-m-d", $data[$key]);
if (!$d || $d->format("Y-m-d") !== $data[$key])
{
$errors[$key] = array ("error", sprintf (
dgettext ("domframework",
"Incorrect date provided for field '%s'"),
$key));
continue;
}
}
elseif ($data[$key] !== "")
{
$errors[$key] = array ("error", sprintf (dgettext ("domframework",
"Unknown field type for '%s'"), $key));
continue;
}
else
{
// Nothing to do if the value is empty : just save it
}
}
if ($this->debug) echo " verify inconsistency\n";
// Check for inconsistency
$verify = call_user_func ($this->verifyAllFunc, $data);
if (is_null ($verify))
$verify = array ();
$allErrors = array_merge ($errors, $verify);
if (count ($allErrors))
return $allErrors;
$dataOK = array ();
foreach ($this->fields as $field=>$desc)
{
if (array_key_exists ($field, $data))
$dataOK[$field] = $data[$field];
}
if ($updatekey !== false)
{
if ($this->debug) echo " verify in updatekey\n";
// Check if the unique constrain is valid before doing the insertion
// 1. Read the actual state
$before = $this->read (array (array ($this->primary, $updatekey)));
if (count ($before) === 0)
return array ("error", dgettext ("domframework",
"Entry to modify unavailable"));
$before = reset ($before);
// 2. Map the proposal entries into the before state
$after = $before;
foreach ($dataOK as $field=>$val)
$after[$field] = $val;
}
else
{
if ($this->debug) echo " verify NO updatekey\n";
$after = $dataOK;
}
// Check if the unique constrain is valid before doing the insertion
if ($this->unique === null && $this->primary !== null)
$this->unique = array ($this->primary);
// TODO : If Unique is not defined nor primary
// FIXME : If unique is defined, could I test if the primary key is defined
// in a parameter of the unique and if it is not add it automatically
foreach ($this->unique as $k=>$columns)
{
if ($this->primary === null)
{
return array (dgettext ("domframework",
"No field primary defined for tests in primary"));
}
if (is_array ($columns))
{
// Multiple columns in unique
if ($this->debug) echo " verify unique multiple $k\n";
$select = array ();
if ($updatekey !== false)
$select[] = array ($this->primary, $updatekey, "!=");
foreach ($columns as $col)
{
if (!array_key_exists ($col, $after)) continue;
$select[] = array ($col, $after[$col]);
}
// If there is only the primary key, there is no chance to have a
// conflict
// Before updating, check if the new values are not creating an error
$rc = $this->read ($select, array ($this->primary));
if (count ($rc) > 0)
{
$errors[] = array ("error", dgettext ("domframework",
"An entry with these values already exists"));
continue;
}
}
else
{
// One column in unique
if ($this->debug) echo " verify unique one column $k\n";
if (!array_key_exists ($columns, $after)) continue;
$select = array ();
if ($updatekey !== false)
{
// This line have a problem to update a tuple with the same values
// if ($columns === $this->primary)
$select[] = array ($this->primary, $updatekey, "!=");
}
$select[] = array ($columns, $after[$columns]);
$rc = $this->read ($select,
array ($this->primary));
if (count ($rc) > 0)
{
$errors[] = array ("error", dgettext ("domframework",
"An entry with this value already exists"));
continue;
}
}
}
// Check if the foreign keys constrains are valid before doing the insertion
foreach ($this->foreign as $foreign=>$d)
{
if ($updatekey === false)
{
// Before doing the insert, check the foreign keys. In update, they are
// not mandatory and are not checked for existancy.
if ($this->debug) echo " verify foreign $foreign\n";
if (! isset ($data[$foreign]))
{
$errors[] = array ("error", sprintf (dgettext ("domframework",
"The foreign column '%s' is not provided"),
$foreign));
return $errors;
}
if (! isset ($data[$foreign][0]))
{
$errors[] = array ("error", sprintf (dgettext ("domframework",
"The field type for column '%s' is not provided"),
$foreign));
return $errors;
continue;
}
}
else
{
if (! array_key_exists ($foreign, $d))
continue;
}
$table = $d[0];
$column = $d[1];
$req = "SELECT $this->sep$column$this->sep ".
"FROM $this->sep$this->tableprefix$table$this->sep ".
"WHERE $this->sep$column$this->sep=:".md5 ($column);
if ($this->debug) echo "DEBUG : $req\n";
$st = self::$instance[$this->dsn]->prepare ($req);
$val = $data[$foreign];
$key = $column;
if ($this->debug) echo "DEBUG BIND : ".$this->fields[$foreign][0]."\n";
if ($this->debug) echo "DEBUG BIND : $column(".md5 ($column)."->".
var_export ($val, TRUE)."\n";
if ($val === null)
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_NULL);
elseif ($this->fields[$foreign][0] === "integer")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_INT);
elseif ($this->fields[$foreign][0] === "varchar")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
elseif ($this->fields[$foreign][0] === "datetime")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
elseif ($this->fields[$foreign][0] === "date")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
else
{
throw new \Exception ("TO BE DEVELOPPED : ".$this->fields[$foreign][0],
500);
}
$st->execute ();
$res = array ();
while ($d = $st->fetch (PDO::FETCH_ASSOC))
$res[] = $d;
if (count ($res) === 0)
{
$errors[] = array ("error", sprintf (dgettext ("domframework",
"The foreign key '%s' doesn't exists"),
$column));
continue;
}
}
return $errors;
}
/** Insert a new line of data in the table. Datas must be an indexed array
* @param array $data Datas to be recorded (column=>value)
*/
public function insert ($data)
{
if ($this->debug) echo "== Entering insert\n";
if ($this->sep === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
if ($this->table === null)
throw new \Exception (dgettext ("domframework",
"No table name defined to insert in the table"),
500);
if ($this->unique === null)
throw new \Exception (dgettext ("domframework",
"Unique fields of table are not defined"), 500);
if (! is_array ($this->unique))
throw new \Exception (dgettext ("domframework",
"The unique configuration is not an array"), 500);
if (!is_array ($data))
throw new \Exception (dgettext ("domframework",
"The data provided to create are not array"),
405);
foreach ($this->fields as $key=>$params)
{
if (in_array ("autoincrement", $params))
$data[$key] = null;
}
if (!in_array ($this->primary, $this->unique))
$this->unique[] = $this->primary;
$dataOK = array ();
$errors = $this->verify ($data);
if (count ($errors) !== 0)
{
$errors = reset ($errors);
if (! is_array ($errors))
$errors = array (0=>"error", 1=>$errors);
throw new \Exception ($errors[1], 405);
}
foreach ($this->fields as $field=>$desc)
{
if (isset ($data[$field]))
$dataOK[$field] = $data[$field];
}
$binds = array_keys ($dataOK);
array_walk ($binds, function(&$value, $key) {
$value = md5 ($value);
});
$dataOK = call_user_func ($this->hookpreinsertFunc, $dataOK);
$req = "INSERT INTO $this->sep$this->tableprefix$this->table$this->sep ";
$req .= "($this->sep".
implode ("$this->sep,$this->sep", array_keys ($dataOK)).
"$this->sep)";
$req .= " VALUES ";
$req .= "(:".implode (",:", $binds).")";
if ($this->debug) echo "DEBUG : $req\n";
$st = self::$instance[$this->dsn]->prepare ($req);
foreach ($dataOK as $key=>$val)
{
if ($this->debug) echo "DEBUG BIND : $key(".md5 ($key).")->".
var_export ($val, TRUE)."\n";
if ($val === null)
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_NULL);
elseif ($this->fields[$key][0] === "integer")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_INT);
elseif ($this->fields[$key][0] === "varchar")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
elseif ($this->fields[$key][0] === "datetime")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
elseif ($this->fields[$key][0] === "date")
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
else
throw new \Exception ("TO BE DEVELOPPED : ".$this->fields[$key][0],
500);
}
try
{
$st->execute ();
}
catch (\Exception $e)
{
echo "dblayer execute exception : ".$e->getMessage()."\n";
exit;
}
if (key_exists ($this->primary, $data) &&
! in_array ("autoincrement", $this->fields[$this->primary]) &&
$data[$this->primary] !== null)
$lastID = $data[$this->primary];
elseif (key_exists ($this->primary, $data) &&
in_array ("autoincrement", $this->fields[$this->primary]) &&
$data[$this->primary] !== null)
$lastID = $data[$this->primary];
else
$lastID = self::$instance[$this->dsn]->lastInsertId();
$lastID = call_user_func ($this->hookpostinsertFunc, $dataOK, $lastID);
return $lastID;
}
/** Read the table content based on a select filter, ordered by order
* operator and the associated select value
* @param array|null $select Rows to select with
* $select = array (array ($key, $val, $operator), ...)
* $key=>column, $val=>value to found, $operator=>'LIKE', =...
* @param array|null $display Columns displayed
* $display = array ($col1, $col2...);
* @param array|null $order Sort the columns by orientation
* $order = array (array ($key, $orientation), ...)
* $key=>column, $orientation=ASC/DESC
* @param boolean|null $whereOr The WHERE parameters are separated by OR
* instead of AND
* @param array|null $foreignSelect Add a filter on foreign keys
* @return array array ([0] => array (column=>value, column=>value),);
*/
public function read ($select=null, $display=null, $order=null,
$whereOr=false, $foreignSelect=null)
{
if ($this->debug) echo "== Entering read\n";
if ($this->sep === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
if ($this->table === null)
throw new \Exception (dgettext ("domframework",
"No table name defined to read the table"), 500);
if ($select !== null && !is_array ($select))
throw new \Exception (dgettext ("domframework",
"Select information provided is not an array"),
405);
if ($display !== null && !is_array ($display))
throw new \Exception (dgettext ("domframework",
"Display information provided is not an array"),
405);
if ($order !== null && !is_array ($order))
throw new \Exception (dgettext ("domframework",
"Order information provided is not an array"),
405);
if ($display !== null)
{
foreach ($display as $f)
{
if (!in_array ($f, array_keys ($this->fields)))
throw new \Exception (sprintf (dgettext ("domframework",
"Field '%s' not allowed"), $f), 500);
}
}
else
{
$display = array_keys ($this->fields);
}
$foreignSelectCols = array ();
if ($foreignSelect !== null)
{
foreach ($foreignSelect as $s)
$foreignSelectCols[] = $s[0];
}
call_user_func_array ($this->hookprereadFunc,
array (&$select, &$display, &$order,
&$whereOr, &$foreignSelect));
$req = "SELECT $this->sep";
$req .= implode ("$this->sep,$this->sep", $display);
$req .= "$this->sep ";
$req .= "FROM $this->sep$this->tableprefix$this->table$this->sep";
if ($select !== null || $foreignSelect !== null)
{
$req .= " WHERE (";
}
if ($select !== null)
{
// TODO Allow a field=>value in plus of array("field","value")
foreach ($select as $n=>$s)
{
if (! is_array ($s))
throw new \Exception ("Select not an array for element $n", 500);
// The foreign keys can not be in the select too (conflict)
if (in_array ($s[0],$foreignSelectCols))
continue;
if (! array_key_exists (0, $s))
throw new \Exception ("Select field for key $n not provided", 406);
if (! array_key_exists (1, $s))
throw new \Exception ("Select value for key $n not provided", 406);
if ($n > 0)
{
if ($whereOr === false)
$req .= " AND";
else
$req .= " OR ";
}
if (!isset ($s[2]))
$s[2] = "=";
if (!isset ($s[0]))
throw new \Exception (sprintf (dgettext ("domframework",
"Select not found for id=%d"), $n), 500);
// The double-quotes are added for sqlite to escape the column if its
// name is 'group'
// Don't put single quotes : don't work with SQLite
// TODO : Test for PostgreSQL (Tested for SQLite and MySQL)
if ($s[1] !== null)
$req .= " $this->sep".$s[0]."$this->sep ".$s[2]." :".
md5 ($s[0].$s[1]);
else
$req .= " $this->sep".$s[0]."$this->sep IS NULL";
}
$req .=")";
}
if ($select !== null && $foreignSelect !== null)
$req .= " AND (";
if ($foreignSelect !== null)
{
// TODO Allow a field=>value in plus of array("field","value")
foreach ($foreignSelect as $n=>$s)
{
if (! array_key_exists (0, $s))
throw new \Exception ("Foreign field for key $n not provided", 406);
if (! array_key_exists (1, $s))
throw new \Exception ("Foreign value for key $n not provided", 406);
if ($n > 0)
{
$req .= " AND";
}
if (!isset ($s[2]))
$s[2] = "=";
$req .= " $this->sep".$s[0]."$this->sep ".$s[2]." :".md5 ($s[0].$s[1]);
}
$req .=")";
}
if ($order !== null)
{
$req .= " ORDER BY ";
foreach ($order as $n=>$o)
{
if ($n > 0)
$req .= ",";
$req .= $this->sep.$o[0].$this->sep;
if (isset ($o[1]) && strtoupper ($o[1]) === "DESC")
$req .= " DESC";
else
$req .= " ASC";
}
}
if ($this->debug) echo "DEBUG : $req\n";
try
{
$st = self::$instance[$this->dsn]->prepare ($req);
}
catch (\Exception $e)
{
if ($this->debug) echo "DEBUG : PREPARE ERROR ! Return FALSE".
$e->getMessage()."\n";
throw new \Exception ($e->getMessage(), 500);
}
if ($select !== NULL)
{
foreach ($select as $s)
{
if ($s[1] === null)
continue;
if ($this->debug)
echo "DEBUG BIND : ".$s[0]."(".md5 ($s[0].$s[1]).")->".
var_export ($s[1], TRUE)."\n";
$st->bindValue (":".md5 ($s[0].$s[1]), $s[1]);
}
}
if ($foreignSelect !== null)
{
foreach ($foreignSelect as $s)
{
if ($this->debug)
echo "DEBUG BIND : ".$s[0]."(".md5 ($s[0].$s[1]).")->".
var_export ($s[1], TRUE)."\n";
$st->bindValue (":".md5 ($s[0].$s[1]), $s[1]);
}
}
$rc = $st->execute ();
if ($rc === false)
{
if ($this->debug) echo "DEBUG : EXECUTE ERROR ! Return FALSE\n";
}
$res = array ();
while ($d = $st->fetch (PDO::FETCH_ASSOC))
$res[] = $d;
$res = call_user_func ($this->hookpostreadFunc, $res);
return $res;
}
/** Update the key tuple with the provided data
* Return the number of rows modified
* @param string|integer $updatekey The key applied on primary key to be
* updated
* @param array $data The values to be updated
* @return the number of lines modified
*/
public function update ($updatekey, $data)
{
if ($this->debug) echo "== Entering update\n";
if ($this->sep === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
if ($this->table === null)
throw new \Exception (dgettext ("domframework",
"No table name defined to update in the table"),
500);
if (count ($this->fields) === 0)
throw new \Exception (dgettext ("domframework", "No Field defined"), 500);
if ($this->primary === null)
throw new \Exception (dgettext ("domframework", "No Primary defined"),
500);
if (count ($data) === 0)
throw new \Exception (dgettext ("domframework",
"No data to update provided"), 500);
$dataOK = array ();
$errors = $this->verify ($data, $updatekey);
if (count ($errors) !== 0)
{
if (is_array ($errors))
{
if (isset ($errors[0][1]))
throw new \Exception ($errors[0][1], 405);
$err = reset ($errors);
throw new \Exception ($err[1], 405);
}
throw new \Exception ($errors[1], 405);
}
foreach ($this->fields as $field=>$desc)
{
if (array_key_exists ($field, $data))
$dataOK[$field] = $data[$field];
}
$dataOK = call_user_func ($this->hookpreupdateFunc, $updatekey, $dataOK);
$req = "UPDATE $this->sep".$this->tableprefix."$this->table$this->sep SET ";
$i = 0;
foreach ($dataOK as $key=>$val)
{
if ($i>0) $req .= ",";
$req .= "$this->sep$key$this->sep=:".md5 ($key);
$i++;
}
$req .= " WHERE $this->sep$this->primary$this->sep=:".
md5 ("PRIMARY".$this->primary);
if ($this->debug) echo "DEBUG : $req\n";
$st = self::$instance[$this->dsn]->prepare ($req);
// Add the primary key to field list temporaly. It will permit to update the
// primary key
$fields = $this->fields;
$dataOK["PRIMARY".$this->primary] = $updatekey;
$fields["PRIMARY".$this->primary] = $this->fields[$this->primary];
foreach ($dataOK as $key=>$val)
{
if ($this->debug) echo "DEBUG BIND : $key(".md5 ($key).")->".
var_export ($val, TRUE)." ";
if ($val === null)
{
if ($this->debug) echo "(null)\n";
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_NULL);
}
elseif ($fields[$key][0] === "integer")
{
if ($this->debug) echo "(integer)\n";
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_INT);
}
elseif ($fields[$key][0] === "varchar")
{
if ($this->debug) echo "(varchar)\n";
$st->bindValue (":".md5 ($key), "$val", PDO::PARAM_STR);
}
elseif ($fields[$key][0] === "datetime")
{
if ($this->debug) echo "(datetime)\n";
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
}
elseif ($fields[$key][0] === "date")
{
if ($this->debug) echo "(date)\n";
$st->bindValue (":".md5 ($key), $val, PDO::PARAM_STR);
}
else
{
if ($this->debug) echo "(UNKNOWN)\n";
throw new \Exception ("TO BE DEVELOPPED : ".$fields[$key][0], 500);
}
}
$st->execute ();
$nbLinesUpdated = $st->rowCount ();
$nbLinesUpdated = call_user_func ($this->hookpostupdateFunc, $updatekey,
$dataOK, $nbLinesUpdated);
return $nbLinesUpdated;
}
/** Delete a tuple identified by its primary key
* Return the number of deleted rows (can be 0 !)
* @param string|integer $deletekey The key of primary key to be deleted
*/
public function delete ($deletekey)
{
if ($this->debug) echo "== Entering delete\n";
if ($this->sep === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
if ($this->table === null)
throw new \Exception (dgettext ("domframework",
"No table name defined to delete in the table"),
500);
$deletekey = call_user_func ($this->hookpredeleteFunc, $deletekey);
$req = "DELETE FROM $this->sep$this->tableprefix$this->table$this->sep ";
$req .= "WHERE $this->primary = :primary";
$st = self::$instance[$this->dsn]->prepare ($req);
if ($this->debug) echo "DEBUG : $req\n";
if ($this->debug) echo "DEBUG BIND : primary->".
var_export ($deletekey, TRUE)."\n";
$st->bindValue (":primary", $deletekey);
try
{
$st->execute ();
}
catch (\Exception $e)
{
throw new \Exception ($e->getMessage (), 500);
}
$nbLinesDeleted = $st->rowCount();
$nbLinesDeleted = call_user_func ($this->hookpostdeleteFunc, $deletekey,
$nbLinesDeleted);
return $nbLinesDeleted;
}
/** Translation of fieldsi
*/
public function titles ()
{
if ($this->debug) echo "== Entering titles\n";
if (count ($this->fields) === 0)
throw new \Exception (dgettext ("domframework", "No Field defined"), 500);
$arr = array ();
if (count ($this->titles) !== 0)
{
foreach ($this->titles as $field=>$v)
$arr[$field] = $field;
}
else
{
foreach ($this->fields as $field=>$v)
$arr[$field] = $field;
}
return $arr;
}
/** Drop the table
*/
public function dropTable ()
{
if ($this->debug) echo "== Entering dropTables\n";
if ($this->sep === "")
throw new \Exception (dgettext ("domframework", "Database not connected"),
500);
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";
if ($this->debug)
echo "$sql\n";
return self::$instance[$this->dsn]->exec ($sql);
}
/** 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 ()
{
if ($this->debug) echo "== Entering createTable\n";
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 ";
// Type of field : in $params[0]
if (!isset ($params[0]))
throw new \Exception (sprintf (
dgettext ("domframework",
"No database type defined for field '%s'"),
$field), 500);
switch ($params[0])
{
case "integer":
$sql .= "INTEGER";
$params = array_slice ($params, 1);
break;
case "varchar":
if (!isset ($params[1]))
throw new \Exception (sprintf (dgettext ("domframework",
"No Size provided for varchar field '%s'"),
$field), 500);
$sql .= "VARCHAR(".$params[1].")";
$params = array_slice ($params, 2);
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'"),
$params[0], $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 for field '%s'"),
$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 ";
// Type of field : in $params[0]
if (!isset ($params[0]))
throw new \Exception (dgettext ("domframework",
"No database type defined for field"), 500);
switch ($params[0])
{
case "integer":
$sql .= "INTEGER";
$params = array_slice ($params, 1);
break;
case "varchar":
if (!isset ($params[1]))
throw new \Exception (dgettext ("domframework",
"No Size provided for varchar field"), 500);
$sql .= "VARCHAR(".$params[1].")";
$params = array_slice ($params, 2);
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 parameter for field '%s'"),
$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
{
// Type of field : in $params[0]
if (!isset ($params[0]))
throw new \Exception (sprintf (
dgettext ("domframework",
"No database type defined for field '%s'"),
$field), 500);
switch ($params[0])
{
case "integer":
$sql .= "INTEGER";
$params = array_slice ($params, 1);
break;
case "varchar":
if (!isset ($params[1]))
throw new \Exception (sprintf (
dgettext ("domframework",
"No Size provided for varchar field '%s'"),
$field), 500);
$sql .= "VARCHAR(".$params[1].")";
$params = array_slice ($params, 2);
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 for field '%s'"),
$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 dbLayer"), 500);
}
if ($this->debug)
echo "$sql\n";
return self::$instance[$this->dsn]->exec ($sql);
}
/** This function permit to send a SQL request to the database to do a SELECT
* Return the an array with the data
* @param string $sql A valid SQL request to be execute
*/
public function directRead ($sql)
{
if ($this->debug) echo "== Entering directRead\n";
if ($this->debug)
echo "$sql\n";
$st = self::$instance[$this->dsn]->prepare ($sql);
$st->execute ();
$res = array ();
while ($d = $st->fetch (PDO::FETCH_ASSOC))
$res[] = $d;
return $res;
}
/** This function disconnect the database. It is normally only used in phpunit
* unit tests
*/
public function disconnect ()
{
unset (self::$instance[$this->dsn]);
}
/** The prepare method
* @param string $statement The valid template to be replaced
* @param array|null $driver_options The replacement to be done
*/
public function prepare ($statement, $driver_options = array())
{
return self::$instance[$this->dsn]->prepare ($statement, $driver_options);
}
/** 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 ();
}
/** Hook preread
* This hook is run before selecting the data in the database, after the
* verification
* @param array|null &$select Rows to select with
* $select = array (array ($key, $val, $operator), ...)
* $key=>column, $val=>value to found, $operator=>'LIKE', =...
* @param array|null &$display Columns displayed
* $display = array ($col1, $col2...);
* @param array|null &$order Sort the columns by orientation
* $order = array (array ($key, $orientation), ...)
* $key=>column, $orientation=ASC/DESC
* @param boolean|null &$whereOr The WHERE parameters are separated by OR
* instead of AND
* @param array|null &$foreignSelect Add a filter on foreign keys
*/
public function hookpreread (&$select, &$display, &$order, &$whereOr,
&$foreignSelect)
{
}
/** Hook postread
* This hook is run after selecting the data.
* @param array $data the data selected by the select
* @return array The data modified by the hook
*/
public function hookpostread ($data)
{
return $data;
}
/** Hook preinsert
* This hook is run before inserting a new data in the database, after the
* verification
* @param array $data the data to insert in the database
* @return the modified data
*/
public function hookpreinsert ($data)
{
return $data;
}
/** Hook postinsert
* This hook is run after successfuly insert a new data in the database
* @param array $data the data selected by the select
* @param integer $lastID the insert identifier
* @return the modified lastID
*/
public function hookpostinsert ($data, $lastID)
{
return $lastID;
}
/** Hook preupdate
* This hook is run before updating a data in the database, after the
* verification
* @param string $updatekey the key to be modify
* @param array $data the data selected by the select
* @return the modified data
*/
public function hookpreupdate ($updatekey, $data)
{
return $data;
}
/** Hook postupdate
* This hook is run after successfuly update a data in the database
* @param string $updatekey the key which was modified
* @param array $data the data selected by the select
* @param integer $nbLinesUpdated The number of modified lines
* @return the modified $nbLinesUpdated
*/
public function hookpostupdate ($updatekey, $data, $nbLinesUpdated)
{
return $nbLinesUpdated;
}
/** Hook predelete
* This hook is run before deleting a data in the database
* @param string $deletekey The key to be removed
* @return the modified $deletekey
*/
public function hookpredelete ($deletekey)
{
return $deletekey;
}
/** Hook postdelete
* This hook is run after successfuly deleting a data in the database
* @param string $deletekey The removed key
* @param integer $nbLinesDeleted The number of deleted lines
* @return $nbLinesUpdated
*/
public function hookpostdelete ($deletekey, $nbLinesDeleted)
{
return $nbLinesDeleted;
}
///////////////////
/// SETTERS ///
///////////////////
/** Set the table property
* @param string $table The table name to use
*/
public function tableSet ($table)
{
$this->table = $table;
return $this;
}
/** Set the tableprefix property
* @param string $tableprefix The prefix to add in table names
*/
public function tableprefixSet ($tableprefix)
{
$this->tableprefix = $tableprefix;
return $this;
}
/** Set the fields property
* @param array $fields Set the definition of the fields
*/
public function fieldsSet ($fields)
{
$this->fields = $fields;
return $this;
}
/** Set the primary property
* @param string $primary The primary field
*/
public function primarySet ($primary)
{
$this->primary = $primary;
return $this;
}
/** Set the unique property
* @param array $unique The array of the unique contraints
*/
public function uniqueSet ($unique)
{
$this->unique = $unique;
return $this;
}
/** Set the foreign property
* @param array $foreign The foreign array
*/
public function foreignSet ($foreign)
{
$this->foreign = $foreign;
return $this;
}
/** Set the debug property
* @param integer $debug The debug value
*/
public function debugSet ($debug)
{
$this->debug = $debug;
return $this;
}
/** Set the dsn property
* @param string $dsn The DSN to use
*/
public function dsnSet ($dsn)
{
$this->dsn = $dsn;
return $this;
}
/** Set the titles property
* @param array $titles The titles of the fields
*/
public function titlesSet ($titles)
{
$this->titles = $titles;
return $this;
}
}
/** POC :
error_reporting (E_ALL);
require_once ("domframework/dbLayer.php");
class zone extends dbLayer
{
// The database must be initialized with
// CREATE TABLE `dns_zones` (id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
// zone VARCHAR(255) NOT NULL,
// viewname VARCHAR(255),
// viewclients VARCHAR(255),
// comment VARCHAR(1024),
// opendate DATETIME NOT NULL,
// closedate DATETIME,
// UNIQUE (zone,viewname));
protected $table = "dns_zones";
protected $fields = array (
"id"=>array ("integer", "not null", "autoincrement"),
"zone"=>array ("varchar", "255", "not null"),
"viewname"=>array ("varchar", "255"),
"viewclients"=>array ("varchar", "255"),
"comment"=>array ("varchar", "1024"),
"opendate"=>array ("datetime", "not null"),
"closedate"=>array ("datetime"),
);
protected $primary = "id";
protected $unique = array ("id", array ("zone", "viewname"));
}
ini_set ("date.timezone", "Europe/Paris");
$zone = new zone ("sqlite:data/database.db");
$last = $zone->create (array ("zone"=>"testZone",
"opendate"=>date("Y-m-d H:i:s")));
//print_r ($zone->read ());
$zone->update (2040, array ("zone"=>"testZone2"));
print_r ($zone->delete ($last));
*/