Files
objects/inc/mysqlmanager.php
2015-12-10 23:15:37 +01:00

622 lines
18 KiB
PHP
Executable File

<?php
/**
* MySql query manager and generator
* @author franzz
* @version 1.0
*/
class MySqlManager extends PhpObject
{
const DB_NO_CONN = 'ERR_1';
const DB_NO_DATA = 'ERR_2';
const ID_TAG = 'id_';
const MYSQL_TIMESTAMP = 'Y-m-d H:i:s';
public $sDbState;
private $bTrace;
/**
* SQL connection Handle
* @var mysqli
*/
private $oConnection;
private $sDatabase;
private $asOptions;
/**
* Tables & fields descriptions
* array( 'tables'=>array('table_name1'=>array('table_field1', 'table_field2', ...), 'table_name2'=>array(...)),
* 'types'=>array('field1'=>'field_type1', 'field2'=>'field_type2', ...)
* 'constraints'=>array('table_name1'=>'table_contraint1', 'table_name2'=>'table_contraint2', ...),
* 'cascading_delete'=>array('table_name1'=>array('linked_table1', 'linked_table2', ...), 'table_name2'=>...))
* @var Array
*/
public function __construct($sDbServer, $sLogin, $sPass, $sDatabase, $asOptions, $sEncoding='utf8mb4')
{
parent::__construct(__CLASS__, Settings::DEBUG);
$this->sDatabase = $sDatabase;
$this->asOptions = $asOptions;
//$this->oConnection = mysql_connect(self::DB_SERVER, self::DB_LOGIN, self::DB_PASS);
$this->oConnection = new mysqli($sDbServer, $sLogin, $sPass);
$this->syncPhpParams($sEncoding);
/*
$dsn = 'mysql:dbname='.$this->sDatabase.';host='.self::DB_SERVER;
try {$dbh = new PDO($dsn, self::DB_LOGIN, self::DB_PASS);}
catch (PDOException $e) {$this->addError('Connexion échouée : ' . $e->getMessage());}
*/
$this->setTrace(false);
if($this->oConnection->connect_error)
{
$this->addError('bug connection : '.$this->oConnection->connect_error);
$this->sDbState = self::DB_NO_CONN;
}
else
{
if(!$this->oConnection->select_db($this->sDatabase))
{
$this->addError('bug selecting database. Installing...');
$this->sDbState = self::DB_NO_DATA;
}
}
}
private function syncPhpParams($sEncoding)
{
//Characters encoding
$this->oConnection->set_charset($sEncoding); //SET NAMES
//Time zone
$oNow = new DateTime();
$iMins = $oNow->getOffset() / 60;
$iSign = ($iMins < 0)?-1:1;
$iMins = abs($iMins);
$iHours = floor($iMins / 60);
$iMins -= $iHours * 60;
$sOffset = sprintf('%+d:%02d', $iHours*$iSign, $iMins);
$this->setQuery("SET time_zone='{$sOffset}';");
}
public function __destruct()
{
parent::__destruct();
$this->oConnection->close();
}
public function setTrace($bTrace)
{
$this->bTrace = $bTrace;
if($bTrace) $this->setDebug(true);
}
public function getTrace()
{
return $this->bTrace;
}
public function getTables()
{
return array_keys($this->asOptions['tables']);
}
public function install()
{
//Create Database
$this->setQuery("DROP DATABASE IF EXISTS ".$this->sDatabase);
$this->setQuery("CREATE DATABASE ".$this->sDatabase." DEFAULT CHARACTER SET ".Settings::DB_ENC." DEFAULT COLLATE ".Settings::DB_ENC."_general_ci");
$this->oConnection->select_db($this->sDatabase);
//Create tables
@array_walk($this->getInstallQueries(), array($this, 'setQuery'));
}
//For debug purposes
public function getFullInstallQuery()
{
$asInstallQueries = $this->getInstallQueries();
return str_replace("\n", "<br />", implode(";\n\n", $asInstallQueries))."\n\n<!-- \n".implode(";\n\n", $asInstallQueries)."\n -->";
}
private function getInstallQueries()
{
$asTables = $this->getTables();
$asInstallQueries = array_map(array($this, 'getInstallQuery'), $asTables);
$asAlterQueries = $this->getForeignKeyQueries($asTables);
return array_merge($asInstallQueries, $asAlterQueries);
}
private function getInstallQuery($sTableName)
{
$asTableColumns = $this->getTableColumns($sTableName);
$sQuery = "\n".$this->implodeAll($asTableColumns, "` ", "\n", "`", ",")."\n".implode(", \n", $this->getTableConstraints($sTableName));
return "CREATE TABLE `{$sTableName}` ({$sQuery})";
}
private function getForeignKeyQueries($asTableNames)
{
$asForeignKeyQueries = array();
foreach($asTableNames as $sTableName)
{
$asTableColumns = array_keys($this->getTablecolumns($sTableName));
foreach($asTableColumns as $sColumnName)
{
if($this->isId($sColumnName) && $sColumnName!=self::getId($sTableName))
{
$asForeignKeyQueries[] = "ALTER TABLE ".$sTableName." ADD INDEX(`".$sColumnName."`)";
$asForeignKeyQueries[] = "ALTER TABLE ".$sTableName." ADD FOREIGN KEY (`".$sColumnName."`) REFERENCES ".self::getTable($sColumnName)."(`".$sColumnName."`)";
}
}
}
return $asForeignKeyQueries;
}
private function setQuery($sQuery, $sTypeQuery=__FUNCTION__)
{
$this->getQuery($sQuery, $sTypeQuery);
return ($this->oConnection->affected_rows!=0);
}
private function getQuery($sQuery, $sTypeQuery=__FUNCTION__)
{
$sQuery = str_replace(array("\n", "\t"), array(" ", ""), $sQuery);
if($this->getTrace()) $this->addNotice($sQuery);
if(!($oResult = $this->oConnection->query($sQuery)))
{
$this->addError("\nErreur SQL : \n".str_replace("\t", "", $sQuery)."\n\n".str_replace(array("\t", "\n"), "", $this->getLastError()));
}
return $oResult;
}
public function getLastError()
{
return $this->oConnection->error;
}
public function getArrayQuery($sQuery, $bStringOnly=false, $sGroupBy='', $sTypeQuery=__FUNCTION__)
{
$iIndex = 0;
$iColumnCount = 0;
$asResult = array();
$oResult = $this->getQuery($sQuery, true, $sTypeQuery);
if($oResult!==false)
{
while($asCurrentRow = $oResult->fetch_array())
{
if($bStringOnly) $asCurrentRow = $this->arrayKeyFilter($asCurrentRow, 'is_string');
//Add table reel keys
if($sGroupBy!='' && array_key_exists($sGroupBy, $asCurrentRow))
{
$iRowKey = $asCurrentRow[$sGroupBy];
unset($asCurrentRow[$sGroupBy]);
}
else $iRowKey = $iIndex;
//For first loop, check table width
if($iIndex==0) $iColumnCount = count($asCurrentRow);
//One column case : collapse a level
if($iColumnCount==1) $asCurrentRow = array_shift($asCurrentRow);
$asResult[$iRowKey] = $asCurrentRow;
$iIndex++;
}
}
return $asResult;
}
private function getMaxIncrementedValue($sTable)
{
return $this->selectValue($sTable, "MAX(".$this->getId($sTable).")");
}
public static function getId($sTableName, $bFull=false)
{
$sColumnName = self::ID_TAG.self::getText($sTableName);
return $bFull?self::getFullColumnName($sTableName, $sColumnName):$sColumnName;
}
public static function getText($sTableName, $bFull=false)
{
$sColumnName = mb_substr(str_replace('`', '', $sTableName), 0, -1);
$sColumnName = mb_substr($sColumnName, -2)=='ie'?mb_substr($sColumnName, 0, -2).'y':$sColumnName;
return $bFull?self::getFullColumnName($sTableName, $sColumnName):$sColumnName;
}
public static function getFullColumnName($sTableName, $sColumnName)
{
return $sTableName.".".$sColumnName;
}
private function isId($sColumnName, $sTableName='')
{
$asTables = ($sTableName=='')?$this->getTables():array($sTableName);
$asTableIds = array_map(array('self', 'getId'), $asTables);
return in_array($sColumnName, $asTableIds);
}
private function getTable($sTableId)
{
$asTables = $this->getTables();
$asTableIds = array_map(array('self', 'getId'), $asTables);
if(in_array($sTableId, $asTableIds)) return $asTables[array_search($sTableId, $asTableIds)];
else
{
$this->addError('Id '.$sTableId.' pr&eacute;sent dans aucune table');
return false;
}
}
public function getTablecolumns($sTableName)
{
if(!array_key_exists($sTableName, $this->asOptions['tables'])) return false;
$asTableColumns = array(self::getId($sTableName));
foreach($this->asOptions['tables'][$sTableName] as $sFieldName) $asTableColumns[] = $sFieldName;
$asTableColumns[] = 'led';
$asTableName = array_fill(0, count($asTableColumns), $sTableName);
return array_combine($asTableColumns, array_map(array('self', 'getColumnType'), $asTableColumns, $asTableName));
}
private function getColumnType($sColumnName, $sTableName)
{
$sColumnType = '';
switch($sColumnName)
{
case array_key_exists($sColumnName, $this->asOptions['types']):
$sColumnType = $this->asOptions['types'][$sColumnName];
break;
case 'led':
$sColumnType = "TIMESTAMP NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP";
break;
case $this->isId($sColumnName, $sTableName):
$sColumnType = "int(10) UNSIGNED auto_increment";
break;
case $this->isId($sColumnName):
$sColumnType = "int(10) UNSIGNED";
break;
}
return $sColumnType;
}
private function getTableConstraints($sTableName)
{
//Primary key
$asTableConstraints = array('PRIMARY' => "PRIMARY KEY (`".self::getId($sTableName)."`)");
//Foreign keys: applied using ALTER TABLE syntax at the end to prevent scheduling CREATE TABLE queries
//Other constraints
if(array_key_exists($sTableName, $this->asOptions['constraints'])) $asTableConstraints[] = $this->asOptions['constraints'][$sTableName];
return $asTableConstraints;
}
private function addQuotes($oData)
{
//TODO remake
$asTrustedFunc = array('CURDATE()', 'NOW()');
$sChar = "'";
if(is_array($oData))
{
$asChar = array_fill(1, count($oData), $sChar);
return array_combine(array_keys($oData), array_map(array($this, 'addQuotes'), $oData, $asChar));
}
else
{
if(in_array($oData, $asTrustedFunc)) return $oData;
else return $sChar.$oData.$sChar;
}
}
private function getLastId()
{
return $this->oConnection->insert_id;
}
public function insertRow($sTableName, $asData)
{
$this->cleanSql($sTableName);
$this->cleanSql($asData);
$asQueryValues = $this->addQuotes($asData);
$sQuery = "INSERT INTO ".$sTableName." (`".implode("`, `", array_keys($asQueryValues))."`) VALUES (".implode(", ", $asQueryValues).")";
return $this->setQuery($sQuery)?$this->getLastId():0;
}
public function updateRow($sTableName, $asConstraints, $asData)
{
return $this->updateRows($sTableName, $asConstraints, $asData, 1);
}
public function updateRows($sTableName, $asConstraints, $asData, $iLimit=0)
{
$sTableIdCol = self::getId($sTableName);
if(!is_array($asConstraints))
{
$asConstraints = array($sTableIdCol=>$asConstraints);
}
$this->cleanSql($sTableName);
$this->cleanSql($iTableId);
$this->cleanSql($asData);
$this->cleanSql($asConstraints);
$asQueryValues = $this->addQuotes($asData);
$asConstraintsValues = $this->addQuotes($asConstraints);
$this->addColumnSelectors($asQueryValues);
$this->addColumnSelectors($asConstraintsValues);
$sLimit = $iLimit>0?" LIMIT $iLimit":"";
$sQuery = "UPDATE {$sTableName} ".
"SET ".$this->implodeAll($asQueryValues, " = ", ", ")." ".
"WHERE ".$this->implodeAll($asConstraintsValues, " = ", " AND ").$sLimit;
$iResult = false;
if($this->setQuery($sQuery))
{
$this->setTrace(true);
if($iLimit==1)
{
$iResult = array_key_exists($sTableIdCol, $asConstraints)?$asConstraints[$sTableIdCol]:$this->selectValue($sTableName, $sTableIdCol, $asConstraints);
}
else $iResult = true;
}
return $iResult;
}
public function insertUpdateRow($sTableName, $asData, $asKeys=array(), $bUpdate=true)
{
$sTableIdName = self::getId($sTableName);
//check for data in the db
if($asKeys==array())
{
$asKeys[] = $sTableIdName;
}
$asValues = array_intersect_key($asData, array_flip($asKeys));
$iTableId = $this->selectValue($sTableName, $sTableIdName, $asValues);
//insert
if(!$iTableId)
{
$iTableId = $this->insertRow($sTableName, $asData);
}
//Update
elseif($bUpdate)
{
if(array_key_exists($sTableIdName, $asData))
{
unset($asData[$sTableIdName]);
}
$this->updateRow($sTableName, $iTableId, $asData);
}
return $iTableId;
}
public function selectInsert($sTableName, $asData, $asKeys=array())
{
return $this->insertUpdateRow($sTableName, $asData, $asKeys, false);
}
public function deleteRow($sTableName, $iTableId)
{
$this->cleanSql($sTableName);
$this->cleanSql($iTableId);
//linked tables
switch($sTableName)
{
case array_key_exists($sTableName, $this->asOptions['cascading_delete']) :
$asTables = array_filter(is_array($sTableName)?$sTableName:array($sTableName) + $this->asOptions['cascading_delete'][$sTableName]);
break;
case is_string($sTableName) :
$asTables = array($sTableName);
break;
case is_array($sTableName):
$asTables = $sTableName;
break;
default:
$asTables = array();
}
foreach($asTables as $sTable)
{
$this->setQuery("DELETE FROM ".$sTable." WHERE ".$this->getId($sTableName)." = ".$iTableId);
}
}
public function emptyTable($sTableName)
{
$this->cleanSql($sTableName);
return $this->setQuery("TRUNCATE ".$sTableName);
}
public function selectList($sTableName, $sColumnName='', $asConstraints=array())
{
$sColumnName = $sColumnName==''?self::getText($sTableName):$sColumnName;
$sIdColumnName = self::getId($sTableName);
return $this->selectRows( array( 'select' => array($sIdColumnName, $sColumnName),
'from' => $sTableName,
'constraint'=> $asConstraints),
true,
$sIdColumnName);
}
public function selectRows($asInfo, $sGroupBy='', $bStringOnly=true)
{
$asAttributes = array('select'=>"SELECT", 'from'=>"FROM", 'join'=>"LEFT JOIN", 'joinOn'=>"LEFT JOIN", 'constraint'=>"WHERE", 'groupBy'=>"GROUP BY", 'orderBy'=>"ORDER BY", 'limit'=>'LIMIT');
$asRowSeparators = array('select'=>", ", 'from'=>"", 'join'=>" LEFT JOIN ", 'joinOn'=>" LEFT JOIN ", 'constraint'=>" AND ", 'groupBy'=>", ", 'orderBy'=>", ", 'limit'=>"");
$asOperators = array('constraint'=>" = ", 'orderBy'=>" ", 'join'=>" USING(", 'joinOn'=>" ON ");
$asEndOfStatement = array('constraint'=>"", 'orderBy'=>"", 'join'=>")", 'joinOn'=>"");
//Get table by key
if($sGroupBy===true)
{
$sGroupBy = self::getId($asInfo['from']);
//Add id to selection
if(isset($asInfo['select']) && $asInfo['select'][0]!="*") $asInfo['select'][] = $sGroupBy;
}
$sQuery = "";
foreach($asAttributes as $sStatement => $sKeyWord)
{
$asSelection = array_key_exists($sStatement, $asInfo)?$asInfo[$sStatement]:array();
if(!is_array($asSelection))
{
$asSelection = array($asSelection);
}
//if provided values
if(!empty($asSelection))
{
$this->cleanSql($asSelection);
if($sStatement=='constraint' && !array_key_exists('constVar', $asInfo))
{
$asSelection = $this->addQuotes($asSelection);
}
$this->addColumnSelectors($asSelection);
$sQuery .= " ".$sKeyWord." ";
//in case of double value input
if(array_key_exists($sStatement, $asOperators))
{
if($sStatement=='constraint' && array_key_exists('constOpe', $asInfo))
{
$asOperators[$sStatement] = $asInfo['constOpe'];
}
elseif($sStatement=='joinOn')
{
$asSimplifiedSelection = array();
foreach($asSelection as $sTable => $asJoinFields)
{
$asSimplifiedSelection[$sTable] = $this->implodeAll($asJoinFields, " = ", " AND ", $sTable.".", ".[/KEY\]");
}
$asSelection = $asSimplifiedSelection;
}
$sQuery .= $this->implodeAll($asSelection, $asOperators[$sStatement], $asRowSeparators[$sStatement], "", $asEndOfStatement[$sStatement]);
}
else
{
$sQuery .= implode($asRowSeparators[$sStatement], $asSelection);
}
}
//default value for select
elseif($sStatement=='select')
{
$sQuery .= " ".$sKeyWord." * ";
}
}
return $this->getArrayQuery($sQuery, $bStringOnly, $sGroupBy);
}
private function addColumnSelectors(&$asSelection)
{
//FIXME get rid of this
$sSqlWord = 'option';
$sKey = array_search($sSqlWord, $asSelection);
if($sKey!==false)
{
$asSelection[$sKey] = "`".$asSelection[$sKey]."`";
}
elseif(array_key_exists($sSqlWord, $asSelection))
{
$asSelection["`".$sSqlWord."`"] = $asSelection[$sSqlWord];
unset($asSelection[$sSqlWord]);
}
}
public function selectRow($sTableName, $asConstraints=array(), $sColumnName='*')
{
//Table ID directly
if(!is_array($asConstraints)) $asConstraints = array($this->getId($sTableName)=>$asConstraints);
$asRows = $this->selectRows(array('select'=>$sColumnName, 'from'=>$sTableName, 'constraint'=>$asConstraints));
$iCountNb = count($asRows);
switch($iCountNb)
{
case 0 :
$asResult = array();
break;
case $iCountNb > 1 :
$this->addError('Trop de r&eacute;sultats pour un selectRow() : '.$iCountNb.' lignes. Table: '.$sTableName.', contrainte: '.self::implodeAll($asConstraints, '=', ' ').', colonne: '.$sColumnName);
default:
$asResult = array_shift($asRows);
}
return $asResult;
}
public function selectValue($sTableName, $sColumnName, $oConstraints=array())
{
if(!is_array($oConstraints))
{
$oConstraints = array($this->getId($sTableName)=>$oConstraints);
}
$oResult = $this->selectRow($sTableName, $oConstraints, $sColumnName);
return empty($oResult)?false:$oResult;
}
public function pingValue($sTableName, $oConstraints)
{
return $this->selectValue($sTableName, 'COUNT(1)', $oConstraints);
}
public function cleanSql(&$oData)
{
$this->cleanData($oData);
$oData = $this->cleanData($oData);
}
//TODO déplacer dans ToolBox::implodeAll
public static function implodeAll($asText, $asKeyValueSeparator='', $sRowSeparator='', $sKeyPre='', $sValuePost=false)
{
if($sValuePost===false)
{
$sValuePost = $sKeyPre;
}
$asCombinedText = array();
//if unique value for key value separator
if(!is_array($asKeyValueSeparator) && !empty($asText))
{
$asKeyValueSeparator = array_combine(array_keys($asText), array_fill(0, count($asText), $asKeyValueSeparator));
}
$asFrom = array('[/KEY\]', '[/VALUE\]');
foreach($asText as $sKey=>$sValue)
{
$asTo = array($sKey, $sValue);
$sRepKeyPre = str_replace($asFrom, $asTo, $sKeyPre);
$asRepKeyValueSeparator = str_replace($asFrom, $asTo, $asKeyValueSeparator[$sKey]);
$sRepValuePost = str_replace($asFrom, $asTo, $sValuePost);
$asCombinedText[] = $sRepKeyPre.$sKey.$asRepKeyValueSeparator.(is_array($sValue)?implode($sValue):$sValue).$sRepValuePost;
}
return implode($sRowSeparator, $asCombinedText);
}
public static function arrayKeyFilter($asArray, $sCallBack)
{
$asValidKeys = array_flip(array_filter(array_keys($asArray), $sCallBack));
return array_intersect_key($asArray, $asValidKeys);
}
public function cleanData($oData)
{
if(!is_array($oData))
{
return $this->oConnection->real_escape_string($oData);
}
elseif(count($oData)>0)
{
$asKeys = array_map(array($this, 'cleanData'), array_keys($oData));
$asValues = array_map(array($this, 'cleanData'), $oData);
return array_combine($asKeys, $asValues);
}
}
}
?>