PageRenderTime 64ms CodeModel.GetById 33ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/utils/ConstraintHandler.php

https://bitbucket.org/wildanm/orangehrm
PHP | 434 lines | 253 code | 66 blank | 115 comment | 36 complexity | 093e0774e26459281606ab39041b6e25 MD5 | raw file
Possible License(s): CC-BY-SA-3.0, AGPL-3.0, BSD-3-Clause, AGPL-1.0, GPL-2.0, LGPL-2.1, LGPL-3.0
  1. <?php
  2. /**
  3. * OrangeHRM is a comprehensive Human Resource Management (HRM) System that captures
  4. * all the essential functionalities required for any enterprise.
  5. * Copyright (C) 2006 OrangeHRM Inc., http://www.orangehrm.com
  6. *
  7. * OrangeHRM is free software; you can redistribute it and/or modify it under the terms of
  8. * the GNU General Public License as published by the Free Software Foundation; either
  9. * version 2 of the License, or (at your option) any later version.
  10. *
  11. * OrangeHRM is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
  12. * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  13. * See the GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along with this program;
  16. * if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
  17. * Boston, MA 02110-1301, USA
  18. *
  19. */
  20. /*
  21. * Class that is used by upgrader to apply foreign key constraints to the database.
  22. * Handles failures when applying constraints and attempts to recover by restoring
  23. * referential integrity to the database.
  24. *
  25. * NOTE: This class does not attempt to connect to mysql.
  26. */
  27. class ConstraintHandler {
  28. const TABLE_NDX = 0;
  29. const FIELDS_NDX = 1;
  30. const CHILD_TABLE_NDX = 2;
  31. const CHILD_FIELDS_NDX = 3;
  32. const DEL_CLAUSE_NDX = 4;
  33. protected $logFile;
  34. /**
  35. * Set the file to log to. If not set, no logging is done
  36. */
  37. public function setLogFile($logFile) {
  38. $this->logFile = $logFile;
  39. }
  40. /**
  41. * Get the log file.
  42. */
  43. public function getLogFile() {
  44. return $this->logFile;
  45. }
  46. /**
  47. * Apply the supplied array of constraints to the database.
  48. *
  49. * TODO: Pass an array of Constraint objects. But this would make
  50. * dbscripts/constrainst.php a bit more difficult to read.
  51. *
  52. * @param array $constraints Array of constraints
  53. * @param string $database The database to connect to.
  54. *
  55. * @return array Array of failed constraints. Empty array if none failed.
  56. */
  57. public function applyConstraints($constraints, $database = null) {
  58. $maxTries = 10;
  59. $tries = 0;
  60. $cleaned = 0;
  61. do {
  62. $cleaned = $this->_cleanup($constraints);
  63. $tries++;
  64. } while (($tries < $maxTries) && ($cleaned > 0));
  65. // Handle case where unable to clean up
  66. if ($cleaned > 0) {
  67. $this->_log("Some constraint failures may not have been cleaned (after {$maxTries} tries)");
  68. }
  69. $failedConstraints = array();
  70. foreach ($constraints as $constraint) {
  71. $result = $this->_addConstraint($constraint);
  72. if (!$result) {
  73. $failedConstraints[] = $constraint;
  74. }
  75. }
  76. return $failedConstraints;
  77. }
  78. /**
  79. * Get Alter table SQL to apply the given constraint.
  80. * (Made public for ease of testing)
  81. *
  82. * @param string constraint apply constraint
  83. * @return string Alter table statement to apply constraint to table
  84. */
  85. public function getConstraintSQL($constraint) {
  86. $table = $constraint[ConstraintHandler::TABLE_NDX];
  87. $fields = implode(",", $constraint[ConstraintHandler::FIELDS_NDX]);
  88. $childTable = $constraint[ConstraintHandler::CHILD_TABLE_NDX];
  89. $childFields = implode(",", $constraint[ConstraintHandler::CHILD_FIELDS_NDX]);
  90. $onDelete = $constraint[ConstraintHandler::DEL_CLAUSE_NDX];
  91. $delClause = "";
  92. switch ($onDelete) {
  93. case "null":
  94. $delClause = "ON DELETE SET NULL";
  95. break;
  96. case "cascade":
  97. $delClause = "ON DELETE CASCADE";
  98. break;
  99. case "restrict":
  100. $delClause = "ON DELETE RESTRICT";
  101. break;
  102. default:
  103. $delClause = "";
  104. break;
  105. }
  106. $sql = sprintf("ALTER TABLE %s ADD CONSTRAINT FOREIGN KEY (%s) REFERENCES %s(%s) %s",
  107. $table, $fields, $childTable, $childFields, $delClause);
  108. return $sql;
  109. }
  110. /**
  111. * Add the given constraint to the database
  112. * @param array $constraint Constraint as an array
  113. * @return boolean True if successfully added. False if failed to add
  114. */
  115. private function _addConstraint($constraint) {
  116. $sql = $this->getConstraintSQL($constraint);
  117. $result = mysql_query($sql);
  118. if ($result) {
  119. return true;
  120. } else {
  121. $this->_log("Failed to apply constraint: $sql");
  122. return false;
  123. }
  124. }
  125. /**
  126. * Checks if any data exists that violate the given constraints and
  127. * attempt to clean them up.
  128. *
  129. * @param string $constraints The constraints to cleanup
  130. * @return int Number of constraints which needed to be cleaned
  131. */
  132. private function _cleanup($constraints) {
  133. $cleaned = 0;
  134. foreach ($constraints as $constraint) {
  135. $type = $constraint[self::DEL_CLAUSE_NDX];
  136. $fields = $constraint[ConstraintHandler::FIELDS_NDX];
  137. $childFields = $constraint[ConstraintHandler::CHILD_FIELDS_NDX];
  138. if (count($fields) != count($childFields)) {
  139. throw new ConstraintHandlerException("Invalid constraint: ". getConstraintSQL($constraint));
  140. }
  141. switch ($type) {
  142. case "null":
  143. $result = $this->_cleanSetNullConstraint($constraint);
  144. break;
  145. case "cascade":
  146. $result = $this->_cleanCascadeConstraint($constraint);
  147. break;
  148. case "restrict":
  149. $result = $this->_cleanRestrictConstraint($constraint);
  150. break;
  151. case null:
  152. $result = $this->_cleanEmptyConstraint($constraint);
  153. break;
  154. default:
  155. /* Unsupported */
  156. $this->_log("Unsupported constraint type: $type");
  157. $result = false;
  158. break;
  159. }
  160. if ($result) {
  161. $cleaned++;
  162. }
  163. }
  164. return $cleaned;
  165. }
  166. /**
  167. * Attempts to clean up the given constraint
  168. *
  169. * TODO: Change naming. child actually refers to parent table
  170. *
  171. * @param array Constraint
  172. * @return int number of rows affected (cleaned).
  173. */
  174. private function _cleanSetNullConstraint($constraint) {
  175. $numAffected = 0;
  176. $table = $constraint[ConstraintHandler::TABLE_NDX];
  177. $fields = $constraint[ConstraintHandler::FIELDS_NDX];
  178. $childTable = $constraint[ConstraintHandler::CHILD_TABLE_NDX];
  179. $childFields = $constraint[ConstraintHandler::CHILD_FIELDS_NDX];
  180. for ($i=0; $i< count($fields); $i++) {
  181. $field = $fields[$i];
  182. $childField = $childFields[$i];
  183. $setArray[] = "`$field` = NULL";
  184. $checkNullArray[] = " (`{$table}`.`{$field}` IS NULL)";
  185. $whereArray[] = "`$table`.`$field` = `$childTable`.`$childField`";
  186. }
  187. $setClause = implode(",", $setArray);
  188. $checkNull = implode(" OR ", $checkNullArray);
  189. $whereClause = implode(" AND ", $whereArray);
  190. /* Self references need to be handled differently since we can't select and update
  191. * from the same table in a multi table update.
  192. */
  193. if ($table == $childTable) {
  194. /* This happens only for hs_hr_users - modified_user_id and created_by fields.
  195. * So we are considering only the case where one field is in the constraint.
  196. */
  197. if (count($fields) == 1) {
  198. $field = $fields[0];
  199. $childField = $childFields[0];
  200. $sql = "SELECT DISTINCT A.`{$field}` FROM `{$table}` As A WHERE A.`{$field}` IS NOT NULL AND A.`{$field}` NOT IN " .
  201. "(SELECT B.id FROM `{$table}` as B)";
  202. $result = mysql_query($sql);
  203. if ($result) {
  204. if (mysql_num_rows($result) > 0) {
  205. while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
  206. $ids[] = "'" . $row[0] . "'";
  207. }
  208. $idList = implode(",", $ids);
  209. $sql = "UPDATE `{$table}` SET `{$field}` = NULL WHERE `{$field}` IN ({$idList})";
  210. $result = mysql_query($sql);
  211. if (!$result) {
  212. throw new ConstraintHandlerException("Error when running query: $sql. MysqlError:" . mysql_error());
  213. }
  214. $numAffected = mysql_affected_rows();
  215. }
  216. } else {
  217. throw new ConstraintHandlerException("Error when running query: $sql. MysqlError:" . mysql_error());
  218. }
  219. }
  220. } else {
  221. $sql = sprintf("UPDATE `{$table}` SET {$setClause} WHERE NOT ({$checkNull}) AND NOT EXISTS(SELECT 1 FROM `{$childTable}` " .
  222. " WHERE {$whereClause} )");
  223. $result = mysql_query($sql);
  224. if (!$result) {
  225. throw new ConstraintHandlerException("Error when running query: $sql. MysqlError:" . mysql_error());
  226. }
  227. $numAffected = mysql_affected_rows();
  228. }
  229. if ($numAffected > 0) {
  230. $this->_log($sql . "NUM = " . $numAffected);
  231. }
  232. return $numAffected;
  233. }
  234. /**
  235. * Attempts to clean up the given constraint
  236. *
  237. * @param array Constraint
  238. * @return int number of rows affected (cleaned).
  239. */
  240. private function _cleanCascadeConstraint($constraint) {
  241. $numAffected = 0;
  242. $table = $constraint[ConstraintHandler::TABLE_NDX];
  243. $fields = $constraint[ConstraintHandler::FIELDS_NDX];
  244. $childTable = $constraint[ConstraintHandler::CHILD_TABLE_NDX];
  245. $childFields = $constraint[ConstraintHandler::CHILD_FIELDS_NDX];
  246. for ($i=0; $i< count($fields); $i++) {
  247. $field = $fields[$i];
  248. $childField = $childFields[$i];
  249. $checkNullArray[] = " (`{$table}`.`{$field}` IS NULL)";
  250. $whereArray[] = "`$table`.`$field` = `$childTable`.`$childField`";
  251. }
  252. $checkNull = implode(" OR ", $checkNullArray);
  253. $whereClause = implode(" AND ", $whereArray);
  254. $countSql = "SELECT COUNT(*) FROM `{$childTable}`";
  255. $result = mysql_query($countSql);
  256. if (!$result) {
  257. throw new ConstraintHandlerException("Error when running query: $sql. MysqlError:" . mysql_error());
  258. }
  259. $row = mysql_fetch_array($result, MYSQL_NUM);
  260. $count = $row[0];
  261. /* If no parents all the children have to be deleted */
  262. if ($count == 0) {
  263. $sql = sprintf("DELETE FROM `{$table}` WHERE NOT ({$checkNull})");
  264. } else {
  265. $sql = sprintf("DELETE FROM `{$table}` USING `{$table}`, `{$childTable}` WHERE NOT ({$checkNull}) AND NOT EXISTS(SELECT 1 FROM `{$childTable}` " .
  266. " WHERE {$whereClause} )");
  267. }
  268. $result = mysql_query($sql);
  269. if (!$result) {
  270. throw new ConstraintHandlerException("Error when running query: $sql. MysqlError:" . mysql_error());
  271. }
  272. $numAffected = mysql_affected_rows();
  273. if ($numAffected > 0) {
  274. $this->_log($sql . "NUM = " . $numAffected);
  275. }
  276. return $numAffected;
  277. }
  278. /**
  279. * Attempts to clean up the given constraint
  280. *
  281. * @param array Constraint
  282. * @return int number of rows affected (cleaned).
  283. */
  284. private function _cleanRestrictConstraint($constraint) {
  285. /*
  286. * The only restrict constraints are hs_hr_project and hs_hr_compstructtree
  287. * Entries in hs_hr_project are not deleted, in hs_hr_compstructtree
  288. * loc_code can be set to null (but shouldn't be necessary
  289. * since we prevent deletion of locations if in use),
  290. * so consider this the same as a null constraint.
  291. */
  292. $numAffected = $this->_cleanSetNullConstraint($constraint);
  293. return $numAffected;
  294. }
  295. /**
  296. * Attempts to clean up the given constraint
  297. *
  298. * @param array Constraint
  299. * @return int number of rows affected (cleaned).
  300. */
  301. private function _cleanEmptyConstraint($constraint) {
  302. $numAffected = 0;
  303. /* Not implemented yet. We don't have empty constraints */
  304. return $numAffected;
  305. }
  306. /**
  307. * Verifies that the given constraints have been set in the database.
  308. * @param array $constraints constraint array.
  309. *
  310. * @return array Array of constraints that were missing.
  311. */
  312. public function getMissingConstraints($constraints) {
  313. $failedConstraints = array();
  314. foreach ($constraints as $constraint) {
  315. $found = false;
  316. $table = $constraint[ConstraintHandler::TABLE_NDX];
  317. $fields = implode("[\s,`]+", $constraint[ConstraintHandler::FIELDS_NDX]);
  318. $childTable = $constraint[ConstraintHandler::CHILD_TABLE_NDX];
  319. $childFields = implode("[\s,`]+", $constraint[ConstraintHandler::CHILD_FIELDS_NDX]);
  320. $onDelete = $constraint[ConstraintHandler::DEL_CLAUSE_NDX];
  321. $delClause = "ON\s+DELETE";
  322. switch ($onDelete) {
  323. case "null":
  324. $delClause = "ON\s+DELETE\s+SET\s+NULL";
  325. break;
  326. case "cascade":
  327. $delClause = "ON\s+DELETE\s+CASCADE";
  328. break;
  329. default:
  330. $delClause = "";
  331. break;
  332. }
  333. $sql = "SHOW CREATE TABLE $table";
  334. $result = mysql_query($sql);
  335. if ($result === false) {
  336. throw new ConstraintHandlerException("Error when running query: $sql. MysqlError:" . mysql_error());
  337. }
  338. $row = mysql_fetch_array($result, MYSQL_NUM);
  339. $createTable = $row[1];
  340. $lines = explode("\n", $createTable);
  341. foreach ($lines as $line) {
  342. $regexp = "/\w*CONSTRAINT\b.*\bFOREIGN\s+KEY[\s(`]+{$fields}[\s)`]+REFERENCES[\s`)]+{$childTable}[\s`(]+{$childFields}[\s`)]+{$delClause}.*/";
  343. $count = preg_match($regexp, $line);
  344. if ($count === 1) {
  345. $found = true;
  346. break;
  347. }
  348. }
  349. if (!$found) {
  350. $failedConstraints[] = $constraint;
  351. }
  352. }
  353. return $failedConstraints;
  354. }
  355. /**
  356. * Log the given message to the log file
  357. * @param string Log message
  358. *
  359. */
  360. private function _log($message) {
  361. if (!empty($this->logFile)) {
  362. error_log (date("r") . " " . $message ."\n", 3, $this->logFile);
  363. }
  364. }
  365. }
  366. class ConstraintHandlerException extends Exception {
  367. }
  368. ?>