PageRenderTime 69ms CodeModel.GetById 30ms RepoModel.GetById 1ms app.codeStats 0ms

/classes/fORMValidation.php

https://bitbucket.org/ZilIsiltk/flourish
PHP | 1593 lines | 864 code | 231 blank | 498 comment | 151 complexity | f318fc65484c25fe68ac2f0a51775201 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Handles validation for fActiveRecord classes
  4. *
  5. * @copyright Copyright (c) 2007-2010 Will Bond, others
  6. * @author Will Bond [wb] <will@flourishlib.com>
  7. * @author Jeff Turcotte [jt] <jeff.turcotte@gmail.com>
  8. * @license http://flourishlib.com/license
  9. *
  10. * @package Flourish
  11. * @link http://flourishlib.com/fORMValidation
  12. *
  13. * @version 1.0.0b28
  14. * @changes 1.0.0b28 Updated the class to work with the new nested array structure for validation messages [wb, 2010-10-03]
  15. * @changes 1.0.0b27 Fixed ::hasValue() to properly detect zero-value floats, made ::hasValue() internal public [wb, 2010-07-26]
  16. * @changes 1.0.0b26 Improved the error message for integers to say `whole number` instead of just `number` [wb, 2010-05-29]
  17. * @changes 1.0.0b25 Added ::addRegexRule(), changed validation messages array to use column name keys [wb, 2010-05-26]
  18. * @changes 1.0.0b24 Added ::addRequiredRule() for required columns that aren't automatically handled via schema detection [wb, 2010-04-06]
  19. * @changes 1.0.0b23 Added support for checking integers and floats to ensure they fit within the range imposed by the database schema [wb, 2010-03-17]
  20. * @changes 1.0.0b22 Made the value checking for one-or-more and only-one rules more robust when detecting the absence of a value [wb, 2009-12-17]
  21. * @changes 1.0.0b21 Fixed a bug affecting where conditions with columns that are not null but have a default value [wb, 2009-11-03]
  22. * @changes 1.0.0b20 Updated code for the new fORMDatabase and fORMSchema APIs [wb, 2009-10-28]
  23. * @changes 1.0.0b19 Changed SQL statements to use value placeholders, identifier escaping and schema support [wb, 2009-10-22]
  24. * @changes 1.0.0b18 Fixed ::checkOnlyOneRule() and ::checkOneOrMoreRule() to consider blank strings as NULL [wb, 2009-08-21]
  25. * @changes 1.0.0b17 Added @internal methods ::removeStringReplacement() and ::removeRegexReplacement() [wb, 2009-07-29]
  26. * @changes 1.0.0b16 Backwards Compatibility Break - renamed ::addConditionalValidationRule() to ::addConditionalRule(), ::addManyToManyValidationRule() to ::addManyToManyRule(), ::addOneOrMoreValidationRule() to ::addOneOrMoreRule(), ::addOneToManyValidationRule() to ::addOneToManyRule(), ::addOnlyOneValidationRule() to ::addOnlyOneRule(), ::addValidValuesValidationRule() to ::addValidValuesRule() [wb, 2009-07-13]
  27. * @changes 1.0.0b15 Added ::addValidValuesValidationRule() [wb/jt, 2009-07-13]
  28. * @changes 1.0.0b14 Added ::addStringReplacement() and ::addRegexReplacement() for simple validation message modification [wb, 2009-07-01]
  29. * @changes 1.0.0b13 Changed ::reorderMessages() to compare string in a case-insensitive manner [wb, 2009-06-30]
  30. * @changes 1.0.0b12 Updated ::addConditionalValidationRule() to allow any number of `$main_columns`, and if any of those have a matching value, the condtional columns will be required [wb, 2009-06-30]
  31. * @changes 1.0.0b11 Fixed a couple of bugs with validating related records [wb, 2009-06-26]
  32. * @changes 1.0.0b10 Fixed UNIQUE constraint checking so it is only done once per constraint, fixed some UTF-8 case sensitivity issues [wb, 2009-06-17]
  33. * @changes 1.0.0b9 Updated code for new fORM API [wb, 2009-06-15]
  34. * @changes 1.0.0b8 Updated code to use new fValidationException::formatField() method [wb, 2009-06-04]
  35. * @changes 1.0.0b7 Updated ::validateRelated() to use new fORMRelated::validate() method and ::checkRelatedOneOrMoreRule() to use new `$related_records` structure [wb, 2009-06-02]
  36. * @changes 1.0.0b6 Changed date/time/timestamp checking from `strtotime()` to fDate/fTime/fTimestamp for better localization support [wb, 2009-06-01]
  37. * @changes 1.0.0b5 Fixed a bug in ::checkOnlyOneRule() where no values would not be flagged as an error [wb, 2009-04-23]
  38. * @changes 1.0.0b4 Fixed a bug in ::checkUniqueConstraints() related to case-insensitive columns [wb, 2009-02-15]
  39. * @changes 1.0.0b3 Implemented proper fix for ::addManyToManyValidationRule() [wb, 2008-12-12]
  40. * @changes 1.0.0b2 Fixed a bug with ::addManyToManyValidationRule() [wb, 2008-12-08]
  41. * @changes 1.0.0b The initial implementation [wb, 2007-08-04]
  42. */
  43. class fORMValidation
  44. {
  45. // The following constants allow for nice looking callbacks to static methods
  46. const addConditionalRule = 'fORMValidation::addConditionalRule';
  47. const addManyToManyRule = 'fORMValidation::addManyToManyRule';
  48. const addOneOrMoreRule = 'fORMValidation::addOneOrMoreRule';
  49. const addOneToManyRule = 'fORMValidation::addOneToManyRule';
  50. const addOnlyOneRule = 'fORMValidation::addOnlyOneRule';
  51. const addRegexReplacement = 'fORMValidation::addRegexReplacement';
  52. const addRegexRule = 'fORMValidation::addRegexRule';
  53. const addRequiredRule = 'fORMValidation::addRequiredRule';
  54. const addStringReplacement = 'fORMValidation::addStringReplacement';
  55. const addValidValuesRule = 'fORMValidation::addValidValuesRule';
  56. const hasValue = 'fORMValidation::hasValue';
  57. const inspect = 'fORMValidation::inspect';
  58. const removeStringReplacement = 'fORMValidation::removeStringReplacement';
  59. const removeRegexReplacement = 'fORMValidation::removeRegexReplacement';
  60. const reorderMessages = 'fORMValidation::reorderMessages';
  61. const replaceMessages = 'fORMValidation::replaceMessages';
  62. const reset = 'fORMValidation::reset';
  63. const setColumnCaseInsensitive = 'fORMValidation::setColumnCaseInsensitive';
  64. const setMessageOrder = 'fORMValidation::setMessageOrder';
  65. const validate = 'fORMValidation::validate';
  66. const validateRelated = 'fORMValidation::validateRelated';
  67. /**
  68. * Columns that should be treated as case insensitive when checking uniqueness
  69. *
  70. * @var array
  71. */
  72. static private $case_insensitive_columns = array();
  73. /**
  74. * Conditional rules
  75. *
  76. * @var array
  77. */
  78. static private $conditional_rules = array();
  79. /**
  80. * Ordering rules for messages
  81. *
  82. * @var array
  83. */
  84. static private $message_orders = array();
  85. /**
  86. * One or more rules
  87. *
  88. * @var array
  89. */
  90. static private $one_or_more_rules = array();
  91. /**
  92. * Only one rules
  93. *
  94. * @var array
  95. */
  96. static private $only_one_rules = array();
  97. /**
  98. * Regular expression replacements performed on each message
  99. *
  100. * @var array
  101. */
  102. static private $regex_replacements = array();
  103. /**
  104. * Rules that require at least one or more *-to-many related records to be associated
  105. *
  106. * @var array
  107. */
  108. static private $related_one_or_more_rules = array();
  109. /**
  110. * Rules that require a value to match a regular expression
  111. *
  112. * @var array
  113. */
  114. static private $regex_rules = array();
  115. /**
  116. * Rules that require a value be present in a column even if the database schema doesn't require it
  117. *
  118. * @var array
  119. */
  120. static private $required_rules = array();
  121. /**
  122. * String replacements performed on each message
  123. *
  124. * @var array
  125. */
  126. static private $string_replacements = array();
  127. /**
  128. * Valid values rules
  129. *
  130. * @var array
  131. */
  132. static private $valid_values_rules = array();
  133. /**
  134. * Adds a conditional rule
  135. *
  136. * If a non-empty value is found in one of the `$main_columns`, or if
  137. * specified, a value from the `$conditional_values` array, all of the
  138. * `$conditional_columns` will also be required to have a value.
  139. *
  140. * @param mixed $class The class name or instance of the class this rule applies to
  141. * @param string|array $main_columns The column(s) to check for a value
  142. * @param mixed $conditional_values If `NULL`, any value in the main column will trigger the conditional column(s), otherwise the value must match this scalar value or be present in the array of values
  143. * @param string|array $conditional_columns The column(s) that are to be required
  144. * @return void
  145. */
  146. static public function addConditionalRule($class, $main_columns, $conditional_values, $conditional_columns)
  147. {
  148. $class = fORM::getClass($class);
  149. if (!isset(self::$conditional_rules[$class])) {
  150. self::$conditional_rules[$class] = array();
  151. }
  152. settype($main_columns, 'array');
  153. settype($conditional_columns, 'array');
  154. if ($conditional_values !== NULL) {
  155. settype($conditional_values, 'array');
  156. }
  157. $rule = array();
  158. $rule['main_columns'] = $main_columns;
  159. $rule['conditional_values'] = $conditional_values;
  160. $rule['conditional_columns'] = $conditional_columns;
  161. self::$conditional_rules[$class][] = $rule;
  162. }
  163. /**
  164. * Add a many-to-many rule that requires at least one related record is associated with the current record
  165. *
  166. * @param mixed $class The class name or instance of the class to add the rule for
  167. * @param string $related_class The name of the related class
  168. * @param string $route The route to the related class
  169. * @return void
  170. */
  171. static public function addManyToManyRule($class, $related_class, $route=NULL)
  172. {
  173. $class = fORM::getClass($class);
  174. if (!isset(self::$related_one_or_more_rules[$class])) {
  175. self::$related_one_or_more_rules[$class] = array();
  176. }
  177. if (!isset(self::$related_one_or_more_rules[$class][$related_class])) {
  178. self::$related_one_or_more_rules[$class][$related_class] = array();
  179. }
  180. $route = fORMSchema::getRouteName(
  181. fORMSchema::retrieve($class),
  182. fORM::tablize($class),
  183. fORM::tablize($related_class),
  184. $route,
  185. 'many-to-many'
  186. );
  187. self::$related_one_or_more_rules[$class][$related_class][$route] = TRUE;
  188. }
  189. /**
  190. * Adds a one-or-more rule that requires at least one of the columns specified has a value
  191. *
  192. * @param mixed $class The class name or instance of the class the columns exists in
  193. * @param array $columns The columns to check
  194. * @return void
  195. */
  196. static public function addOneOrMoreRule($class, $columns)
  197. {
  198. $class = fORM::getClass($class);
  199. settype($columns, 'array');
  200. if (!isset(self::$one_or_more_rules[$class])) {
  201. self::$one_or_more_rules[$class] = array();
  202. }
  203. $rule = array();
  204. $rule['columns'] = $columns;
  205. self::$one_or_more_rules[$class][] = $rule;
  206. }
  207. /**
  208. * Add a one-to-many rule that requires at least one related record is associated with the current record
  209. *
  210. * @param mixed $class The class name or instance of the class to add the rule for
  211. * @param string $related_class The name of the related class
  212. * @param string $route The route to the related class
  213. * @return void
  214. */
  215. static public function addOneToManyRule($class, $related_class, $route=NULL)
  216. {
  217. $class = fORM::getClass($class);
  218. if (!isset(self::$related_one_or_more_rules[$class])) {
  219. self::$related_one_or_more_rules[$class] = array();
  220. }
  221. if (!isset(self::$related_one_or_more_rules[$class][$related_class])) {
  222. self::$related_one_or_more_rules[$class][$related_class] = array();
  223. }
  224. $route = fORMSchema::getRouteName(
  225. fORMSchema::retrieve($class),
  226. fORM::tablize($class),
  227. fORM::tablize($related_class),
  228. $route,
  229. 'one-to-many'
  230. );
  231. self::$related_one_or_more_rules[$class][$related_class][$route] = TRUE;
  232. }
  233. /**
  234. * Add an only-one rule that requires exactly one of the columns must have a value
  235. *
  236. * @param mixed $class The class name or instance of the class the columns exists in
  237. * @param array $columns The columns to check
  238. * @return void
  239. */
  240. static public function addOnlyOneRule($class, $columns)
  241. {
  242. $class = fORM::getClass($class);
  243. settype($columns, 'array');
  244. if (!isset(self::$only_one_rules[$class])) {
  245. self::$only_one_rules[$class] = array();
  246. }
  247. $rule = array();
  248. $rule['columns'] = $columns;
  249. self::$only_one_rules[$class][] = $rule;
  250. }
  251. /**
  252. * Adds a call to [http://php.net/preg_replace `preg_replace()`] for each message
  253. *
  254. * Regex replacement is done after the `post::validate()` hook, and right
  255. * before the messages are reordered.
  256. *
  257. * If a message is an empty string after replacement, it will be
  258. * removed from the list of messages.
  259. *
  260. * @param mixed $class The class name or instance of the class the columns exists in
  261. * @param string $search The PCRE regex to search for - see http://php.net/pcre for details
  262. * @param string $replace The string to replace with - all $ and \ are used in back references and must be escaped with a \ when meant literally
  263. * @return void
  264. */
  265. static public function addRegexReplacement($class, $search, $replace)
  266. {
  267. $class = fORM::getClass($class);
  268. if (!isset(self::$regex_replacements[$class])) {
  269. self::$regex_replacements[$class] = array(
  270. 'search' => array(),
  271. 'replace' => array()
  272. );
  273. }
  274. self::$regex_replacements[$class]['search'][] = $search;
  275. self::$regex_replacements[$class]['replace'][] = $replace;
  276. }
  277. /**
  278. * Adds a rule to validate a column against a PCRE regular expression - the rule is not run if the value is `NULL`
  279. *
  280. * @param mixed $class The class name or instance of the class the columns exists in
  281. * @param string $column The column to match with the regex
  282. * @param string $regex The PCRE regex to match against - see http://php.net/pcre for details
  283. * @param string $message The message to use if the value does not match the regular expression
  284. * @return void
  285. */
  286. static public function addRegexRule($class, $column, $regex, $message)
  287. {
  288. $class = fORM::getClass($class);
  289. if (!isset(self::$regex_rules[$class])) {
  290. self::$regex_rules[$class] = array();
  291. }
  292. self::$regex_rules[$class][$column] = array(
  293. 'regex' => $regex,
  294. 'message' => $message
  295. );
  296. }
  297. /**
  298. * Requires that a column have a non-`NULL` value
  299. *
  300. * Before using this method, try setting the database column to `NOT NULL`
  301. * and remove any default value. Such a configuration will trigger the same
  302. * functionality as this method, and will enforce the rule on the database
  303. * level for any other code that queries it.
  304. *
  305. * @param mixed $class The class name or instance of the class the column(s) exists in
  306. * @param array $columns The column or columns to check - each column will require a value
  307. * @return void
  308. */
  309. static public function addRequiredRule($class, $columns)
  310. {
  311. $class = fORM::getClass($class);
  312. settype($columns, 'array');
  313. if (!isset(self::$required_rules[$class])) {
  314. self::$required_rules[$class] = array();
  315. }
  316. foreach ($columns as $column) {
  317. self::$required_rules[$class][$column] = TRUE;
  318. }
  319. }
  320. /**
  321. * Adds a call to [http://php.net/str_replace `str_replace()`] for each message
  322. *
  323. * String replacement is done after the `post::validate()` hook, and right
  324. * before the messages are reordered.
  325. *
  326. * If a message is an empty string after replacement, it will be
  327. * removed from the list of messages.
  328. *
  329. * @param mixed $class The class name or instance of the class the columns exists in
  330. * @param string $search The string to search for
  331. * @param string $replace The string to replace with
  332. * @return void
  333. */
  334. static public function addStringReplacement($class, $search, $replace)
  335. {
  336. $class = fORM::getClass($class);
  337. if (!isset(self::$string_replacements[$class])) {
  338. self::$string_replacements[$class] = array(
  339. 'search' => array(),
  340. 'replace' => array()
  341. );
  342. }
  343. self::$string_replacements[$class]['search'][] = $search;
  344. self::$string_replacements[$class]['replace'][] = $replace;
  345. }
  346. /**
  347. * Restricts a column to having only a value from the list of valid values
  348. *
  349. * Please note that `NULL` values are always allowed, even if not listed in
  350. * the `$valid_values` array, if the column is not set as `NOT NULL`.
  351. *
  352. * This functionality can also be accomplished by added a `CHECK` constraint
  353. * on the column in the database, or using a MySQL `ENUM` data type.
  354. *
  355. * @param mixed $class The class name or instance of the class this rule applies to
  356. * @param string $column The column to validate
  357. * @param array $valid_values The valid values to check - `NULL` values are always allows if the column is not set to `NOT NULL`
  358. * @return void
  359. */
  360. static public function addValidValuesRule($class, $column, $valid_values)
  361. {
  362. $class = fORM::getClass($class);
  363. if (!isset(self::$valid_values_rules[$class])) {
  364. self::$valid_values_rules[$class] = array();
  365. }
  366. settype($valid_values, 'array');
  367. self::$valid_values_rules[$class][$column] = $valid_values;
  368. fORM::registerInspectCallback($class, $column, self::inspect);
  369. }
  370. /**
  371. * Validates a value against the database schema
  372. *
  373. * @param fActiveRecord $object The instance of the class the column is part of
  374. * @param string $column The column to check
  375. * @param array &$values An associative array of all values going into the row (needs all for multi-field unique constraint checking)
  376. * @param array &$old_values The old values from the record
  377. * @return string An error message for the column specified
  378. */
  379. static private function checkAgainstSchema($object, $column, &$values, &$old_values)
  380. {
  381. $class = get_class($object);
  382. $table = fORM::tablize($class);
  383. $schema = fORMSchema::retrieve($class);
  384. $info = $schema->getColumnInfo($table, $column);
  385. // Make sure a value is provided for required columns
  386. $schema_not_null = $info['not_null'] && $info['default'] === NULL && $info['auto_increment'] === FALSE;
  387. $rule_not_null = isset(self::$required_rules[$class][$column]);
  388. if ($values[$column] === NULL && ($schema_not_null || $rule_not_null)) {
  389. return self::compose(
  390. '%sPlease enter a value',
  391. fValidationException::formatField(fORM::getColumnName($class, $column))
  392. );
  393. }
  394. $message = self::checkDataType($class, $column, $values[$column]);
  395. if ($message) { return $message; }
  396. // Make sure a valid value is chosen
  397. if (isset($info['valid_values']) && $values[$column] !== NULL && !in_array($values[$column], $info['valid_values'])) {
  398. return self::compose(
  399. '%1$sPlease choose from one of the following: %2$s',
  400. fValidationException::formatField(fORM::getColumnName($class, $column)),
  401. join(', ', $info['valid_values'])
  402. );
  403. }
  404. // Make sure the value isn't too long
  405. if ($info['type'] == 'varchar' && isset($info['max_length']) && $values[$column] !== NULL && is_string($values[$column]) && fUTF8::len($values[$column]) > $info['max_length']) {
  406. return self::compose(
  407. '%1$sPlease enter a value no longer than %2$s characters',
  408. fValidationException::formatField(fORM::getColumnName($class, $column)),
  409. $info['max_length']
  410. );
  411. }
  412. // Make sure the value is the proper length
  413. if ($info['type'] == 'char' && isset($info['max_length']) && $values[$column] !== NULL && is_string($values[$column]) && fUTF8::len($values[$column]) != $info['max_length']) {
  414. return self::compose(
  415. '%1$sPlease enter exactly %2$s characters',
  416. fValidationException::formatField(fORM::getColumnName($class, $column)),
  417. $info['max_length']
  418. );
  419. }
  420. // Make sure the value fits in the numeric range
  421. if (self::stringlike($values[$column]) && in_array($info['type'], array('integer', 'float')) && $info['min_value'] && $info['max_value'] && ($info['min_value']->gt($values[$column]) || $info['max_value']->lt($values[$column]))) {
  422. return self::compose(
  423. '%1$sPlease enter a number between %2$s and %3$s',
  424. fValidationException::formatField(fORM::getColumnName($class, $column)),
  425. $info['min_value']->__toString(),
  426. $info['max_value']->__toString()
  427. );
  428. }
  429. $message = self::checkForeignKeyConstraints($class, $column, $values);
  430. if ($message) { return $message; }
  431. }
  432. /**
  433. * Validates against a conditional rule
  434. *
  435. * @param string $class The class this rule applies to
  436. * @param array &$values An associative array of all values for the record
  437. * @param array $main_columns The columns to check for a value
  438. * @param array $conditional_values If `NULL`, any value in the main column will trigger the conditional columns, otherwise the value must match one of these
  439. * @param array $conditional_columns The columns that are to be required
  440. * @return array The error messages for the rule specified
  441. */
  442. static private function checkConditionalRule($class, &$values, $main_columns, $conditional_values, $conditional_columns)
  443. {
  444. $check_for_missing_values = FALSE;
  445. foreach ($main_columns as $main_column) {
  446. $matches_conditional_value = $conditional_values !== NULL && in_array($values[$main_column], $conditional_values);
  447. $has_some_value = $conditional_values === NULL && strlen((string) $values[$main_column]);
  448. if ($matches_conditional_value || $has_some_value) {
  449. $check_for_missing_values = TRUE;
  450. break;
  451. }
  452. }
  453. if (!$check_for_missing_values) {
  454. return;
  455. }
  456. $messages = array();
  457. foreach ($conditional_columns as $conditional_column) {
  458. if ($values[$conditional_column] !== NULL) { continue; }
  459. $messages[$conditional_column] = self::compose(
  460. '%sPlease enter a value',
  461. fValidationException::formatField(fORM::getColumnName($class, $conditional_column))
  462. );
  463. }
  464. if ($messages) {
  465. return $messages;
  466. }
  467. }
  468. /**
  469. * Validates a value against the database data type
  470. *
  471. * @param string $class The class the column is part of
  472. * @param string $column The column to check
  473. * @param mixed $value The value to check
  474. * @return string An error message for the column specified
  475. */
  476. static private function checkDataType($class, $column, $value)
  477. {
  478. $table = fORM::tablize($class);
  479. $schema = fORMSchema::retrieve($class);
  480. $column_info = $schema->getColumnInfo($table, $column);
  481. if ($value !== NULL) {
  482. switch ($column_info['type']) {
  483. case 'varchar':
  484. case 'char':
  485. case 'text':
  486. case 'blob':
  487. if (!is_string($value) && !is_numeric($value)) {
  488. return self::compose(
  489. '%sPlease enter a string',
  490. fValidationException::formatField(fORM::getColumnName($class, $column))
  491. );
  492. }
  493. break;
  494. case 'integer':
  495. if (!is_numeric($value)) {
  496. return self::compose(
  497. '%sPlease enter a whole number',
  498. fValidationException::formatField(fORM::getColumnName($class, $column))
  499. );
  500. }
  501. break;
  502. case 'float':
  503. if (!is_numeric($value)) {
  504. return self::compose(
  505. '%sPlease enter a number',
  506. fValidationException::formatField(fORM::getColumnName($class, $column))
  507. );
  508. }
  509. break;
  510. case 'timestamp':
  511. try {
  512. new fTimestamp($value);
  513. } catch (fValidationException $e) {
  514. return self::compose(
  515. '%sPlease enter a date/time',
  516. fValidationException::formatField(fORM::getColumnName($class, $column))
  517. );
  518. }
  519. break;
  520. case 'date':
  521. try {
  522. new fDate($value);
  523. } catch (fValidationException $e) {
  524. return self::compose(
  525. '%sPlease enter a date',
  526. fValidationException::formatField(fORM::getColumnName($class, $column))
  527. );
  528. }
  529. break;
  530. case 'time':
  531. try {
  532. new fTime($value);
  533. } catch (fValidationException $e) {
  534. return self::compose(
  535. '%sPlease enter a time',
  536. fValidationException::formatField(fORM::getColumnName($class, $column))
  537. );
  538. }
  539. break;
  540. }
  541. }
  542. }
  543. /**
  544. * Validates values against foreign key constraints
  545. *
  546. * @param string $class The class to check the foreign keys for
  547. * @param string $column The column to check
  548. * @param array &$values The values to check
  549. * @return string An error message for the column specified
  550. */
  551. static private function checkForeignKeyConstraints($class, $column, &$values)
  552. {
  553. if ($values[$column] === NULL) {
  554. return;
  555. }
  556. $db = fORMDatabase::retrieve($class, 'read');
  557. $schema = fORMSchema::retrieve($class);
  558. $table = fORM::tablize($class);
  559. $foreign_keys = $schema->getKeys($table, 'foreign');
  560. foreach ($foreign_keys AS $foreign_key) {
  561. if ($foreign_key['column'] == $column) {
  562. try {
  563. $params = array(
  564. "SELECT %r FROM %r WHERE " . fORMDatabase::makeCondition($schema, $table, $column, '=', $values[$column]),
  565. $foreign_key['foreign_column'],
  566. $foreign_key['foreign_table'],
  567. $foreign_key['foreign_column'],
  568. $values[$column]
  569. );
  570. $result = call_user_func_array($db->translatedQuery, $params);
  571. $result->tossIfNoRows();
  572. } catch (fNoRowsException $e) {
  573. return self::compose(
  574. '%sThe value specified is invalid',
  575. fValidationException::formatField(fORM::getColumnName($class, $column))
  576. );
  577. }
  578. }
  579. }
  580. }
  581. /**
  582. * Validates against a one-or-more rule
  583. *
  584. * @param fSchema $schema The schema object for the table
  585. * @param string $class The class the columns are part of
  586. * @param array &$values An associative array of all values for the record
  587. * @param array $columns The columns to check
  588. * @return string An error message for the rule
  589. */
  590. static private function checkOneOrMoreRule($schema, $class, &$values, $columns)
  591. {
  592. settype($columns, 'array');
  593. $found_value = FALSE;
  594. foreach ($columns as $column) {
  595. if (self::hasValue($schema, $class, $values, $column)) {
  596. $found_value = TRUE;
  597. }
  598. }
  599. if (!$found_value) {
  600. $column_names = array();
  601. foreach ($columns as $column) {
  602. $column_names[] = fORM::getColumnName($class, $column);
  603. }
  604. return self::compose(
  605. '%sPlease enter a value for at least one',
  606. fValidationException::formatField(join(', ', $column_names))
  607. );
  608. }
  609. }
  610. /**
  611. * Validates against an only-one rule
  612. *
  613. * @param fSchema $schema The schema object for the table
  614. * @param string $class The class the columns are part of
  615. * @param array &$values An associative array of all values for the record
  616. * @param array $columns The columns to check
  617. * @return string An error message for the rule
  618. */
  619. static private function checkOnlyOneRule($schema, $class, &$values, $columns)
  620. {
  621. settype($columns, 'array');
  622. $column_names = array();
  623. foreach ($columns as $column) {
  624. $column_names[] = fORM::getColumnName($class, $column);
  625. }
  626. $found_value = FALSE;
  627. foreach ($columns as $column) {
  628. if (self::hasValue($schema, $class, $values, $column)) {
  629. if ($found_value) {
  630. return self::compose(
  631. '%sPlease enter a value for only one',
  632. fValidationException::formatField(join(', ', $column_names))
  633. );
  634. }
  635. $found_value = TRUE;
  636. }
  637. }
  638. if (!$found_value) {
  639. return self::compose(
  640. '%sPlease enter a value for one',
  641. fValidationException::formatField(join(', ', $column_names))
  642. );
  643. }
  644. }
  645. /**
  646. * Makes sure a record with the same primary keys is not already in the database
  647. *
  648. * @param fActiveRecord $object The instance of the class to check
  649. * @param array &$values An associative array of all values going into the row (needs all for multi-field unique constraint checking)
  650. * @param array &$old_values The old values for the record
  651. * @return array A single element associative array with the key being the primary keys joined by ,s and the value being the error message
  652. */
  653. static private function checkPrimaryKeys($object, &$values, &$old_values)
  654. {
  655. $class = get_class($object);
  656. $table = fORM::tablize($class);
  657. $db = fORMDatabase::retrieve($class, 'read');
  658. $schema = fORMSchema::retrieve($class);
  659. $pk_columns = $schema->getKeys($table, 'primary');
  660. $columns = array();
  661. $found_value = FALSE;
  662. foreach ($pk_columns as $pk_column) {
  663. $columns[] = fORM::getColumnName($class, $pk_column);
  664. if ($values[$pk_column]) {
  665. $found_value = TRUE;
  666. }
  667. }
  668. if (!$found_value) {
  669. return;
  670. }
  671. $different = FALSE;
  672. foreach ($pk_columns as $pk_column) {
  673. if (!fActiveRecord::hasOld($old_values, $pk_column)) {
  674. continue;
  675. }
  676. $old_value = fActiveRecord::retrieveOld($old_values, $pk_column);
  677. $value = $values[$pk_column];
  678. if (self::isCaseInsensitive($class, $pk_column) && self::stringlike($value) && self::stringlike($old_value)) {
  679. if (fUTF8::lower($value) != fUTF8::lower($old_value)) {
  680. $different = TRUE;
  681. }
  682. } elseif ($old_value != $value) {
  683. $different = TRUE;
  684. }
  685. }
  686. if (!$different) {
  687. return;
  688. }
  689. try {
  690. $params = array(
  691. "SELECT %r FROM %r WHERE ",
  692. $pk_columns,
  693. $table
  694. );
  695. $column_info = $schema->getColumnInfo($table);
  696. $conditions = array();
  697. foreach ($pk_columns as $pk_column) {
  698. $value = $values[$pk_column];
  699. // This makes sure the query performs the way an insert will
  700. if ($value === NULL && $column_info[$pk_column]['not_null'] && $column_info[$pk_column]['default'] !== NULL) {
  701. $value = $column_info[$pk_column]['default'];
  702. }
  703. if (self::isCaseInsensitive($class, $pk_column) && self::stringlike($value)) {
  704. $condition = fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value);
  705. $conditions[] = str_replace('%r', 'LOWER(%r)', $condition);
  706. $params[] = $pk_column;
  707. $params[] = fUTF8::lower($value);
  708. } else {
  709. $conditions[] = fORMDatabase::makeCondition($schema, $table, $pk_column, '=', $value);
  710. $params[] = $pk_column;
  711. $params[] = $value;
  712. }
  713. }
  714. $params[0] .= join(' AND ', $conditions);
  715. $result = call_user_func_array($db->translatedQuery, $params);
  716. $result->tossIfNoRows();
  717. return array(join(',', $pk_columns) => self::compose(
  718. 'Another %1$s with the same %2$s already exists',
  719. fORM::getRecordName($class),
  720. fGrammar::joinArray($columns, 'and')
  721. ));
  722. } catch (fNoRowsException $e) { }
  723. }
  724. /**
  725. * Validates against a regex rule
  726. *
  727. * @param string $class The class the column is part of
  728. * @param array &$values An associative array of all values for the record
  729. * @param string $column The column to check
  730. * @param string $regex The PCRE regular expression
  731. * @param string $message The message to use if the value does not match the regular expression
  732. * @return string An error message for the rule
  733. */
  734. static private function checkRegexRule($class, &$values, $column, $regex, $message)
  735. {
  736. if ($values[$column] === NULL) {
  737. return;
  738. }
  739. if (preg_match($regex, $values[$column])) {
  740. return;
  741. }
  742. return self::compose(
  743. '%s' . str_replace('%', '%%', $message),
  744. fValidationException::formatField(fORM::getColumnName($class, $column))
  745. );
  746. }
  747. /**
  748. * Validates against a *-to-many one or more rule
  749. *
  750. * @param fActiveRecord $object The object being checked
  751. * @param array &$values The values for the object
  752. * @param array &$related_records The related records for the object
  753. * @param string $related_class The name of the related class
  754. * @param string $route The name of the route from the class to the related class
  755. * @return string An error message for the rule
  756. */
  757. static private function checkRelatedOneOrMoreRule($object, &$values, &$related_records, $related_class, $route)
  758. {
  759. $related_table = fORM::tablize($related_class);
  760. $class = get_class($object);
  761. $exists = $object->exists();
  762. $records_are_set = isset($related_records[$related_table][$route]);
  763. $has_records = $records_are_set && $related_records[$related_table][$route]['count'];
  764. if ($exists && (!$records_are_set || $has_records)) {
  765. return;
  766. }
  767. if (!$exists && $has_records) {
  768. return;
  769. }
  770. return self::compose(
  771. '%sPlease select at least one',
  772. fValidationException::formatField(fGrammar::pluralize(fORMRelated::getRelatedRecordName($class, $related_class, $route)))
  773. );
  774. }
  775. /**
  776. * Validates values against unique constraints
  777. *
  778. * @param fActiveRecord $object The instance of the class to check
  779. * @param array &$values The values to check
  780. * @param array &$old_values The old values for the record
  781. * @return array An aray of error messages for the unique constraints
  782. */
  783. static private function checkUniqueConstraints($object, &$values, &$old_values)
  784. {
  785. $class = get_class($object);
  786. $table = fORM::tablize($class);
  787. $db = fORMDatabase::retrieve($class, 'read');
  788. $schema = fORMSchema::retrieve($class);
  789. $key_info = $schema->getKeys($table);
  790. $pk_columns = $key_info['primary'];
  791. $unique_keys = $key_info['unique'];
  792. $messages = array();
  793. foreach ($unique_keys AS $unique_columns) {
  794. settype($unique_columns, 'array');
  795. // NULL values are unique
  796. $found_not_null = FALSE;
  797. foreach ($unique_columns as $unique_column) {
  798. if ($values[$unique_column] !== NULL) {
  799. $found_not_null = TRUE;
  800. }
  801. }
  802. if (!$found_not_null) {
  803. continue;
  804. }
  805. $params = array(
  806. "SELECT %r FROM %r WHERE ",
  807. $key_info['primary'],
  808. $table
  809. );
  810. $column_info = $schema->getColumnInfo($table);
  811. $conditions = array();
  812. foreach ($unique_columns as $unique_column) {
  813. $value = $values[$unique_column];
  814. // This makes sure the query performs the way an insert will
  815. if ($value === NULL && $column_info[$unique_column]['not_null'] && $column_info[$unique_column]['default'] !== NULL) {
  816. $value = $column_info[$unique_column]['default'];
  817. }
  818. if (self::isCaseInsensitive($class, $unique_column) && self::stringlike($value)) {
  819. $condition = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value);
  820. $conditions[] = str_replace('%r', 'LOWER(%r)', $condition);
  821. $params[] = $table . '.' . $unique_column;
  822. $params[] = fUTF8::lower($value);
  823. } else {
  824. $conditions[] = fORMDatabase::makeCondition($schema, $table, $unique_column, '=', $value);
  825. $params[] = $table . '.' . $unique_column;
  826. $params[] = $value;
  827. }
  828. }
  829. $params[0] .= join(' AND ', $conditions);
  830. if ($object->exists()) {
  831. foreach ($pk_columns as $pk_column) {
  832. $value = fActiveRecord::retrieveOld($old_values, $pk_column, $values[$pk_column]);
  833. $params[0] .= ' AND ' . fORMDatabase::makeCondition($schema, $table, $pk_column, '<>', $value);
  834. $params[] = $table . '.' . $pk_column;
  835. $params[] = $value;
  836. }
  837. }
  838. try {
  839. $result = call_user_func_array($db->translatedQuery, $params);
  840. $result->tossIfNoRows();
  841. // If an exception was not throw, we have existing values
  842. $column_names = array();
  843. foreach ($unique_columns as $unique_column) {
  844. $column_names[] = fORM::getColumnName($class, $unique_column);
  845. }
  846. if (sizeof($column_names) == 1) {
  847. $messages[join('', $unique_columns)] = self::compose(
  848. '%sThe value specified must be unique, however it already exists',
  849. fValidationException::formatField(join('', $column_names))
  850. );
  851. } else {
  852. $messages[join(',', $unique_columns)] = self::compose(
  853. '%sThe values specified must be a unique combination, however the specified combination already exists',
  854. fValidationException::formatField(join(', ', $column_names))
  855. );
  856. }
  857. } catch (fNoRowsException $e) { }
  858. }
  859. return $messages;
  860. }
  861. /**
  862. * Validates against a valid values rule
  863. *
  864. * @param string $class The class this rule applies to
  865. * @param array &$values An associative array of all values for the record
  866. * @param string $column The column the rule applies to
  867. * @param array $valid_values An array of valid values to check the column against
  868. * @return string The error message for the rule specified
  869. */
  870. static private function checkValidValuesRule($class, &$values, $column, $valid_values)
  871. {
  872. if ($values[$column] === NULL) {
  873. return;
  874. }
  875. if (!in_array($values[$column], $valid_values)) {
  876. return self::compose(
  877. '%1$sPlease choose from one of the following: %2$s',
  878. fValidationException::formatField(fORM::getColumnName($class, $column)),
  879. join(', ', $valid_values)
  880. );
  881. }
  882. }
  883. /**
  884. * Composes text using fText if loaded
  885. *
  886. * @param string $message The message to compose
  887. * @param mixed $component A string or number to insert into the message
  888. * @param mixed ...
  889. * @return string The composed and possible translated message
  890. */
  891. static private function compose($message)
  892. {
  893. $args = array_slice(func_get_args(), 1);
  894. if (class_exists('fText', FALSE)) {
  895. return call_user_func_array(
  896. array('fText', 'compose'),
  897. array($message, $args)
  898. );
  899. } else {
  900. return vsprintf($message, $args);
  901. }
  902. }
  903. /**
  904. * Makes sure each rule array is set to at least an empty array
  905. *
  906. * @internal
  907. *
  908. * @param string $class The class to initilize the arrays for
  909. * @return void
  910. */
  911. static private function initializeRuleArrays($class)
  912. {
  913. self::$conditional_rules[$class] = (isset(self::$conditional_rules[$class])) ? self::$conditional_rules[$class] : array();
  914. self::$one_or_more_rules[$class] = (isset(self::$one_or_more_rules[$class])) ? self::$one_or_more_rules[$class] : array();
  915. self::$only_one_rules[$class] = (isset(self::$only_one_rules[$class])) ? self::$only_one_rules[$class] : array();
  916. self::$regex_rules[$class] = (isset(self::$regex_rules[$class])) ? self::$regex_rules[$class] : array();
  917. self::$related_one_or_more_rules[$class] = (isset(self::$related_one_or_more_rules[$class])) ? self::$related_one_or_more_rules[$class] : array();
  918. self::$valid_values_rules[$class] = (isset(self::$valid_values_rules[$class])) ? self::$valid_values_rules[$class] : array();
  919. }
  920. /**
  921. * Adds metadata about features added by this class
  922. *
  923. * @internal
  924. *
  925. * @param string $class The class being inspected
  926. * @param string $column The column being inspected
  927. * @param array &$metadata The array of metadata about a column
  928. * @return void
  929. */
  930. static public function inspect($class, $column, &$metadata)
  931. {
  932. if (!empty(self::$valid_values_rules[$class][$column])) {
  933. $metadata['valid_values'] = self::$valid_values_rules[$class][$column];
  934. }
  935. }
  936. /**
  937. * Checks to see if a columns has a value, but based on the schema and if the column allows NULL
  938. *
  939. * If the columns allows NULL values, than anything other than NULL
  940. * will be returned as TRUE. If the column does not allow NULL and
  941. * the value is anything other than the "empty" value for that data type,
  942. * then TRUE will be returned.
  943. *
  944. * The values that are considered "empty" for each data type are as follows.
  945. * Please note that there is no "empty" value for dates, times or
  946. * timestamps.
  947. *
  948. * - Blob: ''
  949. * - Boolean: FALSE
  950. * - Float: 0.0
  951. * - Integer: 0
  952. * - String: ''
  953. *
  954. * @internal
  955. *
  956. * @param fSchema $schema The schema object for the table
  957. * @param string $class The class the column is part of
  958. * @param array &$values An associative array of all values for the record
  959. * @param array $columns The column to check
  960. * @return string An error message for the rule
  961. */
  962. static public function hasValue($schema, $class, &$values, $column)
  963. {
  964. $value = $values[$column];
  965. if ($value === NULL) {
  966. return FALSE;
  967. }
  968. $table = fORM::tablize($class);
  969. $data_type = $schema->getColumnInfo($table, $column, 'type');
  970. $allows_null = !$schema->getColumnInfo($table, $column, 'not_null');
  971. if ($allows_null) {
  972. return TRUE;
  973. }
  974. switch ($data_type) {
  975. case 'blob':
  976. case 'char':
  977. case 'text':
  978. case 'varchar':
  979. if ($value === '') {
  980. return FALSE;
  981. }
  982. break;
  983. case 'boolean':
  984. if ($value === FALSE) {
  985. return FALSE;
  986. }
  987. break;
  988. case 'integer':
  989. if ($value === 0 || $value === '0') {
  990. return FALSE;
  991. }
  992. break;
  993. case 'float':
  994. if (preg_match('#^0(\.0*)?$|^\.0+$#D', $value)) {
  995. return FALSE;
  996. }
  997. break;
  998. }
  999. return TRUE;
  1000. }
  1001. /**
  1002. * Checks to see if a column has been set as case insensitive
  1003. *
  1004. * @internal
  1005. *
  1006. * @param string $class The class to check
  1007. * @param string $column The column to check
  1008. * @return boolean If the column is set to be case insensitive
  1009. */
  1010. static private function isCaseInsensitive($class, $column)
  1011. {
  1012. return isset(self::$case_insensitive_columns[$class][$column]);
  1013. }
  1014. /**
  1015. * Returns FALSE if the string is empty - used for array filtering
  1016. *
  1017. * @param string $string The string to check
  1018. * @return boolean If the string is not blank
  1019. */
  1020. static private function isNonBlankString($string)
  1021. {
  1022. return ((string) $string) !== '';
  1023. }
  1024. /**
  1025. * Removes a regex replacement
  1026. *
  1027. * @internal
  1028. *
  1029. * @param mixed $class The class name or instance of the class the columns exists in
  1030. * @param string $search The string to search for
  1031. * @param string $replace The string to replace with
  1032. * @return void
  1033. */
  1034. static public function removeRegexReplacement($class, $search, $replace)
  1035. {
  1036. $class = fORM::getClass($class);
  1037. if (!isset(self::$regex_replacements[$class])) {
  1038. self::$regex_replacements[$class] = array(
  1039. 'search' => array(),
  1040. 'replace' => array()
  1041. );
  1042. }
  1043. $replacements = count(self::$regex_replacements[$class]['search']);
  1044. for ($i = 0; $i < $replacements; $i++) {
  1045. $match_search = self::$regex_replacements[$class]['search'][$i] == $search;
  1046. $match_replace = self::$regex_replacements[$class]['replace'][$i] == $replace;
  1047. if ($match_search && $match_replace) {
  1048. unset(self::$regex_replacements[$class]['search'][$i]);
  1049. unset(self::$regex_replacements[$class]['replace'][$i]);
  1050. }
  1051. }
  1052. // Remove the any gaps in the arrays
  1053. self::$regex_replacements[$class]['search'] = array_merge(self::$regex_replacements[$class]['search']);
  1054. self::$regex_replacements[$class]['replace'] = array_merge(self::$regex_replacements[$class]['replace']);
  1055. }
  1056. /**
  1057. * Removes a string replacement
  1058. *
  1059. * @internal
  1060. *
  1061. * @param mixed $class The class name or instance of the class the columns exists in
  1062. * @param string $search The string to search for
  1063. * @param string $replace The string to replace with
  1064. * @return void
  1065. */
  1066. static public function removeStringReplacement($class, $search, $replace)
  1067. {
  1068. $class = fORM::getClass($class);
  1069. if (!isset(self::$string_replacements[$class])) {
  1070. self::$string_replacements[$class] = array(
  1071. 'search' => array(),
  1072. 'replace' => array()
  1073. );
  1074. }
  1075. $replacements = count(self::$string_replacements[$class]['search']);
  1076. for ($i = 0; $i < $replacements; $i++) {
  1077. $match_search = self::$string_replacements[$class]['search'][$i] == $search;
  1078. $match_replace = self::$string_replacements[$class]['replace'][$i] == $replace;
  1079. if ($match_search && $match_replace) {
  1080. unset(self::$string_replacements[$class]['search'][$i]);
  1081. unset(self::$string_replacements[$class]['replace'][$i]);
  1082. }
  1083. }
  1084. // Remove the any gaps in the arrays
  1085. self::$string_replacements[$class]['search'] = array_merge(self::$string_replacements[$class]['search']);
  1086. self::$string_replacements[$class]['replace'] = array_merge(self::$string_replacements[$class]['replace']);
  1087. }
  1088. /**
  1089. * Reorders list items in an html string based on their contents
  1090. *
  1091. * @internal
  1092. *
  1093. * @param string $class The class to reorder messages for
  1094. * @param array $messages An array of the messages
  1095. * @return array The reordered messages
  1096. */
  1097. static public function reorderMessages($class, $messages)
  1098. {
  1099. if (!isset(self::$message_orders[$class])) {
  1100. return $messages;
  1101. }
  1102. $matches = self::$message_orders[$class];
  1103. $ordered_items = array_fill(0, sizeof($matches), array());
  1104. $other_items = array();
  1105. foreach ($messages as $key => $message) {
  1106. foreach ($matches as $num => $match_string) {
  1107. $string = is_array($message) ? $message['name'] : $message;
  1108. if (fUTF8::ipos($string, $match_string) !== FALSE) {
  1109. $ordered_items[$num][$key] = $message;
  1110. continue 2;
  1111. }
  1112. }
  1113. $other_items[$key] = $message;
  1114. }
  1115. $final_list = array();
  1116. foreach ($ordered_items as $ordered_item) {
  1117. $final_list = array_merge($final_list, $ordered_item);
  1118. }
  1119. return array_merge($final_list, $other_items);
  1120. }
  1121. /**
  1122. * Takes a list of messages and performs string and regex replacements on them
  1123. *
  1124. * @internal
  1125. *
  1126. * @param string $class The class to reorder messages for
  1127. * @param array $messages The array of messages
  1128. * @return array The new array of messages
  1129. */
  1130. static public function replaceMessages($class, $messages)
  1131. {
  1132. if (isset(self::$string_replacements[$class])) {
  1133. foreach ($messages as $key => $message) {
  1134. if (is_array($message)) {
  1135. continue;
  1136. }
  1137. $messages[$key] = str_replace(
  1138. self::$string_replacements[$class]['search'],
  1139. self::$string_replacements[$class]['replace'],
  1140. $message
  1141. );
  1142. }
  1143. }
  1144. if (isset(self::$regex_replacements[$class])) {
  1145. foreach ($messages as $key => $message) {
  1146. if (is_array($message)) {
  1147. continue;
  1148. }
  1149. $messages[$key] = preg_replace(
  1150. self::$regex_replacements[$class]['search'],
  1151. self::$regex_replacements[$class]['replace'],
  1152. $message
  1153. );
  1154. }
  1155. }
  1156. return array_filter($messages, array('fORMValidation', 'isNonBlankString'));
  1157. }
  1158. /**
  1159. * Resets the configuration of the class
  1160. *
  1161. * @internal
  1162. *
  1163. * @return void
  1164. */
  1165. static public function reset()
  1166. {
  1167. self::$case_insensitive_columns = array();
  1168. self::$conditional_rules = array();
  1169. self::$message_orders = array();
  1170. self::$one_or_more_rules = array();
  1171. self::$only_one_rules = array();
  1172. self::$regex_replacements = array();
  1173. self::$related_one_or_more_rules = array();
  1174. self::$regex_rules = array();
  1175. self::$required_rules = array();
  1176. self::$string_replacements = array();
  1177. self::$valid_values_rules = array();
  1178. }
  1179. /**
  1180. * Sets a column to be compared in a case-insensitive manner when checking `UNIQUE` and `PRIMARY KEY` constraints
  1181. *
  1182. * @param mixed $class The class name or instance of the class the column is located in
  1183. * @param string $column The column to set as case-insensitive
  1184. * @return void
  1185. */
  1186. static public function setColumnCaseInsensitive($class, $column)
  1187. {
  1188. $class = fORM::getClass($class);
  1189. $table = fORM::tablize($class);
  1190. $schema = fORMSchema::retrieve($class);
  1191. $type = $schema->getColumnInfo($table, $column, 'type');
  1192. $valid_types = array('varchar', 'char', 'text');
  1193. if (!in_array($type, $valid_types)) {
  1194. throw new fProgrammerException(
  1195. 'The column specified, %1$s, is of the data type %2$s. Must be one of %3$s to be treated as case insensitive.',
  1196. $column,
  1197. $type,
  1198. join(', ', $valid_types)
  1199. );
  1200. }
  1201. if (!isset(self::$case_insensitive_columns[$class])) {
  1202. self::$case_insensitive_columns[$class] = array();
  1203. }
  1204. self::$case_insensitive_columns[$class][$column] = TRUE;
  1205. }
  1206. /**
  1207. * Allows setting the order that the list items in a message will be displayed
  1208. *
  1209. * All string comparisons during the reordering process are done in a
  1210. * case-insensitive manner.
  1211. *
  1212. * @param mixed $class The class name or an instance of the class to set the message order for
  1213. * @param array $matches This should be an ordered array of strings. If a line contains the string it will be displayed in the relative order it occurs in this array.
  1214. * @return void
  1215. */
  1216. static public function setMessageOrder($class, $matches)
  1217. {
  1218. $class = fORM::getClass($class);
  1219. // Handle the alternate form allowed with fValidation::setMessageOrder()
  1220. $args = func_get_args();
  1221. array_shift($args);
  1222. if (sizeof($args) == 1 && is_array($args[0])) {
  1223. $matches = $args[0];
  1224. }
  1225. uasort($matches, array('self', 'sortMessageMatches'));
  1226. self::$message_orders[$class] = $matches;
  1227. }
  1228. /**
  1229. * Compares the message matching strings by longest first so that the longest matches are made first
  1230. *
  1231. * @param string $a The first string to compare
  1232. * @param string $b The second string to compare
  1233. * @return integer `-1` if `$a` is longer than `$b`, `0` if they are equal length, `1` if `$a` is shorter than `$b`
  1234. */
  1235. static private function sortMessageMatches($a, $b)
  1236. {
  1237. if (strlen($a) == strlen($b)) {
  1238. return 0;
  1239. }
  1240. if (strlen($a) > strlen($b)) {
  1241. return -1;
  1242. }
  1243. return 1;
  1244. }
  1245. /**
  1246. * Returns `TRUE` for non-empty strings, numbers, objects, empty numbers and string-like numbers (such as `0`, `0.0`, `'0'`)
  1247. *
  1248. * @param mixed $value The value to check
  1249. * @return boolean If the value is string-like
  1250. */
  1251. static private function stringlike($value)
  1252. {
  1253. if ((!is_string($value) && !is_object($value) && !is_numeric($value)) || !strlen(trim($value))) {
  1254. return FALSE;
  1255. }
  1256. return TRUE;
  1257. }
  1258. /**
  1259. * Validates values for an fActiveRecord object against the database schema and any additional rules that have been added
  1260. *
  1261. * @internal
  1262. *
  1263. * @param fActiveRecord $object The instance of the class to validate
  1264. * @param array $values The values to validate
  1265. * @param array $old_values The old values for the record
  1266. * @return array An array of messages
  1267. */
  1268. static public function validate($object, $values, $old_values)
  1269. {
  1270. $class = get_class($object);
  1271. $table = fORM::tablize($class);
  1272. $schema = fORMSchema::retrieve($class);
  1273. self::initializeRuleArrays($class);
  1274. $validation_messages = array();
  1275. // Convert objects into values for validation
  1276. foreach ($values as $column => $value) {
  1277. $values[$column] = fORM::scalarize($class, $column, $value);
  1278. }
  1279. foreach ($old_values as $column => $column_values) {
  1280. foreach ($column_values as $key =

Large files files are truncated, but you can click here to view the full file