PageRenderTime 42ms CodeModel.GetById 12ms RepoModel.GetById 0ms app.codeStats 1ms

/library/Stato/Webflow/Forms/Fields.php

https://github.com/goldoraf/stato
PHP | 503 lines | 485 code | 18 blank | 0 comment | 7 complexity | 16b374ff4b81d26ab91b747bba92ef6f MD5 | raw file
  1. <?php
  2. namespace Stato\Webflow\Forms;
  3. class ValidationError extends \Exception
  4. {
  5. protected $args;
  6. protected $cleanedValue;
  7. public function __construct($message, $args = array(), $cleanedValue = null)
  8. {
  9. parent::__construct($message);
  10. $this->args = $args;
  11. $this->cleanedValue = $cleanedValue;
  12. }
  13. public function getArgs()
  14. {
  15. return $this->args;
  16. }
  17. public function getCleanedValue()
  18. {
  19. return $this->cleanedValue;
  20. }
  21. }
  22. class Field
  23. {
  24. public $label;
  25. public $initial;
  26. public $helpText;
  27. protected $options;
  28. protected $required;
  29. protected $errorMessages;
  30. protected $inputAttrs;
  31. protected $name = null;
  32. protected $value = null;
  33. protected $input = null;
  34. protected $inputClass = 'TextInput';
  35. protected $defaultOptions = array();
  36. protected $defaultErrorMessages = array();
  37. protected $baseDefaultOptions = array(
  38. 'required' => false, 'label' => null, 'initial' => null,
  39. 'help_text' => null, 'error_messages' => array(), 'input_attrs' => array()
  40. );
  41. protected $baseDefaultErrorMessages = array(
  42. 'required' => 'This field is required.',
  43. 'invalid' => 'Enter a valid value.'
  44. );
  45. public function __construct(array $options = array())
  46. {
  47. $this->options = array_merge($this->baseDefaultOptions, $this->defaultOptions, $options);
  48. $this->errorMessages = array_merge($this->baseDefaultErrorMessages,
  49. $this->defaultErrorMessages, $this->options['error_messages']);
  50. list($this->required, $this->label, $this->initial, $this->helpText, $this->inputAttrs)
  51. = array($this->options['required'], $this->options['label'], $this->options['initial'],
  52. $this->options['help_text'], $this->options['input_attrs']);
  53. if (array_key_exists('input', $this->options)) {
  54. if (!$this->options['input'] instanceof Input)
  55. throw new \Exception(get_class($this->options['input']).' is not a subclass of Input');
  56. $this->input = $this->options['input'];
  57. }
  58. }
  59. public function __toString()
  60. {
  61. return $this->render($this->name, $this->value);
  62. }
  63. public function bind($name, $value)
  64. {
  65. $this->name = $name;
  66. $this->value = $value;
  67. return $this;
  68. }
  69. public function render($name, $value = null, $htmlAttrs = array())
  70. {
  71. $input = $this->getInput();
  72. $attrs = array_merge($this->getInputAttrs(), $this->inputAttrs, $htmlAttrs);
  73. if (!empty($attrs)) $input->addAttrs($attrs);
  74. return $input->render($name, $value);
  75. }
  76. public function clean($value)
  77. {
  78. if ($this->required && $this->isEmpty($value))
  79. throw new ValidationError($this->errorMessages['required']);
  80. if ($this->isEmpty($value)) return null;
  81. return $value;
  82. }
  83. public function getInput()
  84. {
  85. if (is_null($this->input)) {
  86. $inputClass = __NAMESPACE__.'\\'.$this->inputClass;
  87. $this->input = new $inputClass();
  88. }
  89. return $this->input;
  90. }
  91. protected function getInputAttrs()
  92. {
  93. return array();
  94. }
  95. protected function isEmpty($value)
  96. {
  97. return $value === '' || $value === null;
  98. }
  99. }
  100. class CharField extends Field
  101. {
  102. protected $regex;
  103. protected $length;
  104. protected $minLength;
  105. protected $maxLength;
  106. protected $defaultOptions = array(
  107. 'length' => null, 'min_length' => null, 'max_length' => null, 'regex' => null
  108. );
  109. protected $defaultErrorMessages = array(
  110. 'length' => 'Ensure this value has %d characters (it has %d).',
  111. 'min_length' => 'Ensure this value has at least %d characters (it has %d).',
  112. 'max_length' => 'Ensure this value has at most %d characters (it has %d).'
  113. );
  114. public function __construct(array $options = array())
  115. {
  116. parent::__construct($options);
  117. list($this->regex, $this->length, $this->minLength, $this->maxLength)
  118. = array($this->options['regex'], $this->options['length'],
  119. $this->options['min_length'], $this->options['max_length']);
  120. }
  121. public function clean($value)
  122. {
  123. $value = parent::clean($value);
  124. if ($this->isEmpty($value)) return '';
  125. $value = filter_var($value, FILTER_SANITIZE_STRING, FILTER_FLAG_NO_ENCODE_QUOTES);
  126. if (!is_null($this->regex) && !filter_var($value, FILTER_VALIDATE_REGEXP, array('options' => array('regexp' => $this->regex))))
  127. throw new ValidationError($this->errorMessages['invalid'], array(), $value);
  128. $length = mb_strlen($value);
  129. if (!is_null($this->length) && $length != $this->length)
  130. throw new ValidationError($this->errorMessages['length'], array($this->length, $length), $value);
  131. if (!is_null($this->minLength) && $length < $this->minLength)
  132. throw new ValidationError($this->errorMessages['min_length'], array($this->minLength, $length), $value);
  133. if (!is_null($this->maxLength) && $length > $this->maxLength)
  134. throw new ValidationError($this->errorMessages['max_length'], array($this->maxLength, $length), $value);
  135. return $value;
  136. }
  137. protected function getInputAttrs()
  138. {
  139. if (!is_null($this->maxLength))
  140. return array('maxlength' => $this->maxLength);
  141. return parent::getInputAttrs();
  142. }
  143. }
  144. class TextField extends CharField
  145. {
  146. protected $inputClass = 'Textarea';
  147. }
  148. class IntegerField extends Field
  149. {
  150. protected $minValue;
  151. protected $maxValue;
  152. protected $defaultOptions = array(
  153. 'min_value' => null, 'max_value' => null
  154. );
  155. protected $defaultErrorMessages = array(
  156. 'invalid' => 'Enter a whole number.',
  157. 'min_value' => 'Ensure this value is less than or equal to %s.',
  158. 'max_value' => 'Ensure this value is greater than or equal to %s.'
  159. );
  160. public function __construct(array $options = array())
  161. {
  162. parent::__construct($options);
  163. list($this->minValue, $this->maxValue)
  164. = array($this->options['min_value'], $this->options['max_value']);
  165. }
  166. public function clean($value)
  167. {
  168. $value = parent::clean($value);
  169. if ($this->isEmpty($value)) return null;
  170. $value = (int) filter_var((string) $value, FILTER_SANITIZE_NUMBER_INT);
  171. if (!is_null($this->minValue) && $value < $this->minValue)
  172. throw new ValidationError($this->errorMessages['min_value'], array($this->minValue), $value);
  173. if (!is_null($this->maxValue) && $value > $this->maxValue)
  174. throw new ValidationError($this->errorMessages['max_value'], array($this->maxValue), $value);
  175. return $value;
  176. }
  177. }
  178. class FloatField extends Field
  179. {
  180. protected $minValue;
  181. protected $maxValue;
  182. protected $defaultOptions = array(
  183. 'min_value' => null, 'max_value' => null
  184. );
  185. protected $defaultErrorMessages = array(
  186. 'invalid' => 'Enter a number.',
  187. 'min_value' => 'Ensure this value is less than or equal to %s.',
  188. 'max_value' => 'Ensure this value is greater than or equal to %s.'
  189. );
  190. public function __construct(array $options = array())
  191. {
  192. parent::__construct($options);
  193. list($this->minValue, $this->maxValue)
  194. = array($this->options['min_value'], $this->options['max_value']);
  195. }
  196. public function clean($value)
  197. {
  198. $value = parent::clean($value);
  199. if ($this->isEmpty($value)) return null;
  200. $value = (float) filter_var((string) $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_SCIENTIFIC);
  201. if (!is_null($this->minValue) && $value < $this->minValue)
  202. throw new ValidationError($this->errorMessages['min_value'], array($this->minValue), $value);
  203. if (!is_null($this->maxValue) && $value > $this->maxValue)
  204. throw new ValidationError($this->errorMessages['max_value'], array($this->maxValue), $value);
  205. return $value;
  206. }
  207. }
  208. /**
  209. * Validates that the input can be converted to a DateTime object.
  210. *
  211. * Consequently, accepted input formats are that of strtotime() PHP function,
  212. * and the returned cleaned value is a DateTime object.
  213. */
  214. class DateTimeField extends Field
  215. {
  216. protected $defaultErrorMessages = array(
  217. 'invalid' => 'Enter a valid date.'
  218. );
  219. public function clean($value)
  220. {
  221. $value = parent::clean($value);
  222. if ($this->isEmpty($value)) return null;
  223. if ($value instanceof \DateTime) return $value;
  224. try {
  225. $value = filter_var($value, FILTER_SANITIZE_STRING);
  226. // With PHP 5.3, we could use DateTime::createFromFormat()
  227. // It will open new opportunities ;)
  228. $value = new \DateTime($value);
  229. } catch (\Exception $e) {
  230. throw new ValidationError($this->errorMessages['invalid'], array(), $value);
  231. }
  232. return $value;
  233. }
  234. }
  235. class EmailField extends Field
  236. {
  237. protected $defaultErrorMessages = array(
  238. 'invalid' => 'Enter a valid e-mail address.'
  239. );
  240. public function clean($value)
  241. {
  242. $value = parent::clean($value);
  243. if ($this->isEmpty($value)) return null;
  244. $value = filter_var($value, FILTER_SANITIZE_EMAIL);
  245. if (!filter_var($value, FILTER_VALIDATE_EMAIL))
  246. throw new ValidationError($this->errorMessages['invalid'], array(), $value);
  247. return $value;
  248. }
  249. }
  250. class UrlField extends Field
  251. {
  252. protected $defaultErrorMessages = array(
  253. 'invalid' => 'Enter a valid URL.'
  254. );
  255. public function clean($value)
  256. {
  257. $value = parent::clean($value);
  258. if ($this->isEmpty($value)) return null;
  259. $value = filter_var($value, FILTER_SANITIZE_URL);
  260. if (!filter_var($value, FILTER_VALIDATE_URL))
  261. throw new ValidationError($this->errorMessages['invalid'], array(), $value);
  262. return $value;
  263. }
  264. }
  265. class IpField extends Field
  266. {
  267. protected $defaultErrorMessages = array(
  268. 'invalid' => 'Enter a valid IP.'
  269. );
  270. public function clean($value)
  271. {
  272. $value = parent::clean($value);
  273. if ($this->isEmpty($value)) return null;
  274. $value = filter_var($value, FILTER_SANITIZE_STRING);
  275. if (!filter_var($value, FILTER_VALIDATE_IP))
  276. throw new ValidationError($this->errorMessages['invalid'], array(), $value);
  277. return $value;
  278. }
  279. }
  280. class BooleanField extends Field
  281. {
  282. protected $checkedValue;
  283. protected $uncheckedValue;
  284. protected $inputClass = 'CheckboxInput';
  285. protected $defaultOptions = array(
  286. 'unchecked_value' => '0', 'checked_value' => '1'
  287. );
  288. public function __construct(array $options = array())
  289. {
  290. parent::__construct($options);
  291. list($this->checkedValue, $this->uncheckedValue)
  292. = array($this->options['checked_value'], $this->options['unchecked_value']);
  293. }
  294. public function clean($value)
  295. {
  296. $value = filter_var($value, FILTER_VALIDATE_BOOLEAN);
  297. if ($value !== true && $this->required)
  298. throw new ValidationError($this->errorMessages['required']);
  299. return $value;
  300. }
  301. public function render($name, $value = false, $htmlAttrs = array())
  302. {
  303. if ($this->inputClass == 'CheckboxInput' || $this->input instanceof CheckboxInput) {
  304. $checkbox = $this->getInput();
  305. $checkbox->addAttrs(array('checked' => (bool) $value));
  306. $hidden = new HiddenInput();
  307. return $hidden->render($name, $this->uncheckedValue) . $checkbox->render($name, $this->checkedValue, $htmlAttrs);
  308. }
  309. return parent::render($name, $value, $htmlAttrs);
  310. }
  311. }
  312. class ChoiceField extends Field
  313. {
  314. protected $choices;
  315. protected $inputClass = 'Select';
  316. protected $defaultOptions = array(
  317. 'choices' => array()
  318. );
  319. protected $defaultErrorMessages = array(
  320. 'invalid_choice' => 'Select a valid choice.'
  321. );
  322. public function __construct(array $options = array())
  323. {
  324. parent::__construct($options);
  325. $this->choices = $this->options['choices'];
  326. }
  327. public function clean($value)
  328. {
  329. $value = parent::clean($value);
  330. if ($this->isEmpty($value)) return '';
  331. if (!$this->isChoiceValid($value))
  332. throw new ValidationError($this->errorMessages['invalid_choice'], array($value));
  333. return $value;
  334. }
  335. public function getInput()
  336. {
  337. $input = parent::getInput();
  338. $input->setChoices($this->choices);
  339. return $input;
  340. }
  341. protected function isChoiceValid($choice)
  342. {
  343. $nonAssoc = (key($this->choices) === 0);
  344. foreach ($this->choices as $k => $v) {
  345. if (is_array($v)) {
  346. $nonAssoc2 = (key($v) === 0);
  347. foreach ($v as $k2 => $v2) {
  348. if ($nonAssoc2) $k2 = $v2;
  349. if ($choice == $k2) return true;
  350. }
  351. } else {
  352. if ($nonAssoc) $k = $v;
  353. if ($choice == $k) return true;
  354. }
  355. }
  356. return false;
  357. }
  358. }
  359. class MultipleChoiceField extends ChoiceField
  360. {
  361. protected $inputClass = 'MultipleSelect';
  362. protected $defaultErrorMessages = array(
  363. 'invalid_choice' => 'Select a valid choice.',
  364. 'invalid_list' => 'Enter a list of values.'
  365. );
  366. public function clean($value)
  367. {
  368. if ($this->required && $this->isEmpty($value))
  369. throw new ValidationError($this->errorMessages['required']);
  370. if ($this->isEmpty($value)) return array();
  371. if (!is_array($value))
  372. throw new ValidationError($this->errorMessages['invalid_list']);
  373. foreach ($value as $v) {
  374. if (!$this->isChoiceValid($v))
  375. throw new ValidationError($this->errorMessages['invalid_choice'], array($v));
  376. }
  377. return $value;
  378. }
  379. }
  380. class FileField extends Field
  381. {
  382. protected $inputClass = 'FileInput';
  383. protected $defaultErrorMessages = array(
  384. 'required' => 'A file is required',
  385. 'missing' => 'No file was submitted.',
  386. 'empty' => 'The submitted file is empty.',
  387. 'size' => 'The submitted file exceeds maximum file size.',
  388. 'unknown' => 'An error occured during file upload. Please try submitting the file again.'
  389. );
  390. public function clean($value)
  391. {
  392. if ($this->required && $this->isEmpty($value))
  393. throw new ValidationError($this->errorMessages['required']);
  394. if ($this->isEmpty($value) || !$value instanceof \Stato\Webflow\UploadedFile) return null;
  395. if (!$value->isSafe())
  396. throw new ValidationError($this->errorMessages['missing']);
  397. if (!$value->error) {
  398. if ($value->size === 0)
  399. throw new ValidationError($this->errorMessages['empty']);
  400. return $value;
  401. }
  402. switch ($value->error) {
  403. case \Stato\Webflow\UploadedFile::SIZE:
  404. $msg = $this->errorMessages['size'];
  405. break;
  406. case \Stato\Webflow\UploadedFile::NO_FILE:
  407. $msg = $this->errorMessages['missing'];
  408. break;
  409. default:
  410. $msg = $this->errorMessages['unknown'];
  411. }
  412. throw new ValidationError($msg);
  413. }
  414. }