Source of file DBHelper.php
Size: 41,074 Bytes - Last Modified: 2020-10-24T02:46:31+00:00
/home/travis/build/NextDom/nextdom-core/src/Helpers/DBHelper.php
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158 | <?php /* This file is part of Jeedom. * * Jeedom is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Jeedom is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Jeedom. If not, see <http://www.gnu.org/licenses/>. */ /* This file is part of NextDom Software. * * NextDom is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * NextDom Software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with NextDom Software. If not, see <http://www.gnu.org/licenses/>. */ namespace NextDom\Helpers; use NextDom\Exceptions\CoreException; /** * Class DBHelper * @package NextDom\Helpers */ class DBHelper { const FETCH_TYPE_ROW = 0; const FETCH_TYPE_ALL = 1; const CONNECTION_TIMEOUT = 120; private static $sharedInstance; private static $fieldsCache = []; private static $fieldsQuery = []; private $connection; private $lastConnection; /** * Private constructor for singleton */ private function __construct() { global $CONFIG; if (isset($CONFIG['db']['unix_socket'])) { $this->connection = new \PDO('mysql:unix_socket=' . $CONFIG['db']['unix_socket'] . ';dbname=' . $CONFIG['db']['dbname'], $CONFIG['db']['username'], $CONFIG['db']['password'], [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', \PDO::ATTR_PERSISTENT => true]); } else { $this->connection = new \PDO('mysql:host=' . $CONFIG['db']['host'] . ';port=' . $CONFIG['db']['port'] . ';dbname=' . $CONFIG['db']['dbname'], $CONFIG['db']['username'], $CONFIG['db']['password'], [\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', \PDO::ATTR_PERSISTENT => true]); } } /** * Call procedure * @TODO: Y en a-t-il d'utilisé ? * * @param string $procName Procedure name * @param array $params Parameters * @param int $fetchType Fetch type * @param string|null $className Name of the class * @param null $fetchOpt * @return array|mixed|null * * @throws CoreException */ public static function &CallStoredProc($procName, $params, $fetchType, $className = NULL, $fetchOpt = NULL) { // Generate parameters to bind string $bindParams = str_repeat('?, ', count($params)); // Remove last , $bindParams = trim($bindParams, ', '); if ($className != NULL && class_exists($className)) { return self::Prepare("CALL $procName($bindParams)", $params, $fetchType, \PDO::FETCH_CLASS, $className); } else if ($fetchOpt != NULL) { return self::Prepare("CALL $procName($bindParams)", $params, $fetchType, $fetchOpt, $className); } else { return self::Prepare("CALL $procName($bindParams)", $params, $fetchType); } } /** * Prepare a query and execute * * @param $query * @param array $params * @param int $fetchType * @param int $fetchParam * @param mixed $fetchOpt * * @return array|mixed|null * * @throws CoreException */ public static function &Prepare($query, $params = [], $fetchType = self::FETCH_TYPE_ROW, $fetchParam = \PDO::FETCH_ASSOC, $fetchOpt = NULL) { // Prepare statement $statement = self::getConnection()->prepare($query); $result = NULL; // If execution success if ($statement !== false && $statement->execute($params) !== false) { if ($fetchType == self::FETCH_TYPE_ROW) { if ($fetchOpt === null) { $result = $statement->fetch($fetchParam); } else if ($fetchParam == \PDO::FETCH_CLASS) { $result = $statement->fetchObject($fetchOpt); } } else { if ($fetchOpt === null) { $result = $statement->fetchAll($fetchParam); } else { $result = $statement->fetchAll($fetchParam, $fetchOpt); } } } // Get error $errorInfo = $statement->errorInfo(); // @TODO: Revoir cette chaine if ($errorInfo[0] != 0000) { throw new CoreException('[MySQL] Error code : ' . $errorInfo[0] . ' (' . $errorInfo[1] . '). ' . $errorInfo[2] . ' : ' . $query); } return $result; } /** * Get the connection. Connect to the database if not. * * @return \PDO Connection */ public static function getConnection() { if (!isset(self::$sharedInstance) || self::$sharedInstance->lastConnection + self::CONNECTION_TIMEOUT < time()) { self::$sharedInstance = new self(); } // Store last connection self::$sharedInstance->lastConnection = time(); return self::$sharedInstance->connection; } /** * Get objects from database * * @param string $query SQL query * @param array $params Query params * @param string $objectClassName Class of object * * @return mixed|null Array of instances of the class * * @throws CoreException */ public static function &getAllObjects(string $query, array $params, string $objectClassName) { return self::Prepare($query, $params, self::FETCH_TYPE_ALL, \PDO::FETCH_CLASS, $objectClassName); } /** * Optimize all tables * * @throws CoreException */ public static function optimize() { $tables = self::getAll('SELECT TABLE_NAME FROM information_schema.TABLES WHERE Data_Free > 0', []); foreach ($tables as $table) { $table = array_values($table); $table = $table[0]; self::exec('OPTIMIZE TABLE `' . $table . '`'); } } /** * Get data from database * * @param string $query SQL query * @param array $params Query params * * @return mixed Associative array with data * * @throws CoreException */ public static function &getAll(string $query, array $params = []) { return self::Prepare($query, $params, self::FETCH_TYPE_ALL, \PDO::FETCH_ASSOC); } /** * Execute a query without result (DELETE, UPDATE, INSERT, etc.) * * @param string $query SQL query * @param array $params Query params * * @return bool True on success */ public static function exec(string $query, array $params = []) { $statement = self::getConnection()->prepare($query); if ($statement !== false && $statement->execute($params) !== false) { $errorInfo = $statement->errorInfo(); // TODO: Revoir cette chaine if ($errorInfo[0] != 0000) { return false; } return true; } return false; } /** * Begin transaction */ public static function beginTransaction() { self::getConnection()->beginTransaction(); } /** * Commit transaction */ public static function commit() { self::getConnection()->commit(); } /** * Rollback a transaction */ public static function rollBack() { self::getConnection()->rollBack(); } /** * Saves an entity inside the repository. If the entity is new a new row * will be created. If the entity is not new the row will be updated. * * @param mixed $objToSave Object to save * @param bool $noProcess Don't call process before and after (preSave, preInsert, postSave, etc.) * @param bool $forceReplace @TODO: Force le remplacement si pas d'ID ???? * * @return boolean True on save success * * @throws CoreException * @throws \ReflectionException */ public static function save($objToSave, $noProcess = false, $forceReplace = false) { if (!$noProcess && method_exists($objToSave, 'preSave')) { $objToSave->preSave(); } // Check if id is defined if (!self::getField($objToSave, 'id')) { //New object to save. $fields = self::getFields($objToSave); if (in_array('id', $fields)) { self::setField($objToSave, 'id', null); } if (!$noProcess && method_exists($objToSave, 'preInsert')) { $objToSave->preInsert(); } list($sql, $parameters) = self::buildQuery($objToSave); if ($forceReplace) { $sql = 'REPLACE INTO `' . self::getTableName($objToSave) . '` SET ' . implode(', ', $sql); } else { $sql = 'INSERT INTO `' . self::getTableName($objToSave) . '` SET ' . implode(', ', $sql); } $res = self::getOne($sql, $parameters); $reflection = self::getReflectionClass($objToSave); if ($reflection->hasProperty('id')) { try { self::setField($objToSave, 'id', self::getLastInsertId()); } catch (\Exception $exc) { trigger_error($exc->getMessage(), E_USER_NOTICE); } } if (!$noProcess && method_exists($objToSave, 'postInsert')) { $objToSave->postInsert(); } } else { //Object to update. if (!$noProcess && method_exists($objToSave, 'preUpdate')) { $objToSave->preUpdate(); } $changed = true; if (method_exists($objToSave, 'getChanged')) { $changed = $objToSave->getChanged(); } if ($changed) { list($sql, $parameters) = self::buildQuery($objToSave); if (!$noProcess && method_exists($objToSave, 'getId')) { $parameters['id'] = $objToSave->getId(); //override if necessary } $sql = 'UPDATE `' . self::getTableName($objToSave) . '` SET ' . implode(', ', $sql) . ' WHERE `id` = :id'; $res = self::getOne($sql, $parameters); } else { $res = true; } if (!$noProcess && method_exists($objToSave, 'postUpdate')) { $objToSave->postUpdate(); } } if (!$noProcess && method_exists($objToSave, 'postSave')) { $objToSave->postSave(); } if (method_exists($objToSave, 'setChanged')) { $objToSave->setChanged(false); } return (null !== $res && false !== $res); } /** * Returns the value of a field of a given object. It'll try to use a * getter first if defined. If not defined, we'll use the reflection API. * * @param mixed $targetObject * @param string $field * @return mixed * @throws \ReflectionException */ private static function getField($targetObject, $field) { $result = null; $method = 'get' . ucfirst($field); if (method_exists($targetObject, $method)) { $result = $targetObject->$method(); } else { $reflection = self::getReflectionClass($targetObject); if ($reflection->hasProperty($field)) { $property = $reflection->getProperty($field); $property->setAccessible(true); $result = $property->getValue($targetObject); $property->setAccessible(false); } } if (is_array($result) || is_object($result)) { $result = json_encode($result, JSON_UNESCAPED_UNICODE); } return $result; } /** * Returns the reflection class for the given object. * * @param object $targetObject * @return \ReflectionClass * @throws \ReflectionException */ private static function getReflectionClass($targetObject) { $reflections = []; $uuid = spl_object_hash($targetObject); if (!isset($reflections[$uuid])) { $reflections[$uuid] = new \ReflectionClass($targetObject); } return $reflections[$uuid]; } /** * * * @param mixed $objectToAnalyze * @return array List of fields * @throws \RuntimeException * @throws \ReflectionException */ private static function getFields($objectToAnalyze) { $table = is_string($objectToAnalyze) ? $objectToAnalyze : self::getTableName($objectToAnalyze); if (isset(self::$fieldsCache[$table])) { return self::$fieldsCache[$table]; } $reflection = is_object($objectToAnalyze) ? self::getReflectionClass($objectToAnalyze) : new \ReflectionClass($objectToAnalyze); $properties = $reflection->getProperties(); self::$fieldsCache[$table] = []; foreach ($properties as $property) { $name = $property->getName(); if ('_' !== $name[0]) { self::$fieldsCache[$table][] = $name; } } if (empty(self::$fieldsCache[$table])) { throw new \RuntimeException('No fields found for class ' . get_class($objectToAnalyze)); } return self::$fieldsCache[$table]; } /** * Returns the name of the table where to save entities. * * @param $targetObject * @return string */ private static function getTableName($targetObject) { if (method_exists($targetObject, 'getTableName')) { return $targetObject->getTableName(); } return get_class($targetObject); } /** * Forces the value of a field of a given object, even if this field is * not accessible. * * @param object $targetObject The entity to alter * @param string $field The name of the member to alter * @param mixed $value The value to give to the member * @throws \ReflectionException */ private static function setField($targetObject, $field, $value) { $method = 'set' . ucfirst($field); if (method_exists($targetObject, $method)) { $targetObject->$method($value); } else { $reflection = self::getReflectionClass($targetObject); if ($reflection->hasProperty($field)) { throw new \InvalidArgumentException('Unknown field ' . get_class($targetObject) . '::' . $field); } $property = $reflection->getProperty($field); $property->setAccessible(true); $property->setValue($targetObject, $value); $property->setAccessible(false); } } /** * Builds the elements for an SQL query. It will return two lists, the * first being the list of parts "key= :key" to inject in the SQL, the * second being the mapping of these parameters to the values. * * @param mixed $targetObject * @return array * @throws \ReflectionException */ private static function buildQuery($targetObject) { $parameters = []; $sql = []; foreach (self::getFields($targetObject) as $field) { $sql[] = '`' . $field . '` = :' . $field; $parameters[$field] = self::getField($targetObject, $field); } return [$sql, $parameters]; } /** * Get one row data from database * * @param string $query SQL query * @param array $params Query params * * @return mixed Associative array with data * * @throws CoreException */ public static function &getOne(string $query, array $params = []) { return self::Prepare($query, $params, self::FETCH_TYPE_ROW, \PDO::FETCH_ASSOC); } /** * Get the last insert id * @TODO: ???? Ca me parait dangereux si il y a une écriture en bdd entre temps * @return mixed * @throws CoreException */ public static function getLastInsertId() { if (!isset(self::$sharedInstance)) { throw new CoreException('DB : Aucune connection active - impossible d\'avoir le dernier ID inséré'); } return self::$sharedInstance->connection->lastInsertId(); } /** * Refresh value of an object * * @param mixed $objectToRefresh * * @return bool True if refresh success * * @throws CoreException * @throws \ReflectionException */ public static function refresh($objectToRefresh) { if (is_subclass_of($objectToRefresh, 'EntityInterface') || !self::getField($objectToRefresh, 'id')) { throw new CoreException('DB ne peut rafraîchir l\'objet sans son ID'); } $parameters = ['id' => self::getField($objectToRefresh, 'id')]; $sql = 'SELECT ' . self::buildField(get_class($objectToRefresh)) . ' FROM `' . self::getTableName($objectToRefresh) . '` ' . ' WHERE '; foreach ($parameters as $field => $value) { if ($value != '') { $sql .= '`' . $field . '` = :' . $field . ' AND '; } else { unset($parameters[$field]); } } // For last AND added to the query WHERE cond AND cond AND ??? $sql .= '1'; $newObject = self::getOneObject($sql, $parameters, get_class($objectToRefresh)); if (!is_object($newObject)) { return false; } foreach (self::getFields($objectToRefresh) as $field) { $reflection = self::getReflectionClass($objectToRefresh); $property = $reflection->getProperty($field); if (!$reflection->hasProperty($field)) { throw new \InvalidArgumentException('Unknown field ' . get_class($objectToRefresh) . '::' . $field); } $property->setAccessible(true); $property->setValue($objectToRefresh, self::getField($newObject, $field)); $property->setAccessible(false); } return true; } /** * Build fields for query * @param $className * @param string $prefix * @return string * @throws \ReflectionException */ public static function buildField($className, $prefix = '') { $className = is_string($className) ? $className : self::getTableName($className); $code = $prefix . $className; if (isset(self::$fieldsQuery[$code])) { return self::$fieldsQuery[$code]; } else { $fields = []; foreach (self::getFields($className) as $field) { if ('_' !== $field[0]) { if ($prefix != '') { $fields[] = '`' . $prefix . '`.' . '`' . $field . '`'; } else { $fields[] = '`' . $field . '`'; } } } self::$fieldsQuery[$code] = implode(', ', $fields); return self::$fieldsQuery[$code]; } } /** * Get one object from database * * @param string $query SQL query * @param array $params Query params * @param string $objectClassName Class of object * * @return mixed|null Instance of the class * * @throws CoreException */ public static function &getOneObject(string $query, array $params, string $objectClassName) { return self::Prepare($query, $params, self::FETCH_TYPE_ROW, \PDO::FETCH_CLASS, $objectClassName); } /** * Get list of objects filtered * * @param array $filters Filtres à appliquer * @param mixed $objectType Objet sur lequel appliquer les filtres * @return array List of objects filtered * @throws CoreException * @throws \ReflectionException */ public static function getWithFilter(array $filters, $objectType) { // operators have to remain in this order. If you put '<' before '<=', algorithm won't make the difference & will think a '<=' is a '<' $operators = ['!=', '<=', '>=', '<', '>', 'NOT LIKE', 'LIKE', '=']; $fields = self::getFields($objectType); $reflectedClass = self::getReflectionClass($objectType)->getName(); // create query $query = 'SELECT ' . self::buildField($reflectedClass) . ' FROM ' . $reflectedClass . ''; $values = []; $where = ' WHERE '; foreach ($fields as $property) { foreach ($filters as $key => $value) { if ($property == $key && $value != '') { // traitement à faire sur value pour obtenir l'opérateur $thereIsOperator = false; $operatorInformation = [ 'index' => -1, 'value' => '=', // by default '=' 'length' => 0, ]; foreach ($operators as $operator) { if (($index = strpos($value, $operator)) !== false) { $thereIsOperator = true; $operatorInformation['index'] = $index; $operatorInformation['value'] = $operator; $operatorInformation['length'] = strlen($operator); break; } } if ($thereIsOperator) { // extract operator from value $value = substr($value, $operatorInformation['length'] + 1); // +1 because of space // add % % to LIKE operator if (in_array($operatorInformation['value'], ['LIKE', 'NOT LIKE'])) { $value = '%' . $value . '%'; } } $where .= $property . ' ' . $operatorInformation['value'] . ' :' . $property . ' AND '; $values[$property] = $value; break; } } } if ($where != ' WHERE ') { $where = substr($where, 0, strlen($where) - 5); // on enlève le dernier ' AND ' $query .= $where; } // si values contient id, on sait qu'il n'y aura au plus qu'une valeur return self::Prepare($query . ';', $values, in_array('id', $values) ? self::FETCH_TYPE_ROW : self::FETCH_TYPE_ALL); } /** * Deletes an object. * * @param mixed $objectToRemove * @return boolean * @throws CoreException * @throws \ReflectionException */ public static function remove($objectToRemove) { if (method_exists($objectToRemove, 'preRemove')) { if ($objectToRemove->preRemove() === false) { return false; } } list(, $parameters) = self::buildQuery($objectToRemove); $sql = 'DELETE FROM `' . self::getTableName($objectToRemove) . '` WHERE '; if (isset($parameters['id'])) { $sql .= '`id` = :id AND '; $parameters = ['id' => $parameters['id']]; } else { foreach ($parameters as $field => $value) { if ($value != '') { $sql .= '`' . $field . '` = :' . $field . ' AND '; } else { unset($parameters[$field]); } } } $sql .= '1'; $res = self::getOne($sql, $parameters); $reflection = self::getReflectionClass($objectToRemove); if ($reflection->hasProperty('id')) { self::setField($objectToRemove, 'id', null); } if (method_exists($objectToRemove, 'postRemove')) { $objectToRemove->postRemove(); } return null !== $res && false !== $res; } /** * @param $_table * @return mixed * @throws CoreException */ /** * @param $_table * @return mixed * @throws CoreException */ /** * @param $_table * @return mixed * @throws CoreException */ public static function checksum($_table) { $sql = 'CHECKSUM TABLE ' . $_table; $result = self::getOne($sql); return $result['Checksum']; } /** * Lock an entity. * * @param object $objectToLock * @return boolean * @throws CoreException * @throws \ReflectionException */ public static function lock($objectToLock) { if (method_exists($objectToLock, 'preLock')) { if ($objectToLock->preLock() === false) { return false; } } list(, $parameters) = self::buildQuery($objectToLock); $sql = 'SELECT * FROM ' . self::getTableName($objectToLock) . ' WHERE '; foreach ($parameters as $field => $value) { if ($value != '') { $sql .= '`' . $field . '` = :' . $field . ' AND '; } else { unset($parameters[$field]); } } $sql .= '1 LOCK IN SHARE MODE'; $res = self::getOne($sql, $parameters); if (method_exists($objectToLock, 'postLock')) { $objectToLock->postLock(); } return null !== $res && false !== $res; } /** * * @param $_database * @param string $_table * @param bool $_verbose * @param int $_loop * @return bool * @throws \Exception */ public static function compareAndFix($_database, $_table = 'all', $_verbose = false, $_loop = 0) { $result = self::compareDatabase($_database); $error = ''; foreach ($result as $tname => $tinfo) { if ($_table != 'all' && $tname != $_table) { continue; } if ($tinfo['sql'] != '') { try { if ($_verbose) { echo "\nFix : " . $tinfo['sql']; } self::exec($tinfo['sql']); } catch (\Exception $e) { $error .= $e->getMessage() . "\n"; } } if (isset($tinfo['indexes']) && count($tinfo['indexes']) > 0) { foreach ($tinfo['indexes'] as $iname => $iinfo) { if (!isset($iinfo['presql']) || trim($iinfo['presql']) == '') { continue; } try { if ($_verbose) { echo "\nFix : " . $iinfo['presql']; } self::exec($iinfo['presql']); } catch (\Exception $e) { $error .= $e->getMessage() . "\n"; } } } if (isset($tinfo['fields']) && count($tinfo['fields']) > 0) { foreach ($tinfo['fields'] as $fname => $finfo) { if (!isset($finfo['sql']) || trim($finfo['sql']) == '') { continue; } try { if ($_verbose) { echo "\nFix : " . $finfo['sql']; } self::exec($finfo['sql']); } catch (\Exception $e) { $error .= $e->getMessage() . "\n"; } } } if (isset($tinfo['indexes']) && count($tinfo['indexes']) > 0) { foreach ($tinfo['indexes'] as $iname => $iinfo) { if (!isset($iinfo['sql']) || trim($iinfo['sql']) == '') { continue; } try { if ($_verbose) { echo "\nFix : " . $iinfo['sql']; } self::exec($iinfo['sql']); } catch (\Exception $e) { $error .= $e->getMessage() . "\n"; } } } } if (trim($error) != '') { if ($_loop < 1) { return self::compareAndFix($_database, $_table, $_verbose, ($_loop + 1)); } throw new CoreException($error); } return true; } /** * @param $database * @return array * @throws CoreException */ /** * @param $database * @return array * @throws CoreException */ /** * @param $database * @return array * @throws CoreException */ private static function compareDatabase($database) { $result = []; foreach ($database['tables'] as $table) { $result = array_merge($result, self::compareTable($table)); } return $result; } /** * @param $_table * @return array * @throws CoreException */ /** * @param $_table * @return array * @throws CoreException */ /** * @param $_table * @return array * @throws CoreException */ private static function compareTable($_table) { try { $describes = self::getAll('describe `' . $_table['name'] . '`', []); } catch (\Exception $e) { $describes = []; } $result = [$_table['name'] => ['status' => 'ok', 'fields' => [], 'indexes' => [], 'sql' => '']]; if (count($describes) == 0) { $result = [$_table['name'] => [ 'status' => 'nok', 'message' => 'Not found', 'sql' => 'CREATE TABLE IF NOT EXISTS ' . '`' . $_table['name'] . '` (', ]]; foreach ($_table['fields'] as $field) { $result[$_table['name']]['sql'] .= "\n" . '`' . $field['name'] . '`'; $result[$_table['name']]['sql'] .= self::buildDefinitionField($field); $result[$_table['name']]['sql'] .= ','; } $result[$_table['name']]['sql'] .= "\n" . 'primary key('; foreach ($_table['fields'] as $field) { if (isset($field['key']) && $field['key'] == 'PRI') { $result[$_table['name']]['sql'] .= '`' . $field['name'] . '`,'; } } $result[$_table['name']]['sql'] = trim($result[$_table['name']]['sql'], ','); $result[$_table['name']]['sql'] .= ')'; $result[$_table['name']]['sql'] .= ')' . "\n"; if (!isset($_table['engine'])) { $_table['engine'] = 'InnoDB'; } $result[$_table['name']]['sql'] .= ' ENGINE ' . $_table['engine'] . ";\n"; foreach ($_table['indexes'] as $index) { $result[$_table['name']]['sql'] .= "\n" . self::buildDefinitionIndex($index, $_table['name']) . ';'; } $result[$_table['name']]['sql'] = trim($result[$_table['name']]['sql'], ';'); return $result; } $forceRebuildIndex = false; foreach ($_table['fields'] as $field) { $found = false; foreach ($describes as $describe) { if ($describe['Field'] != $field['name']) { continue; } $result[$_table['name']]['fields'] = array_merge($result[$_table['name']]['fields'], self::compareField($field, $describe, $_table['name'])); if (isset($result[$_table['name']]['fields'][$field['name']]) && $result[$_table['name']]['fields'][$field['name']]['status'] == 'nok') { $forceRebuildIndex = true; } $found = true; } if (!$found) { $result[$_table['name']]['fields'][$field['name']] = [ 'status' => 'nok', 'message' => 'Not found', 'sql' => 'ALTER TABLE `' . $_table['name'] . '` ADD `' . $field['name'] . '`' ]; $result[$_table['name']]['fields'][$field['name']]['sql'] .= self::buildDefinitionField($field); } } foreach ($describes as $describe) { $found = false; foreach ($_table['fields'] as $field) { if ($describe['Field'] == $field['name']) { $found = true; break; } } if (!$found) { $result[$_table['name']]['fields'][$describe['Field']] = [ 'status' => 'nok', 'message' => 'Should not exist', 'sql' => 'ALTER TABLE `' . $_table['name'] . '` DROP `' . $describe['Field'] . '`' ]; } } $showIndexes = self::prepareIndexCompare(self::getAll('show index from `' . $_table['name'] . '`', [])); foreach ($_table['indexes'] as $index) { $found = false; foreach ($showIndexes as $showIndex) { if ($showIndex['Key_name'] != $index['Key_name']) { continue; } $result[$_table['name']]['indexes'] = array_merge($result[$_table['name']]['indexes'], self::compareIndex($index, $showIndex, $_table['name'], $forceRebuildIndex)); $found = true; } if (!$found) { $result[$_table['name']]['indexes'][$index['Key_name']] = [ 'status' => 'nok', 'message' => 'Not found', 'sql' => '' ]; $result[$_table['name']]['indexes'][$index['Key_name']]['sql'] .= self::buildDefinitionIndex($index, $_table['name']); } } foreach ($showIndexes as $showIndex) { $found = false; foreach ($_table['indexes'] as $index) { if ($showIndex['Key_name'] == $index['Key_name']) { $found = true; break; } } if (!$found) { $result[$_table['name']]['indexes'][$showIndex['Key_name']] = [ 'status' => 'nok', 'message' => 'Should not exist', 'sql' => 'ALTER TABLE `' . $_table['name'] . '` DROP INDEX `' . $showIndex['Key_name'] . '`;' ]; } } return $result; } /** * @param $_field * @return string */ /** * @param $_field * @return string */ /** * @param $_field * @return string */ private static function buildDefinitionField($_field) { $return = ' ' . $_field['type']; if ($_field['null'] == 'NO') { $return .= ' NOT NULL'; } else { $return .= ' NULL'; } if ($_field['default'] != '') { $return .= ' DEFAULT "' . $_field['default'] . '"'; } if ($_field['extra'] == 'auto_increment') { $return .= ' AUTO_INCREMENT'; } return $return; } /** * @param $_index * @param $_table_name * @return string */ /** * @param $_index * @param $_table_name * @return string */ /** * @param $_index * @param $_table_name * @return string */ private static function buildDefinitionIndex($_index, $_table_name) { if ($_index['Non_unique'] == 0) { $return = 'CREATE UNIQUE INDEX `' . $_index['Key_name'] . '` ON `' . $_table_name . '`' . ' ('; } else { $return = 'CREATE INDEX `' . $_index['Key_name'] . '` ON `' . $_table_name . '`' . ' ('; } foreach ($_index['columns'] as $value) { $return .= '`' . $value['column'] . '`'; if ($value['Sub_part'] != null) { $return .= '(' . $value['Sub_part'] . ')'; } $return .= ' ASC,'; } $return = trim($return, ','); $return .= ')'; return $return; } /** * @param $_ref_field * @param $_real_field * @param $_table_name * @return array */ /** * @param $_ref_field * @param $_real_field * @param $_table_name * @return array */ /** * @param $_ref_field * @param $_real_field * @param $_table_name * @return array */ private static function compareField($_ref_field, $_real_field, $_table_name) { $return = [$_ref_field['name'] => ['status' => 'ok', 'sql' => '']]; if ($_ref_field['type'] != $_real_field['Type']) { $return[$_ref_field['name']]['status'] = 'nok'; $return[$_ref_field['name']]['message'] = 'Type nok'; } if ($_ref_field['null'] != $_real_field['Null']) { $return[$_ref_field['name']]['status'] = 'nok'; $return[$_ref_field['name']]['message'] = 'Null nok'; } if ($_ref_field['default'] != $_real_field['Default']) { $return[$_ref_field['name']]['status'] = 'nok'; $return[$_ref_field['name']]['message'] = 'Default nok'; } if ($_ref_field['extra'] != $_real_field['Extra']) { $return[$_ref_field['name']]['status'] = 'nok'; $return[$_ref_field['name']]['message'] = 'Extra nok'; } if ($return[$_ref_field['name']]['status'] == 'nok') { $return[$_ref_field['name']]['sql'] = 'ALTER TABLE `' . $_table_name . '` MODIFY COLUMN `' . $_ref_field['name'] . '` '; $return[$_ref_field['name']]['sql'] .= self::buildDefinitionField($_ref_field); } return $return; } /** * @param $indexes * @return array */ /** * @param $indexes * @return array */ /** * @param $indexes * @return array */ private static function prepareIndexCompare($indexes) { $return = []; foreach ($indexes as $index) { if ($index['Key_name'] == 'PRIMARY') { continue; } if (!isset($return[$index['Key_name']])) { $return[$index['Key_name']] = [ 'Key_name' => $index['Key_name'], 'Non_unique' => 0, 'columns' => [], ]; } $return[$index['Key_name']]['Non_unique'] = $index['Non_unique']; $return[$index['Key_name']]['columns'][$index['Seq_in_index']] = ['column' => $index['Column_name'], 'Sub_part' => $index['Sub_part']]; } return $return; } /** * @param $_ref_index * @param $_real_index * @param $_table_name * @param bool $_forceRebuild * @return array */ private static function compareIndex($_ref_index, $_real_index, $_table_name, $_forceRebuild = false) { $return = [$_ref_index['Key_name'] => ['status' => 'ok', 'presql' => '', 'sql' => '']]; if ($_ref_index['Non_unique'] != $_real_index['Non_unique']) { $return[$_ref_index['Key_name']]['status'] = 'nok'; $return[$_ref_index['Key_name']]['message'] = 'Non_unique nok'; } if ($_ref_index['columns'] != $_real_index['columns']) { $return[$_ref_index['Key_name']]['status'] = 'nok'; $return[$_ref_index['Key_name']]['message'] = 'Columns nok'; } if ($_forceRebuild) { $return[$_ref_index['Key_name']]['status'] = 'nok'; $return[$_ref_index['Key_name']]['message'] = 'Force rebuild'; } if ($return[$_ref_index['Key_name']]['status'] == 'nok') { $return[$_ref_index['Key_name']]['presql'] = 'ALTER TABLE `' . $_table_name . '` DROP INDEX `' . $_ref_index['Key_name'] . '`;'; $return[$_ref_index['Key_name']]['sql'] = "\n" . self::buildDefinitionIndex($_ref_index, $_table_name); } return $return; } /** * Block object cloning */ public function __clone() { trigger_error('DB : Cloner cet objet n\'est pas permis', E_USER_ERROR); } /** * Return all tables in NextDom database * @return array */ public function getAllNextDomTables() { return self::getAll('SHOW TABLES', []); } } |