PageRenderTime 62ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 1ms

/classes/fORMValidation.php

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

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