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($asConf, $asOptions) { $this->asConf = $asConf; $this->asOptions = $asOptions; parent::__construct(__FILE__, Settings::DEBUG); $this->oConnection = new \mysqli($this->getConf('server'), $this->getConf('user'), $this->getConf('pass')); $this->syncPhpParams($this->getConf('encoding')); $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->getConf('database'))) { $this->addError('Could not find database "'.$this->sDatabase.'"'); $this->sDbState = self::DB_NO_DATA; } elseif(empty($this->getArrayQuery("SHOW TABLES"))) { $this->sDbState = self::DB_NO_TABLE; } else $this->sDbState = self::DB_PEACHY; } } private function getConf($sConf) { return $this->asConf[$sConf] ?? null; } private function syncPhpParams($sEncoding) { //Characters encoding $this->oConnection->set_charset($sEncoding); //SET NAMES //Timezone $this->setQuery("SET time_zone='".date_default_timezone_get()."'"); } public function __destruct() { parent::__destruct(); $this->oConnection->close(); } public function setTrace($bTrace=true) { $this->bTrace = $bTrace; } 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 ".$this->getConf('encoding')." DEFAULT COLLATE ".$this->getConf('encoding')."_general_ci"); $this->oConnection->select_db($this->sDatabase); //Create tables @array_walk($this->getInstallQueries(), array($this, 'setQuery')); } public function getBackup() { $sBackupFile = uniqid('backup_').'.sql'; $sAppPath = ''; if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $sAppPath = 'C:\ProgramData\xampp\mysql\bin\\'; exec($sAppPath.'mysqldump --user='.$this->getConf('user').' --password='.$this->getConf('pass').' '.$this->getConf('database').' --add-drop-table --result-file='.$sBackupFile); if(file_exists($sBackupFile)) { $sBackup = file_get_contents($sBackupFile); unlink($sBackupFile); return $sBackup; } else return false; } public function restoreBackup($sBackupFile) { $sAppPath = ''; if(file_exists($sBackupFile)) { if(strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') $sAppPath = 'C:\ProgramData\xampp\mysql\bin\\'; return exec($sAppPath.'mysql --user='.$this->getConf('user').' --password='.$this->getConf('pass').' '.$this->getConf('database').' < '.$sBackupFile); } else return false; } public function loadFile($sFilePath) { set_time_limit(0); $bResult = false; if(file_exists($sFilePath)) { $sContent = file_get_contents($sFilePath); $sContent = ToolBox::fixEOL($sContent); if(str_replace(ToolBox::FILE_EOL, '', $sContent)!='') { $asLines = explode(ToolBox::FILE_EOL, $sContent); $sSql = ''; foreach ($asLines as $sLine) { $sSql .= trim($sLine); if(substr($sSql, -1)==';') //Multi line SQL { $asResult = $this->setQuery($sSql); if($asResult === false) { $this->addError('SQL failed with error: '.$this->db->error, $sSql); $bResult = false; break; } $bResult = true; $sSql = ''; } else $sSql .= " "; } } else $this->addError('File is empty: '.basename($sFilePath)); } else $this->addError('File not found: '.$sFilePath); return $bResult; } //For debug purposes public function getFullInstallQuery() { $asInstallQueries = $this->getInstallQueries(); return str_replace("\n", "
", implode(";\n\n", $asInstallQueries))."\n\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 = $this->getTablecolumns($sTableName, false); 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 ".$this->getTable($sColumnName)."(`".$sColumnName."`)"; } } } return $asForeignKeyQueries; } private function setQuery($sQuery, $sTypeQuery=__FUNCTION__) { $oResult = $this->getQuery($sQuery, $sTypeQuery); return ($oResult!==false); } 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 isField($sTableFieldName) { $asPath = explode('.', str_replace('`', '', $sTableFieldName)); return ( is_array($asPath) && count($asPath)==2 && $this->isColumnInTable($asPath[0], $asPath[1]) ); } 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ésent dans aucune table'); return false; } } public function getTablecolumns($sTableName, $bTypes=true) { 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'; if(!$bTypes) return $asTableColumns; $asTableName = array_fill(0, count($asTableColumns), $sTableName); return array_combine($asTableColumns, array_map(array('self', 'getColumnType'), $asTableColumns, $asTableName)); } public function isColumnInTable($sTableName, $sColName) { $asCols = $this->getTablecolumns($sTableName, false); return ($asCols && in_array($sColName, $asCols)); } 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 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'])) { if(is_array($this->asOptions['constraints'][$sTableName])) $asTableConstraints = array_merge($asTableConstraints, $this->asOptions['constraints'][$sTableName]); else $asTableConstraints[] = $this->asOptions['constraints'][$sTableName]; } return $asTableConstraints; } private function addQuotes($oData) { //TODO remake $asTrustedFunc = array('CURDATE()', 'NOW()', 'NULL'); $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) || $this->isField($oData)) return $oData; else return $sChar.$oData.$sChar; } } private function getLastId() { return $this->oConnection->insert_id; } private function getLastImpact() { return ($this->oConnection->affected_rows > 0); } 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, $bLedUpdate=true) { return $this->updateRows($sTableName, $asConstraints, $asData, 1, $bLedUpdate); } public function updateRows($sTableName, $asConstraints, $asData, $iLimit=0, $bLedUpdate=true) { if(!is_array($asConstraints)) { $asConstraints = array($this->getId($sTableName)=>$asConstraints); } //Cleaning values $this->cleanSql($sTableName); $this->cleanSql($asData); $this->cleanSql($asConstraints); $asQueryValues = $this->addQuotes($asData); $asConstraintsValues = $this->addQuotes($asConstraints); $this->addColumnSelectors($asQueryValues); $this->addColumnSelectors($asConstraintsValues); //Building query if(!$bLedUpdate) $asQueryValues['led'] = 'led'; $sLimit = $iLimit>0?" LIMIT $iLimit":""; $sQuery = "UPDATE {$sTableName} ". "SET ".$this->implodeAll($asQueryValues, " = ", ", ")." ". "WHERE ".$this->implodeAll($asConstraintsValues, " = ", " AND ").$sLimit; $iResult = false; if($this->setQuery($sQuery)) { if(!$this->getLastImpact()) $this->addNotice('Last query had no effect on db: "'.$sQuery.';"'); $iResult = ($iLimit==1)?$this->selectValue($sTableName, $this->getId($sTableName), $asConstraints):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]); } $iTableId = $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); $bSuccess = true; //linked tables if(array_key_exists('cascading_delete', $this->asOptions) && array_key_exists($sTableName, $this->asOptions['cascading_delete'])) { foreach($this->asOptions['cascading_delete'][$sTableName] as $sTable) { $bSuccess = $bSuccess && $this->deleteRow($sTable, $iTableId); } } $bSuccess = $bSuccess && $this->setQuery("DELETE FROM ".$sTableName." WHERE ".$this->getId($sTableName)." = ".$iTableId); return $bSuccess; } 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'=>""); //Simple selectRows if(!is_array($asInfo)) $asInfo = array('from'=>$asInfo); //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); foreach($asSelection as $sField=>$asConstraints) { if(is_array($asConstraints)) { if(array_key_exists('constOpe', $asInfo) && array_key_exists($sField, $asInfo['constOpe']) && $asInfo['constOpe'][$sField]=='BETWEEN') { //Between $asSelection[$sField] = $asConstraints['from'].' AND '.$asConstraints['to']; $asInfo['constOpe'][$sField] = " BETWEEN "; } else { //Multiple values (IN) $asSelection[$sField] = "(".implode(', ', $asConstraints).")"; $asInfo['constOpe'][$sField] = " IN "; } } elseif(!array_key_exists('constOpe', $asInfo) || !array_key_exists($sField, $asInfo['constOpe'])) $asInfo['constOpe'][$sField] = " = "; } } $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) { $asJoinFields = $this->addQuotes($asJoinFields); $asSimplifiedSelection[$sTable] = $this->implodeAll($asJoinFields, " = ", " AND "); } $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(trim($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('More than 1 result for a selectRow(): '.$iCountNb.' results. Table: '.$sTableName.', constraint: '.self::implodeAll($asConstraints, '=', ' ')); default: $asResult = array_shift($asRows); } return $asResult; } public function selectColumn($sTableName, $asColumnNames, $asConstraints) { $sGroupBy = ''; if(!is_array($asColumnNames)) $asColumnNames = array($asColumnNames); else $sGroupBy = $asColumnNames[0]; return $this->selectRows( array( 'select' => $asColumnNames, 'from' => $sTableName, 'constraint'=> $asConstraints ), $sGroupBy ); } 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 selectId($sTableName, $oConstraints) { return $this->selectValue($sTableName, self::getId($sTableName), $oConstraints); } 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); } } }