SproutCMS

This is the code documentation for the SproutCMS project

source of /sprout/Helpers/Validator.php

  1. <?php
  2. /*
  3.  * Copyright (C) 2017 Karmabunny Pty Ltd.
  4.  *
  5.  * This file is a part of SproutCMS.
  6.  *
  7.  * SproutCMS is free software: you can redistribute it and/or modify it under the terms
  8.  * of the GNU General Public License as published by the Free Software Foundation, either
  9.  * version 2 of the License, or (at your option) any later version.
  10.  *
  11.  * For more information, visit <http://getsproutcms.com>.
  12.  */
  13. namespace Sprout\Helpers;
  14.  
  15. use InvalidArgumentException;
  16.  
  17. use Sprout\Exceptions\ValidationException;
  18.  
  19.  
  20. /**
  21.  * New validation class for Sprout 3.
  22.  * Used with the {@see Validity} class.
  23.  *
  24.  * @example
  25.  * // Plain example
  26.  *
  27.  * $valid = new Validator($_POST);
  28.  *
  29.  * $valid->required(['name', 'email']);
  30.  *
  31.  * $valid->check('name', 'Validity::length', 1, 100);
  32.  * $valid->check('email', 'Validity::email');
  33.  *
  34.  * if ($valid->hasErrors()) {
  35.  * $_SESSION['register']['field_errors'] = $valid->getFieldErrors();
  36.  * $valid->createNotifications();
  37.  * Url::redirect('user/register');
  38.  * }
  39.  * @example
  40.  * // Multiedit example for a course with students
  41.  *
  42.  * $has_error = false;
  43.  *
  44.  * $valid = new Validator($_POST);
  45.  * $valid->required(['name']);
  46.  * $valid->check('name', 'Validity::length', 1, 100);
  47.  *
  48.  * if ($valid->hasErrors()) {
  49.  * $_SESSION['course_edit']['field_errors'] = $valid->getFieldErrors();
  50.  * $valid->createNotifications();
  51.  * $has_error = true;
  52.  * }
  53.  *
  54.  * if (empty($_POST['multiedit_students'])) {
  55.  * $_POST['multiedit_students'] = [];
  56.  * }
  57.  *
  58.  * $record_num = 0;
  59.  * foreach ($_POST['multiedit_students'] as $idx => $data) {
  60.  * if (MultiEdit::recordEmpty($data)) continue;
  61.  *
  62.  * ++$record_num;
  63.  *
  64.  * $multi_valid = new Validator($data);
  65.  * $multi_valid->setLabels([
  66.  * 'name' => 'Name for student ' . $record_num,
  67.  * 'email' => 'Email address for student ' . $record_num,
  68.  * ]);
  69.  *
  70.  * $multi_valid->required(['name', 'email']);
  71.  * $multi_valid->check('name', 'Validity::length', 1, 100);
  72.  * $multi_valid->check('email', 'Validity::email');
  73.  *
  74.  * if ($multi_valid->hasErrors()) {
  75.  * $_SESSION['course_edit']['field_errors']['multiedit_students'][$idx] = $multi_valid->getFieldErrors();
  76.  * $multi_valid->createNotifications();
  77.  * $has_error = true;
  78.  * }
  79.  * }
  80.  *
  81.  * if ($has_error) {
  82.  * Url::redirect('course/edit');
  83.  * }
  84.  */
  85. class Validator
  86. {
  87. protected $labels;
  88. protected $data;
  89. protected $field_errors;
  90. protected $general_errors;
  91.  
  92.  
  93. /**
  94.   * Recursive trim data
  95.   *
  96.   * Alters in-place AND returns the array
  97.   * This allows for use such as:
  98.   *
  99.   * $_SESSION['register']['field_values'] = Validator::trim($_POST);
  100.   *
  101.   * When used like this, the session gets set and the POST data is also trimmed,
  102.   * so can be used directly for database inserts.
  103.   *
  104.   * @param array $data Data to trim. Passed by-reference.
  105.   * @return array Trimmed data
  106.   */
  107. public static function trim(array &$data)
  108. {
  109. foreach ($data as $key => $val) {
  110. if (is_string($val)) {
  111. $data[$key] = trim($val);
  112. } elseif (is_array($val)) {
  113. self::trim($val);
  114. }
  115. }
  116.  
  117. return $data;
  118. }
  119.  
  120.  
  121. /**
  122.   * @param array $data Data to validate
  123.   */
  124. public function __construct(array $data)
  125. {
  126. $this->labels = null;
  127. $this->data = $data;
  128. $this->field_errors = array();
  129. $this->general_errors = array();
  130. }
  131.  
  132.  
  133. /**
  134.   * Field labels make error messages a little friendlier
  135.   *
  136.   * @param array $labels Field labels
  137.   */
  138. public function setLabels(array $labels)
  139. {
  140. $this->labels = $labels;
  141. }
  142.  
  143.  
  144. /**
  145.   * Update the data to validate
  146.   *
  147.   * @param array $data Data to validate
  148.   */
  149. public function setData(array $data)
  150. {
  151. $this->data = $data;
  152. }
  153.  
  154.  
  155. /**
  156.   * Set the value for a single data field
  157.   *
  158.   * @param string $field The field to set
  159.   * @param mixed $value The value to set on the field
  160.   */
  161. public function setFieldValue($field, $value)
  162. {
  163. $this->data[$field] = $value;
  164. }
  165.  
  166.  
  167. /**
  168.   * Set the label for a single field
  169.   *
  170.   * @param string $field The field to set
  171.   * @param string $label The label to set on the field
  172.   */
  173. public function setFieldLabel($field, $label)
  174. {
  175. $this->labels[$field] = $label;
  176. }
  177.  
  178.  
  179. /**
  180.   * For a given function, expand the namespace for Sprout helpers
  181.   *
  182.   * @param callable|string $func The function to expand with the Sprout\Helpers namespace.
  183.   * Strings representing function names are affected; closures are not.
  184.   * @return callable $func
  185.   */
  186. protected function expandNs($func)
  187. {
  188. if (is_string($func) and strpos($func, '::') !== false) {
  189. list($class, $func) = explode('::', $func);
  190. $class = Sprout::nsClass($class, ['Sprout\Helpers']);
  191. $func = $class . '::' . $func;
  192. }
  193. return $func;
  194. }
  195.  
  196.  
  197. /**
  198.   * Check the value of a field against a validation method, storing any error messages received
  199.   * Additional arguments are passed to the underlying method
  200.   *
  201.   * Methods which are on classes within the Sprout\Helpers namespace do not need the namespace
  202.   * specified on the function name
  203.   *
  204.   * If a field has already been checked with {@see Validator::required} and the field was empty,
  205.   * this function will not report errors (but will still return an appropriate value)
  206.   *
  207.   * If a empty value is provided, it is not validated - returns true
  208.   *
  209.   * @param string $field_name The field to check
  210.   * @param callable $func The function or method to call.
  211.   * @return bool True if validation was successful, false if it failed
  212.   */
  213. public function check($field_name, $func)
  214. {
  215. if (!isset($this->data[$field_name]) or self::isEmpty($this->data[$field_name])) {
  216. return true;
  217. }
  218.  
  219. $func = self::expandNs($func);
  220.  
  221. $args = func_get_args();
  222. array_shift($args);
  223. array_shift($args);
  224. array_unshift($args, $this->data[$field_name]);
  225.  
  226. try {
  227. call_user_func_array($func, $args);
  228. return true;
  229.  
  230. } catch (ValidationException $ex) {
  231. $this->addFieldError($field_name, $ex->getMessage());
  232. return false;
  233. }
  234. }
  235.  
  236.  
  237. /**
  238.   * Run a validation check against each value in an array.
  239.   * Behaviour is very similar to the {@see Validator::check} method.
  240.   *
  241.   * Only supports single-depth arrays
  242.   *
  243.   * Errors are put into the field_errors array under a subkey matching the array key
  244.   *
  245.   * Return value is an array of key => boolean with the validation result for each key
  246.   *
  247.   * @example
  248.   * $data = ['vals' => [1, 2, 'A', 'B', 5]];
  249.   * $validator = new Validator($data);
  250.   * $result = $validator->arrayCheck('vals', 'Validity::positiveInt');
  251.   * // $result now contains [true, true, false, false, true]
  252.   * $errs = $validator->getFieldErrors();
  253.   * // $errs now contains [ 'vals' => [2 => [...], 3 => [...]] ]
  254.   *
  255.   * @param string $field_name The field to check
  256.   * @param callable $func The function or method to call.
  257.   * @return array Key => Boolean True if validation was successful, false if it failed
  258.   */
  259. public function arrayCheck($field_name, $func)
  260. {
  261. if (!isset($this->data[$field_name]) or self::isEmpty($this->data[$field_name])) {
  262. return true;
  263. }
  264. if (!is_array($this->data[$field_name])) {
  265. throw new InvalidArgumentException("Field <{$field_name}> is not an array");
  266. }
  267.  
  268. $func = self::expandNs($func);
  269.  
  270. $args = func_get_args();
  271. array_shift($args);
  272. array_unshift($args, $this->data[$field_name]);
  273.  
  274. $results = [];
  275. foreach ($this->data[$field_name] as $index => $value) {
  276. $args[0] = $value;
  277.  
  278. try {
  279. call_user_func_array($func, $args);
  280. $results[$index] = true;
  281.  
  282. } catch (ValidationException $ex) {
  283. $this->addArrayFieldError($field_name, $index, $ex->getMessage());
  284. $results[$index] = false;
  285. }
  286. }
  287.  
  288. return $results;
  289. }
  290.  
  291.  
  292. /**
  293.   * Check multiple fields against a validation method
  294.   *
  295.   * This is similar to {@see Validator::check} but it's designed for different validation
  296.   * methods, which work on a set of fields instead of a single field (e.g. Validity::oneRequired)
  297.   *
  298.   * Additional arguments are passed to the underlying method
  299.   * Methods which are on classes within the Sprout\Helpers namespace do not need the namespace
  300.   * specified on the function name
  301.   *
  302.   * @param array $fields The fields to check
  303.   * @param callable $func The function or method to call.
  304.   * @return bool True if validation was successful, false if it failed
  305.   */
  306. public function multipleCheck(array $fields, $func)
  307. {
  308. $func = self::expandNs($func);
  309.  
  310. $vals = array();
  311. foreach ($fields as $field_name) {
  312. $vals[] = @$this->data[$field_name];
  313. }
  314.  
  315. $args = func_get_args();
  316. array_shift($args);
  317. array_shift($args);
  318. array_unshift($args, $vals);
  319.  
  320. try {
  321. call_user_func_array($func, $args);
  322. return true;
  323.  
  324. } catch (ValidationException $ex) {
  325. $this->addMultipleFieldError($fields, $ex->getMessage());
  326. return false;
  327. }
  328. }
  329.  
  330.  
  331. /**
  332.   * Sadly, the PHP builtin empty() considers '0' to be empty, but it actually isn't
  333.   *
  334.   * @param mixed $val
  335.   * @return bool True if empty, false if not.
  336.   */
  337. public static function isEmpty($val)
  338. {
  339. if (is_array($val) and count($val) == 0) {
  340. return true;
  341. } else if ($val == '') {
  342. return true;
  343. }
  344. return false;
  345. }
  346.  
  347.  
  348. /**
  349.   * Checks various fields are required
  350.   * If a field is required and no value is provided, no other validation will be proessed for that field.
  351.   *
  352.   * @param array $fields Fields to check
  353.   */
  354. public function required(array $fields)
  355. {
  356. foreach ($fields as $field_name) {
  357. if (!isset($this->data[$field_name])) {
  358. $this->field_errors[$field_name] = ['required' => 'This field is required'];
  359. } elseif (self::isEmpty($this->data[$field_name])) {
  360. $this->field_errors[$field_name] = ['required' => 'This field is required'];
  361. }
  362. }
  363. }
  364.  
  365.  
  366. /**
  367.   * Add an error message for a given field to the field errors list
  368.   *
  369.   * @param string $field_name The field to add the error message for
  370.   * @param string $message The message text
  371.   */
  372. public function addFieldError($field_name, $message)
  373. {
  374. if (!isset($this->field_errors[$field_name])) {
  375. $this->field_errors[$field_name] = [$message];
  376. } else {
  377. $this->field_errors[$field_name][] = $message;
  378. }
  379. }
  380.  
  381.  
  382. /**
  383.   * Add an error message for a given field to the field errors list
  384.   * This variation is for array validation, e.g. an array of integers
  385.   *
  386.   * @param string $field_name The field to add the error message for
  387.   * @param int $index The array index of the field to report error for
  388.   * @param string $message The message text
  389.   */
  390. public function addArrayFieldError($field_name, $index, $message)
  391. {
  392. if (!isset($this->field_errors[$field_name])) {
  393. $this->field_errors[$field_name] = [];
  394. }
  395. if (!isset($this->field_errors[$field_name][$index])) {
  396. $this->field_errors[$field_name][$index] = [$message];
  397. } else {
  398. $this->field_errors[$field_name][$index][] = $message;
  399. }
  400. }
  401.  
  402.  
  403. /**
  404.   * Add an error message from a multiple-field validation (e.g. checking at least one is set)
  405.   *
  406.   * @param array $fields The fields to add the error message for
  407.   * @param string $message The message text
  408.   */
  409. public function addMultipleFieldError(array $fields, $message)
  410. {
  411. foreach ($fields as $f) {
  412. $this->addFieldError($f, $message);
  413. }
  414. }
  415.  
  416.  
  417. /**
  418.   * Get an array of all field errors, indexed by field name
  419.   * Fields may have multiple errors defined
  420.   *
  421.   * @return array
  422.   */
  423. public function getFieldErrors()
  424. {
  425. return $this->field_errors;
  426. }
  427.  
  428.  
  429. /**
  430.   * Add a general error message, e.g. for errors affecting many fields
  431.   *
  432.   * @param string $message The message text
  433.   */
  434. public function addGeneralError($message)
  435. {
  436. $this->general_errors[] = $message;
  437. }
  438.  
  439.  
  440. /**
  441.   * Get an array of all general errors
  442.   *
  443.   * @return array
  444.   */
  445. public function getGeneralErrors()
  446. {
  447. return $this->general_errors;
  448. }
  449.  
  450.  
  451. /**
  452.   * @return bool True if there were any validation errors, false if there wasn't
  453.   */
  454. public function hasErrors()
  455. {
  456. if (count($this->field_errors)) return true;
  457. if (count($this->general_errors)) return true;
  458. return false;
  459. }
  460.  
  461.  
  462. /**
  463.   * Create notification error messages for each error
  464.   *
  465.   * @param string $scope Set the scope for the notifications
  466.   */
  467. public function createNotifications($scope = 'default')
  468. {
  469. foreach ($this->general_errors as $msg) {
  470. Notification::error($msg, 'plain', $scope);
  471. }
  472.  
  473. foreach ($this->field_errors as $field => $msg) {
  474. if (isset($this->labels[$field])) {
  475. Notification::error($this->labels[$field] . ' -- ' . implode(', ', $msg), 'plain', $scope);
  476. } else {
  477. $label = ucfirst(str_replace('_', ' ', $field));
  478. Notification::error($label . ' -- ' . implode(', ', $msg), 'plain', $scope);
  479. }
  480. }
  481. }
  482.  
  483. }
  484.