ModelValidator.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. <?php
  2. /**
  3. * ModelValidator.
  4. *
  5. * Provides the Model validation logic.
  6. *
  7. * PHP versions 5
  8. *
  9. * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
  10. * Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  11. *
  12. * Licensed under The MIT License
  13. * Redistributions of files must retain the above copyright notice.
  14. *
  15. * @copyright Copyright 2005-2011, Cake Software Foundation, Inc. (http://cakefoundation.org)
  16. * @link http://cakephp.org CakePHP(tm) Project
  17. * @package Cake.Model
  18. * @since CakePHP(tm) v 2.2.0
  19. * @license MIT License (http://www.opensource.org/licenses/mit-license.php)
  20. */
  21. App::uses('CakeValidationSet', 'Model/Validator');
  22. /**
  23. * ModelValidator object.
  24. *
  25. * @package Cake.Model
  26. * @link http://book.cakephp.org/2.0/en/data-validation.html
  27. */
  28. class ModelValidator implements ArrayAccess, IteratorAggregate, Countable {
  29. /**
  30. * Holds the CakeValidationSet objects array
  31. *
  32. * @var array
  33. */
  34. protected $_fields = array();
  35. /**
  36. * Holds the reference to the model the Validator is attached to
  37. *
  38. * @var Model
  39. */
  40. protected $_model = array();
  41. /**
  42. * The validators $validate property
  43. *
  44. * @var array
  45. */
  46. protected $_validate = array();
  47. /**
  48. * Holds the available custom callback methods
  49. *
  50. * @var array
  51. */
  52. protected $_methods = array();
  53. /**
  54. * Constructor
  55. *
  56. * @param Model $Model A reference to the Model the Validator is attached to
  57. */
  58. public function __construct(Model $Model) {
  59. $this->_model = $Model;
  60. }
  61. /**
  62. * Returns true if all fields pass validation. Will validate hasAndBelongsToMany associations
  63. * that use the 'with' key as well. Since _saveMulti is incapable of exiting a save operation.
  64. *
  65. * Will validate the currently set data. Use Model::set() or Model::create() to set the active data.
  66. *
  67. * @param array $options An optional array of custom options to be made available in the beforeValidate callback
  68. * @return boolean True if there are no errors
  69. */
  70. public function validates($options = array()) {
  71. $errors = $this->errors($options);
  72. if (empty($errors) && $errors !== false) {
  73. $errors = $this->_validateWithModels($options);
  74. }
  75. if (is_array($errors)) {
  76. return count($errors) === 0;
  77. }
  78. return $errors;
  79. }
  80. /**
  81. * Validates a single record, as well as all its directly associated records.
  82. *
  83. * #### Options
  84. *
  85. * - atomic: If true (default), returns boolean. If false returns array.
  86. * - fieldList: Equivalent to the $fieldList parameter in Model::save()
  87. * - deep: If set to true, not only directly associated data , but deeper nested associated data is validated as well.
  88. *
  89. * @param array $data Record data to validate. This should be an array indexed by association name.
  90. * @param array $options Options to use when validating record data (see above), See also $options of validates().
  91. * @return array|boolean If atomic: True on success, or false on failure.
  92. * Otherwise: array similar to the $data array passed, but values are set to true/false
  93. * depending on whether each record validated successfully.
  94. */
  95. public function validateAssociated($data, $options = array()) {
  96. $model = $this->getModel();
  97. $options = array_merge(array('atomic' => true, 'deep' => false), $options);
  98. $model->validationErrors = $validationErrors = $return = array();
  99. if (!($model->create($data) && $model->validates($options))) {
  100. $validationErrors[$model->alias] = $model->validationErrors;
  101. $return[$model->alias] = false;
  102. } else {
  103. $return[$model->alias] = true;
  104. }
  105. $associations = $model->getAssociated();
  106. foreach ($data as $association => $values) {
  107. $validates = true;
  108. if (isset($associations[$association])) {
  109. if (in_array($associations[$association], array('belongsTo', 'hasOne'))) {
  110. if ($options['deep']) {
  111. $validates = $model->{$association}->validateAssociated($values, $options);
  112. } else {
  113. $validates = $model->{$association}->create($values) !== null && $model->{$association}->validates($options);
  114. }
  115. if (is_array($validates)) {
  116. if (in_array(false, $validates, true)) {
  117. $validates = false;
  118. } else {
  119. $validates = true;
  120. }
  121. }
  122. $return[$association] = $validates;
  123. } elseif ($associations[$association] === 'hasMany') {
  124. $validates = $model->{$association}->validateMany($values, $options);
  125. $return[$association] = $validates;
  126. }
  127. if (!$validates || (is_array($validates) && in_array(false, $validates, true))) {
  128. $validationErrors[$association] = $model->{$association}->validationErrors;
  129. }
  130. }
  131. }
  132. $model->validationErrors = $validationErrors;
  133. if (isset($validationErrors[$model->alias])) {
  134. $model->validationErrors = $validationErrors[$model->alias];
  135. }
  136. if (!$options['atomic']) {
  137. return $return;
  138. }
  139. if ($return[$model->alias] === false || !empty($model->validationErrors)) {
  140. return false;
  141. }
  142. return true;
  143. }
  144. /**
  145. * Validates multiple individual records for a single model
  146. *
  147. * #### Options
  148. *
  149. * - atomic: If true (default), returns boolean. If false returns array.
  150. * - fieldList: Equivalent to the $fieldList parameter in Model::save()
  151. * - deep: If set to true, all associated data will be validated as well.
  152. *
  153. * @param array $data Record data to validate. This should be a numerically-indexed array
  154. * @param array $options Options to use when validating record data (see above), See also $options of validates().
  155. * @return boolean True on success, or false on failure.
  156. * @return mixed If atomic: True on success, or false on failure.
  157. * Otherwise: array similar to the $data array passed, but values are set to true/false
  158. * depending on whether each record validated successfully.
  159. */
  160. public function validateMany($data, $options = array()) {
  161. $model = $this->getModel();
  162. $options = array_merge(array('atomic' => true, 'deep' => false), $options);
  163. $model->validationErrors = $validationErrors = $return = array();
  164. foreach ($data as $key => $record) {
  165. if ($options['deep']) {
  166. $validates = $model->validateAssociated($record, $options);
  167. } else {
  168. $validates = $model->create($record) && $model->validates($options);
  169. }
  170. if ($validates === false || (is_array($validates) && in_array(false, $validates, true))) {
  171. $validationErrors[$key] = $model->validationErrors;
  172. $validates = false;
  173. } else {
  174. $validates = true;
  175. }
  176. $return[$key] = $validates;
  177. }
  178. $model->validationErrors = $validationErrors;
  179. if (!$options['atomic']) {
  180. return $return;
  181. }
  182. if (empty($model->validationErrors)) {
  183. return true;
  184. }
  185. return false;
  186. }
  187. /**
  188. * Returns an array of fields that have failed validation. On the current model.
  189. *
  190. * @param string $options An optional array of custom options to be made available in the beforeValidate callback
  191. * @return array Array of invalid fields
  192. * @see Model::validates()
  193. */
  194. public function errors($options = array()) {
  195. if (!$this->_triggerBeforeValidate($options)) {
  196. return false;
  197. }
  198. $model = $this->getModel();
  199. if (!$this->_parseRules()) {
  200. return $model->validationErrors;
  201. }
  202. $fieldList = isset($options['fieldList']) ? $options['fieldList'] : array();
  203. $exists = $model->exists();
  204. $methods = $this->getMethods();
  205. $fields = $this->_validationList($fieldList);
  206. foreach ($fields as $field) {
  207. $field->setMethods($methods);
  208. $field->setValidationDomain($model->validationDomain);
  209. $data = isset($model->data[$model->alias]) ? $model->data[$model->alias] : array();
  210. $errors = $field->validate($data, $exists);
  211. foreach ($errors as $error) {
  212. $this->invalidate($field->field, $error);
  213. }
  214. }
  215. return $model->validationErrors;
  216. }
  217. /**
  218. * Marks a field as invalid, optionally setting the name of validation
  219. * rule (in case of multiple validation for field) that was broken.
  220. *
  221. * @param string $field The name of the field to invalidate
  222. * @param string $value Name of validation rule that failed, or validation message to
  223. * be returned. If no validation key is provided, defaults to true.
  224. * @return void
  225. */
  226. public function invalidate($field, $value = true) {
  227. $this->getModel()->validationErrors[$field][] = $value;
  228. }
  229. /**
  230. * Gets all possible custom methods from the Model, Behaviors and the Validator.
  231. * gets the corresponding methods.
  232. *
  233. * @return array The requested methods
  234. */
  235. public function getMethods() {
  236. if (!empty($this->_methods)) {
  237. return $this->_methods;
  238. }
  239. $methods = array();
  240. foreach (get_class_methods($this->_model) as $method) {
  241. $methods[strtolower($method)] = array($this->_model, $method);
  242. }
  243. foreach (array_keys($this->_model->Behaviors->methods()) as $method) {
  244. $methods += array(strtolower($method) => array($this->_model, $method));
  245. }
  246. return $this->_methods = $methods;
  247. }
  248. /**
  249. * Gets all fields if $name is null (default), or the field for fieldname $name if it's found.
  250. *
  251. * @param string $name [optional] The fieldname to fetch. Defaults to null.
  252. * @return mixed Either array of CakeValidationSet objects , single object for $name or false when $name not present in fields
  253. */
  254. public function getFields($name = null) {
  255. if ($name !== null && !empty($this->_fields[$name])) {
  256. return $this->_fields[$name];
  257. } elseif ($name !==null) {
  258. return false;
  259. }
  260. return $this->_fields;
  261. }
  262. /**
  263. * Sets the CakeValidationSet instances from the Model::$validate property after processing the fieldList and whiteList.
  264. * If Model::$validate is not set or empty, this method returns false. True otherwise.
  265. *
  266. * @return boolean True if Model::$validate was processed, false otherwise
  267. */
  268. protected function _parseRules() {
  269. if ($this->_validate === $this->_model->validate) {
  270. return true;
  271. }
  272. if (empty($this->_model->validate)) {
  273. $this->_validate = array();
  274. $this->_fields = array();
  275. return false;
  276. }
  277. $this->_validate = $this->_model->validate;
  278. $this->_fields = array();
  279. $methods = $this->getMethods();
  280. foreach ($this->_validate as $fieldName => $ruleSet) {
  281. $this->_fields[$fieldName] = new CakeValidationSet($fieldName, $ruleSet, $methods);
  282. }
  283. return true;
  284. }
  285. /**
  286. * Sets the I18n domain for validation messages. This method is chainable.
  287. *
  288. * @param string $validationDomain [optional] The validation domain to be used.
  289. * @return ModelValidator
  290. */
  291. public function setValidationDomain($validationDomain = null) {
  292. if (empty($validationDomain)) {
  293. $validationDomain = 'default';
  294. }
  295. $this->getModel()->validationDomain = $validationDomain;
  296. return $this;
  297. }
  298. /**
  299. * Gets the parent Model
  300. *
  301. * @return Model
  302. */
  303. public function getModel() {
  304. return $this->_model;
  305. }
  306. /**
  307. * Processes the Model's whitelist and returns the list of fields
  308. * to be validated
  309. *
  310. * @return array List of validation rules to be applied
  311. */
  312. protected function _validationList($fieldList = array()) {
  313. $model = $this->getModel();
  314. $whitelist = $model->whitelist;
  315. if (!empty($fieldList)) {
  316. if (!empty($fieldList[$model->alias]) && is_array($fieldList[$model->alias])) {
  317. $whitelist = $fieldList[$model->alias];
  318. } else {
  319. $whitelist = $fieldList;
  320. }
  321. }
  322. unset($fieldList);
  323. $validateList = array();
  324. if (!empty($whitelist)) {
  325. $this->validationErrors = array();
  326. foreach ((array)$whitelist as $f) {
  327. if (!empty($this->_fields[$f])) {
  328. $validateList[$f] = $this->_fields[$f];
  329. }
  330. }
  331. } else {
  332. return $this->_fields;
  333. }
  334. return $validateList;
  335. }
  336. /**
  337. * Runs validation for hasAndBelongsToMany associations that have 'with' keys
  338. * set. And data in the set() data set.
  339. *
  340. * @param array $options Array of options to use on Validation of with models
  341. * @return boolean Failure of validation on with models.
  342. * @see Model::validates()
  343. */
  344. protected function _validateWithModels($options) {
  345. $valid = true;
  346. $model = $this->getModel();
  347. foreach ($model->hasAndBelongsToMany as $assoc => $association) {
  348. if (empty($association['with']) || !isset($model->data[$assoc])) {
  349. continue;
  350. }
  351. list($join) = $model->joinModel($model->hasAndBelongsToMany[$assoc]['with']);
  352. $data = $model->data[$assoc];
  353. $newData = array();
  354. foreach ((array)$data as $row) {
  355. if (isset($row[$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
  356. $newData[] = $row;
  357. } elseif (isset($row[$join]) && isset($row[$join][$model->hasAndBelongsToMany[$assoc]['associationForeignKey']])) {
  358. $newData[] = $row[$join];
  359. }
  360. }
  361. if (empty($newData)) {
  362. continue;
  363. }
  364. foreach ($newData as $data) {
  365. $data[$model->hasAndBelongsToMany[$assoc]['foreignKey']] = $model->id;
  366. $model->{$join}->create($data);
  367. $valid = ($valid && $model->{$join}->validator()->validates($options));
  368. }
  369. }
  370. return $valid;
  371. }
  372. /**
  373. * Propagates the beforeValidate event
  374. *
  375. * @param array $options
  376. * @return boolean
  377. */
  378. protected function _triggerBeforeValidate($options = array()) {
  379. $model = $this->getModel();
  380. $event = new CakeEvent('Model.beforeValidate', $model, array($options));
  381. list($event->break, $event->breakOn) = array(true, false);
  382. $model->getEventManager()->dispatch($event);
  383. if ($event->isStopped()) {
  384. return false;
  385. }
  386. return true;
  387. }
  388. /**
  389. * Returns wheter a rule set is defined for a field or not
  390. *
  391. * @param string $field name of the field to check
  392. * @return boolean
  393. **/
  394. public function offsetExists($field) {
  395. $this->_parseRules();
  396. return isset($this->_fields[$field]);
  397. }
  398. /**
  399. * Returns the rule set for a field
  400. *
  401. * @param string $field name of the field to check
  402. * @return CakeValidationSet
  403. **/
  404. public function offsetGet($field) {
  405. $this->_parseRules();
  406. return $this->_fields[$field];
  407. }
  408. /**
  409. * Sets the rule set for a field
  410. *
  411. * @param string $field name of the field to set
  412. * @param array|CakeValidationSet $rules set of rules to apply to field
  413. * @return void
  414. **/
  415. public function offsetSet($field, $rules) {
  416. $this->_parseRules();
  417. if (!$rules instanceof CakeValidationSet) {
  418. $rules = new CakeValidationSet($field, $rules, $this->getMethods());
  419. }
  420. $this->_fields[$field] = $rules;
  421. }
  422. /**
  423. * Unsets the rulset for a field
  424. *
  425. * @param string $field name of the field to unset
  426. * @return void
  427. **/
  428. public function offsetUnset($field) {
  429. $this->_parseRules();
  430. unset($this->_fields[$field]);
  431. }
  432. /**
  433. * Returns an iterator for each of the fields to be validated
  434. *
  435. * @return ArrayIterator
  436. **/
  437. public function getIterator() {
  438. $this->_parseRules();
  439. return new ArrayIterator($this->_fields);
  440. }
  441. /**
  442. * Returns the number of fields having validation rules
  443. *
  444. * @return int
  445. **/
  446. public function count() {
  447. $this->_parseRules();
  448. return count($this->_fields);
  449. }
  450. }