Table.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. <?php
  2. namespace Tools\Model\Table;
  3. use Cake\Core\Configure;
  4. use Cake\ORM\Table as CakeTable;
  5. use Cake\Utility\Inflector;
  6. use Cake\Validation\Validation;
  7. use Cake\Validation\Validator;
  8. use Tools\Utility\Utility;
  9. use Cake\ORM\Query;
  10. use Cake\Event\Event;
  11. use Cake\I18n\Time;
  12. class Table extends CakeTable {
  13. public $order = null;
  14. /**
  15. * initialize()
  16. *
  17. * All models will automatically get Timestamp behavior attached
  18. * if created or modified exists.
  19. *
  20. * @param mixed $config
  21. * @return void
  22. */
  23. public function initialize(array $config) {
  24. // Shims
  25. if (isset($this->primaryKey)) {
  26. $this->primaryKey($this->primaryKey);
  27. }
  28. if (isset($this->displayField)) {
  29. $this->displayField($this->displayField);
  30. }
  31. $this->_shimRelations();
  32. $this->prefixOrderProperty();
  33. if (isset($this->actsAs)) {
  34. foreach ($this->actsAs as $name => $options) {
  35. if (is_numeric($name)) {
  36. $name = $options;
  37. $options = [];
  38. }
  39. $this->addBehavior($name, $options);
  40. }
  41. }
  42. if ($this->hasField('created') || $this->hasField('modified')) {
  43. $this->addBehavior('Timestamp');
  44. }
  45. }
  46. /**
  47. * Shim the 2.x way of class properties for relations.
  48. *
  49. * @return void
  50. */
  51. protected function _shimRelations() {
  52. if (!empty($this->belongsTo)) {
  53. foreach ($this->belongsTo as $k => $v) {
  54. if (is_int($k)) {
  55. $k = $v;
  56. $v = [];
  57. }
  58. if (!empty($v['className'])) {
  59. $v['className'] = Inflector::pluralize($v['className']);
  60. }
  61. $v = array_filter($v);
  62. $this->belongsTo(Inflector::pluralize($k), $v);
  63. }
  64. }
  65. if (!empty($this->hasOne)) {
  66. foreach ($this->hasOne as $k => $v) {
  67. if (is_int($k)) {
  68. $k = $v;
  69. $v = [];
  70. }
  71. if (!empty($v['className'])) {
  72. $v['className'] = Inflector::pluralize($v['className']);
  73. }
  74. $v = array_filter($v);
  75. $this->hasOne(Inflector::pluralize($k), $v);
  76. }
  77. }
  78. if (!empty($this->hasMany)) {
  79. foreach ($this->hasMany as $k => $v) {
  80. if (is_int($k)) {
  81. $k = $v;
  82. $v = [];
  83. }
  84. if (!empty($v['className'])) {
  85. $v['className'] = Inflector::pluralize($v['className']);
  86. }
  87. $v = array_filter($v);
  88. $this->hasMany(Inflector::pluralize($k), $v);
  89. }
  90. }
  91. if (!empty($this->hasAndBelongsToMany)) {
  92. foreach ($this->hasAndBelongsToMany as $k => $v) {
  93. if (is_int($k)) {
  94. $k = $v;
  95. $v = [];
  96. }
  97. if (!empty($v['className'])) {
  98. $v['className'] = Inflector::pluralize($v['className']);
  99. }
  100. $v = array_filter($v);
  101. $this->belongsToMany(Inflector::pluralize($k), $v);
  102. }
  103. }
  104. }
  105. /**
  106. * Shim the 2.x way of validate class properties.
  107. *
  108. * @param Validator $validator
  109. * @return Validator
  110. */
  111. public function validationDefault(Validator $validator) {
  112. if (!empty($this->validate)) {
  113. foreach ($this->validate as $field => $rules) {
  114. if (is_int($field)) {
  115. $field = $rules;
  116. $rules = [];
  117. }
  118. foreach ((array)$rules as $key => $rule) {
  119. if (isset($rule['required'])) {
  120. $validator->requirePresence($field, $rule['required']);
  121. unset($rule['required']);
  122. }
  123. if (isset($rule['allowEmpty'])) {
  124. $validator->allowEmpty($field, $rule['allowEmpty']);
  125. unset($rule['allowEmpty']);
  126. }
  127. if (isset($rule['message'])) {
  128. $rules[$key]['message'] = __($rule['message']);
  129. }
  130. }
  131. $validator->add($field, $rules);
  132. }
  133. }
  134. return $validator;
  135. }
  136. /**
  137. * Validator method used to check the uniqueness of a value for a column.
  138. * This is meant to be used with the validation API and not to be called
  139. * directly.
  140. *
  141. * ### Example:
  142. *
  143. * {{{
  144. * $validator->add('email', [
  145. * 'unique' => ['rule' => 'validateUnique', 'provider' => 'table']
  146. * ])
  147. * }}}
  148. *
  149. * Unique validation can be scoped to the value of another column:
  150. *
  151. * {{{
  152. * $validator->add('email', [
  153. * 'unique' => [
  154. * 'rule' => ['validateUnique', ['scope' => 'site_id']],
  155. * 'provider' => 'table'
  156. * ]
  157. * ]);
  158. * }}}
  159. *
  160. * In the above example, the email uniqueness will be scoped to only rows having
  161. * the same site_id. Scoping will only be used if the scoping field is present in
  162. * the data to be validated.
  163. *
  164. * @override To allow multiple scoped values
  165. *
  166. * @param mixed $value The value of column to be checked for uniqueness
  167. * @param array $options The options array, optionally containing the 'scope' key
  168. * @param array $context The validation context as provided by the validation routine
  169. * @return bool true if the value is unique
  170. */
  171. public function validateUnique($value, array $options, array $context = []) {
  172. if (empty($context)) {
  173. $context = $options;
  174. }
  175. $conditions = [$context['field'] => $value];
  176. if (!empty($options['scope'])) {
  177. foreach ((array)$options['scope'] as $scope) {
  178. if (!isset($context['data'][$scope])) {
  179. continue;
  180. }
  181. $scopedValue = $context['data'][$scope];
  182. $conditions[$scope] = $scopedValue;
  183. }
  184. }
  185. if (!$context['newRecord']) {
  186. $keys = (array)$this->primaryKey();
  187. $not = [];
  188. foreach ($keys as $key) {
  189. if (isset($context['data'][$key])) {
  190. $not[$key] = $context['data'][$key];
  191. }
  192. }
  193. $conditions['NOT'] = $not;
  194. }
  195. return !$this->exists($conditions);
  196. }
  197. /**
  198. * Checks a record, if it is unique - depending on other fields in this table (transfered as array)
  199. * example in model: 'rule' => array ('validateUnique', array('belongs_to_table_id','some_id','user_id')),
  200. * if all keys (of the array transferred) match a record, return false, otherwise true
  201. *
  202. * @param array $fields Other fields to depend on
  203. * TODO: add possibity of deep nested validation (User -> Comment -> CommentCategory: UNIQUE comment_id, Comment.user_id)
  204. * @param array $options
  205. * - requireDependentFields Require all dependent fields for the validation rule to return true
  206. * @return bool Success
  207. */
  208. public function validateUniqueExt($fieldValue, $fields = [], $options = []) {
  209. $id = (!empty($this->data[$this->alias][$this->primaryKey]) ? $this->data[$this->alias][$this->primaryKey] : 0);
  210. if (!$id && $this->id) {
  211. $id = $this->id;
  212. }
  213. $conditions = [
  214. $this->alias . '.' . $fieldName => $fieldValue,
  215. $this->alias . '.id !=' => $id];
  216. $fields = (array)$fields;
  217. if (!array_key_exists('allowEmpty', $fields)) {
  218. foreach ($fields as $dependingField) {
  219. if (isset($this->data[$this->alias][$dependingField])) { // add ONLY if some content is transfered (check on that first!)
  220. $conditions[$this->alias . '.' . $dependingField] = $this->data[$this->alias][$dependingField];
  221. } elseif (isset($this->data['Validation'][$dependingField])) { // add ONLY if some content is transfered (check on that first!
  222. $conditions[$this->alias . '.' . $dependingField] = $this->data['Validation'][$dependingField];
  223. } elseif (!empty($id)) {
  224. // manual query! (only possible on edit)
  225. $res = $this->find('first', ['fields' => [$this->alias . '.' . $dependingField], 'conditions' => [$this->alias . '.id' => $id]]);
  226. if (!empty($res)) {
  227. $conditions[$this->alias . '.' . $dependingField] = $res[$this->alias][$dependingField];
  228. }
  229. } else {
  230. if (!empty($options['requireDependentFields'])) {
  231. trigger_error('Required field ' . $dependingField . ' for validateUnique validation not present');
  232. return false;
  233. }
  234. return true;
  235. }
  236. }
  237. }
  238. $this->recursive = -1;
  239. if (count($conditions) > 2) {
  240. $this->recursive = 0;
  241. }
  242. $options = ['fields' => [$this->alias . '.' . $this->primaryKey], 'conditions' => $conditions];
  243. $res = $this->find('first', $options);
  244. return empty($res);
  245. }
  246. /**
  247. * Shim to provide 2.x way of find('first') for easier upgrade.
  248. *
  249. * @param string $type
  250. * @param array $options
  251. * @return Query
  252. */
  253. public function find($type = 'all', $options = []) {
  254. if ($type === 'first') {
  255. return parent::find('all', $options)->first();
  256. }
  257. if ($type === 'count') {
  258. return parent::find('all', $options)->count();
  259. }
  260. return parent::find($type, $options);
  261. }
  262. /**
  263. * Convenience wrapper inspired by 2.x field() method. Only difference: full $options array
  264. * instead of just $conditions array.
  265. *
  266. * @param string $name
  267. * @param array $options
  268. * @return mixed Field value or null if not available
  269. */
  270. public function field($name, array $options = []) {
  271. $result = $this->find('all', $options)->first();
  272. if (!$result) {
  273. return null;
  274. }
  275. return $result->get($name);
  276. }
  277. /**
  278. * Shim of 2.x field() method.
  279. *
  280. * @param string $name
  281. * @param array $conditions
  282. * @return mixed Field value or null if not available
  283. * @deprecated Port to field() with full $options array
  284. */
  285. public function fieldByConditions($name, array $conditions = []) {
  286. return $this->field($name, ['conditions' => $conditions]);
  287. }
  288. /**
  289. * Sets the default ordering as 2.x shim.
  290. *
  291. * If you don't want that, don't call parent when overwriting it in extending classes.
  292. *
  293. * @param Event $event
  294. * @param Query $query
  295. * @param array $options
  296. * @param boolean $primary
  297. * @return Query
  298. */
  299. public function beforeFind(Event $event, Query $query, $options, $primary) {
  300. $order = $query->clause('order');
  301. if (($order === null || !count($order)) && !empty($this->order)) {
  302. $query->order($this->order);
  303. }
  304. return $query;
  305. }
  306. /**
  307. * Prefixes the order property with the actual alias if its a string or array.
  308. *
  309. * The core fails on using the proper prefix when building the query with two
  310. * different tables.
  311. *
  312. * @return void
  313. */
  314. public function prefixOrderProperty() {
  315. if (is_string($this->order)) {
  316. $this->order = $this->_prefixAlias($this->order);
  317. }
  318. if (is_array($this->order)) {
  319. foreach ($this->order as $key => $value) {
  320. if (is_numeric($key)) {
  321. $this->order[$key] = $this->_prefixAlias($value);
  322. } else {
  323. $newKey = $this->_prefixAlias($key);
  324. $this->order[$newKey] = $value;
  325. if ($newKey !== $key) {
  326. unset($this->order[$key]);
  327. }
  328. }
  329. }
  330. }
  331. }
  332. /**
  333. * Checks if a string of a field name contains a dot if not it will add it and add the alias prefix.
  334. *
  335. * @param string
  336. * @return string
  337. */
  338. protected function _prefixAlias($string) {
  339. if (strpos($string, '.') === false) {
  340. return $this->alias() . '.' . $string;
  341. }
  342. return $string;
  343. }
  344. /**
  345. * Return the next auto increment id from the current table
  346. * UUIDs will return false
  347. *
  348. * @return int|bool next auto increment value or False on failure
  349. */
  350. public function getNextAutoIncrement() {
  351. $query = "SHOW TABLE STATUS WHERE name = '" . $this->table() . "'";
  352. $statement = $this->_connection->execute($query);
  353. $result = $statement->fetch();
  354. if (!isset($result[10])) {
  355. return false;
  356. }
  357. return (int)$result[10];
  358. }
  359. /**
  360. * truncate()
  361. *
  362. * @return void
  363. */
  364. public function truncate() {
  365. $sql = $this->schema()->truncateSql($this->_connection);
  366. foreach ($sql as $snippet) {
  367. $this->_connection->execute($snippet);
  368. }
  369. }
  370. /**
  371. * Get all related entries that have been used so far
  372. *
  373. * @param string $tableName The related model
  374. * @param string $groupField Field to group by
  375. * @param string $type Find type
  376. * @param array $options
  377. * @return array
  378. */
  379. public function getRelatedInUse($tableName, $groupField = null, $type = 'all', $options = []) {
  380. if ($groupField === null) {
  381. $groupField = $this->belongsTo[$tableName]['foreignKey'];
  382. }
  383. $defaults = [
  384. 'contain' => [$tableName],
  385. 'group' => $groupField,
  386. 'order' => isset($this->$tableName->order) ? $this->$tableName->order : [$tableName . '.' . $this->$tableName->displayField() => 'ASC'],
  387. ];
  388. if ($type === 'list') {
  389. $defaults['fields'] = [$tableName . '.' . $this->$tableName->primaryKey(), $tableName . '.' . $this->$tableName->displayField()];
  390. }
  391. $options += $defaults;
  392. return $this->find($type, $options);
  393. }
  394. /**
  395. * Get all fields that have been used so far
  396. *
  397. * @param string $groupField Field to group by
  398. * @param string $type Find type
  399. * @param array $options
  400. * @return array
  401. */
  402. public function getFieldInUse($groupField, $type = 'all', $options = []) {
  403. $defaults = [
  404. 'group' => $groupField,
  405. 'order' => [$this->alias . '.' . $this->displayField => 'ASC'],
  406. ];
  407. if ($type === 'list') {
  408. $defaults['fields'] = [$this->alias . '.' . $this->primaryKey, $this->alias . '.' . $this->displayField];
  409. }
  410. $options += $defaults;
  411. return $this->find($type, $options);
  412. }
  413. /**
  414. * Checks if the content of 2 fields are equal
  415. * Does not check on empty fields! Return TRUE even if both are empty (secure against empty in another rule)!
  416. *
  417. * Options:
  418. * - compare: field to compare to
  419. * - cast: if casting should be applied to both values
  420. *
  421. * @param mixed $value
  422. * @param array $options
  423. * @return bool Success
  424. */
  425. public function validateIdentical($value, $options = [], array $context = []) {
  426. if (!is_array($options)) {
  427. $options = ['compare' => $options];
  428. }
  429. if (!isset($context['data'][$options['compare']])) {
  430. return false;
  431. }
  432. $compareValue = $context['data'][$options['compare']];
  433. $matching = ['string' => 'string', 'int' => 'integer', 'float' => 'float', 'bool' => 'boolean'];
  434. if (!empty($options['cast']) && array_key_exists($options['cast'], $matching)) {
  435. // cast values to string/int/float/bool if desired
  436. settype($compareValue, $matching[$options['cast']]);
  437. settype($value, $matching[$options['cast']]);
  438. }
  439. return ($compareValue === $value);
  440. }
  441. /**
  442. * Checks if a url is valid AND accessable (returns false otherwise)
  443. *
  444. * @param array/string $data: full url(!) starting with http://...
  445. * @options array
  446. * - allowEmpty TRUE/FALSE (TRUE: if empty => return TRUE)
  447. * - required TRUE/FALSE (TRUE: overrides allowEmpty)
  448. * - autoComplete (default: TRUE)
  449. * - deep (default: TRUE)
  450. * @return bool Success
  451. */
  452. public function validateUrl($url, $options = [], array $context = []) {
  453. if (empty($url)) {
  454. if (!empty($options['allowEmpty']) && empty($options['required'])) {
  455. return true;
  456. }
  457. return false;
  458. }
  459. if (!isset($options['autoComplete']) || $options['autoComplete'] !== false) {
  460. $url = $this->_autoCompleteUrl($url);
  461. if (isset($key)) {
  462. $this->data[$this->alias][$key] = $url;
  463. }
  464. }
  465. if (!isset($options['strict']) || $options['strict'] !== false) {
  466. $options['strict'] = true;
  467. }
  468. // validation
  469. if (!Validation::url($url, $options['strict']) && env('REMOTE_ADDR') && env('REMOTE_ADDR') !== '127.0.0.1') {
  470. return false;
  471. }
  472. // same domain?
  473. if (!empty($options['sameDomain']) && env('HTTP_HOST')) {
  474. $is = parse_url($url, PHP_URL_HOST);
  475. $expected = env('HTTP_HOST');
  476. if (mb_strtolower($is) !== mb_strtolower($expected)) {
  477. return false;
  478. }
  479. }
  480. if (isset($options['deep']) && $options['deep'] === false) {
  481. return true;
  482. }
  483. return $this->_validUrl($url);
  484. }
  485. /**
  486. * Prepend protocol if missing
  487. *
  488. * @param string $url
  489. * @return string Url
  490. */
  491. protected function _autoCompleteUrl($url) {
  492. if (mb_strpos($url, '/') === 0) {
  493. $url = Router::url($url, true);
  494. } elseif (mb_strpos($url, '://') === false && mb_strpos($url, 'www.') === 0) {
  495. $url = 'http://' . $url;
  496. }
  497. return $url;
  498. }
  499. /**
  500. * Checks if a url is valid
  501. *
  502. * @param string url
  503. * @return bool Success
  504. */
  505. protected function _validUrl($url) {
  506. $headers = Utility::getHeaderFromUrl($url);
  507. if ($headers === false) {
  508. return false;
  509. }
  510. $headers = implode("\n", $headers);
  511. $protocol = mb_strpos($url, 'https://') === 0 ? 'HTTP' : 'HTTP';
  512. if (!preg_match('#^' . $protocol . '/.*?\s+[(200|301|302)]+\s#i', $headers)) {
  513. return false;
  514. }
  515. if (preg_match('#^' . $protocol . '/.*?\s+[(404|999)]+\s#i', $headers)) {
  516. return false;
  517. }
  518. return true;
  519. }
  520. /**
  521. * Validation of DateTime Fields (both Date and Time together)
  522. *
  523. * @param options
  524. * - dateFormat (defaults to 'ymd')
  525. * - allowEmpty
  526. * - after/before (fieldName to validate against)
  527. * - min/max (defaults to >= 1 - at least 1 minute apart)
  528. * @return bool Success
  529. */
  530. public function validateDateTime($value, $options = [], array $context = []) {
  531. if (!$value) {
  532. if (!empty($options['allowEmpty'])) {
  533. return true;
  534. }
  535. return false;
  536. }
  537. $format = !empty($options['dateFormat']) ? $options['dateFormat'] : 'ymd';
  538. if (!is_object($value)) {
  539. $value = new Time($value);
  540. }
  541. $pieces = $value->format(FORMAT_DB_DATETIME);
  542. $dateTime = explode(' ', $pieces, 2);
  543. $date = $dateTime[0];
  544. $time = (!empty($dateTime[1]) ? $dateTime[1] : '');
  545. if (!empty($options['allowEmpty']) && (empty($date) && empty($time) || $date === DEFAULT_DATE && $time === DEFAULT_TIME || $date === DEFAULT_DATE && empty($time))) {
  546. return true;
  547. }
  548. //TODO: cleanup
  549. if (Validation::date($date, $format) && Validation::time($time)) {
  550. // after/before?
  551. $seconds = isset($options['min']) ? $options['min'] : 1;
  552. if (!empty($options['after']) && isset($context['data'][$options['after']])) {
  553. $compare = $value->subSeconds($seconds);
  554. if (!is_object($context['data'][$options['after']])) {
  555. $context['data'][$options['after']] = new Time($context['data'][$options['after']]);
  556. }
  557. if ($context['data'][$options['after']]->gt($compare)) {
  558. return false;
  559. }
  560. }
  561. if (!empty($options['before']) && isset($context['data'][$options['before']])) {
  562. $compare = $value->addSeconds($seconds);
  563. if (!is_object($context['data'][$options['before']])) {
  564. $context['data'][$options['before']] = new Time($context['data'][$options['before']]);
  565. }
  566. if ($context['data'][$options['before']]->lt($compare)) {
  567. return false;
  568. }
  569. }
  570. return true;
  571. }
  572. return false;
  573. }
  574. /**
  575. * Validation of Date fields (as the core one is buggy!!!)
  576. *
  577. * @param options
  578. * - dateFormat (defaults to 'ymd')
  579. * - allowEmpty
  580. * - after/before (fieldName to validate against)
  581. * - min (defaults to 0 - equal is OK too)
  582. * @return bool Success
  583. */
  584. public function validateDate($value, $options = [], array $context = []) {
  585. if (!$value) {
  586. if (!empty($options['allowEmpty'])) {
  587. return true;
  588. }
  589. return false;
  590. }
  591. $format = !empty($options['format']) ? $options['format'] : 'ymd';
  592. if (!is_object($value)) {
  593. $value = new Time($value);
  594. }
  595. $date = $value->format(FORMAT_DB_DATE);
  596. if (!empty($options['allowEmpty']) && (empty($date) || $date == DEFAULT_DATE)) {
  597. return true;
  598. }
  599. if (Validation::date($date, $format)) {
  600. // after/before?
  601. $days = !empty($options['min']) ? $options['min'] : 0;
  602. if (!empty($options['after']) && isset($context['data'][$options['after']])) {
  603. $compare = $value->subDays($days);
  604. if ($context['data'][$options['after']]->gt($compare)) {
  605. return false;
  606. }
  607. }
  608. if (!empty($options['before']) && isset($context['data'][$options['before']])) {
  609. $compare = $value->addDays($days);
  610. if ($context['data'][$options['before']]->lt($compare)) {
  611. return false;
  612. }
  613. }
  614. return true;
  615. }
  616. return false;
  617. }
  618. /**
  619. * Validation of Time fields
  620. *
  621. * @param array $options
  622. * - timeFormat (defaults to 'hms')
  623. * - allowEmpty
  624. * - after/before (fieldName to validate against)
  625. * - min/max (defaults to >= 1 - at least 1 minute apart)
  626. * @return bool Success
  627. */
  628. public function validateTime($value, $options = [], array $context = []) {
  629. if (!$value) {
  630. return false;
  631. }
  632. $dateTime = explode(' ', $value, 2);
  633. $value = array_pop($dateTime);
  634. if (Validation::time($value)) {
  635. // after/before?
  636. if (!empty($options['after']) && isset($context['data'][$options['after']])) {
  637. if ($context['data'][$options['after']] >= $value) {
  638. return false;
  639. }
  640. }
  641. if (!empty($options['before']) && isset($context['data'][$options['before']])) {
  642. if ($context['data'][$options['before']] <= $value) {
  643. return false;
  644. }
  645. }
  646. return true;
  647. }
  648. return false;
  649. }
  650. /**
  651. * Validation of Date Fields (>= minDate && <= maxDate)
  652. *
  653. * @param options
  654. * - min/max (TODO!!)
  655. */
  656. public function validateDateRange($value, $options = [], array $context = []) {
  657. }
  658. /**
  659. * Validation of Time Fields (>= minTime && <= maxTime)
  660. *
  661. * @param options
  662. * - min/max (TODO!!)
  663. */
  664. public function validateTimeRange($value, $options = [], array $context = []) {
  665. }
  666. }