<?php
namespace Phad;
/**
*
*
* @todo write proper, simple unit tests for validations
* @todo integrate https://github.com/rlanvin/php-form, possibly, for better validation
*/
class FormValidator {
/**
* I don't think this does anything. I really don't know, though.
*/
public $throw_submission_error = false;
/**
* Set true to allow extra fields in the submission that are not defined in the form.
*/
public bool $allow_extra_fields = false;
public $failedSubmitColumns = [];
protected $props = [];
/**
* Set `FormValidator->validators['key'] = function($property_value, $property_settings, &$errors)`. Also set `<div validate="key">` to match `->validators['key']`. Default validation will be ignored in this case.
*
* @feature(Form Property Validation) Custom validation for properties on a form by specifying `validate='key'` in the html and `->validators['key'] = function(...){}` in php
*
* @deprecated in favor of $attribute_validators, because `$validators` causes default prop tests not to be run.
*/
public $validators = [];
/**
* User-added attribute validators.
*
* array<string attribute_name, callable $callable> key/value array of `attribute_name => $callable`s.
*/
protected array $attribute_validators = [];
/**
*
* @param $properties, like ['propName'=>['tagName'=>'textarea', 'attribute'=>'value'], 'prop2'=>[...]];
*/
public function __construct(array $properties=[]){
$this->props = $properties;
}
/**
* Add an html attribute validator. Can overwrite built-in attribute validators or user-added attribute validators.
*
* Define `some-attribute="some_spec"` on an html node and call `addAttributeValidator('some-attribute', $callable)`.
*
* @param $attribute_name a string attribute name, which you'll declare on HTML property nodes
* @param $callable A function/method/callable defined like `function(string $attribute_name, string $attribute_value, mixed $user_input_value)`. Does not actually have to be `Callable` type.
*
* @return true if this overwrites an existing user-defined attribute validator. False otherwise. Also returns false if it overwrites a built-in attribute validator.
*/
public function addAttributeValidator(string $attribute_name, mixed $callable): bool {
$was_set = isset($this->attribute_validators[$attribute_name]);
$this->attribute_validators[$attribute_name] = $callable;
return $was_set;
}
/**
*
* @param $data like `['key'=>'value', 'key2'=>value]`, as you'd get from `$_POST`
* @param &$failed_columns will be filled with an array of columns that failed to validate
* @return true/false whether there are errors or not
*/
public function validate(array $data, ?array &$errors=[], ?array &$failed_columns=[]): bool {
if ($errors==null)$errors = [];
if ($failed_columns==null)$failed_columns = [];
$remaining = $data;
$failed_columns = [];
foreach ($this->props as $propName=>$settings){
unset($remaining[$propName]);
$propValue = $data[$propName] ?? null;
// make `required` into a boolean value
if (!isset($settings['required']) || $settings['required'] == 'false'){
$settings['required'] = false;
} else $settings['required'] = true;
// skip validation if prop is empty & not required
if (($propValue===null||$propValue==='')&&$settings['required']===false){
continue;
}
// run a validator if one is set
if (isset($settings['validate'])){
$validator = $settings['validate'];
if (!isset($this->validators[$validator])){
throw new \Exception("Validator '$validator' does not exist.");
}
$error_count = count($errors);
$didPass = $this->validators[$validator]($propValue, $settings, $errors);
if (!$didPass){
$failed_columns[$propName]['failed_value'] = $propValue;
$failed_columns[$propName]['validator'] = $validator;
if (count($errors)==$error_count){
$errors[] = "'$propName' failed validating with '$validator'";
}
}
} else {
// run default validation
foreach ($settings as $name=>$spec){
$parts = explode('-', $name);
$uc_parts = array_map('ucfirst', $parts);
$method_name = implode('',$uc_parts);
$method = 'validateProp'.$method_name;
//$method = 'validateProp'.ucfirst($name);
$out_value = $propValue;
$doesPass = false;
// @TODO if the validator doesn't exist, we should probably throw an exception, not blindly approve the value.
if (isset($this->attribute_validators[$name])){
$callable = $this->attribute_validators[$name];
$doesPass = $callable($name, $spec, $out_value);
}
else if (method_exists($this,$method))$doesPass = $this->$method($spec, $out_value);
else $doesPass = true;
//$doesPass = !method_exists($this,$method) || $this->$method($spec, $out_value);
if (!$doesPass){
if (is_bool($spec))$spec = $spec ? 'true' : 'false';
if (is_array($spec))$spec = print_r($spec,true);
$errors[] = ['msg'=>"'$propName' failed validation for '$name:$spec'"];
$failed_columns[$propName]['failed_value'] = $propValue;
$failed_columns[$propName][$name] = $spec;
}
// else {
// $data[$propName] = $out_value;
// }
}
}
}
// var_dump($errors);
if (count($errors)>0){
return false;
} else if (($c=count($failed_columns))>0){
$errors[] = ['msg'=>"$c fields failed validation. Cause unkown."];
return false;
}
if (!$this->allow_extra_fields&&($c=count($remaining))>0){
$fields_str = implode(', ', array_keys($remaining));
$errors[] = ['msg'=>"$c unexpected fields were submitted. They were: ".$fields_str];
return false;
}
return true;
}
public function validatePropTagname($tagName, $propValue){
if ($tagName=='input')return true;
if ($tagName=='textarea'&&is_string($propValue))return true;
if ($tagName=='select')return true;
return false;
}
public function validatePropRequired(bool $isRequired, $propValue){
if (!$isRequired)return true;
if ($propValue!==null&&strlen($propValue)>0)return true;
return false;
}
public function validatePropType(string $type, $value){
if ($type=='text'&&is_string($value))return true;
else if ($type=="number"&&is_numeric($value))return true;
else if ($type=="backend")return true;
else if ($type=='hidden')return true;
else if ($type=='date'||$type=='datetime-local'){
if (trim($value??'')=='')return true;
// var_dump($value);
// exit;
// if
$date = date_create($value);
if ($date===false)return false;
return true;
} else if ($type=='time'){
$split = explode(':',$value);
$hour = (int)$split[0];
$minute = (int)$split[1];
// var_dump($split);
// var_dump($hour);
// var_dump($minute);
// var_dump((int)'09');
// exit;
if ( !(is_numeric($split[0])&&((int)$split[0])<24) )return false;
if ( !(is_numeric($split[1])&&((int)$split[1])<60) )return false;
return true;
} else if ($type=='url'){
return filter_var($value,FILTER_VALIDATE_URL);
} else if ($type=='phone'){
// $value = preg_replace('/[^0-9]/','', $value);
if ((
strlen($value)==10
||strlen($value)==11&&$value[0]=='1'
)
&&is_numeric($value)
){
return true;
}
return false;
} else if ($type=='email'){
return filter_var($value, FILTER_VALIDATE_EMAIL);
} else if ($type=='checkbox'){
if ($value=='on'||$value=='1'){
// $value = 1;
return true;
}// else $value = 0;
} else if ($type=='radio'){
return true;
}
return false;
}
public function validatePropMaxlength(int $maxLen, $propValue){
if (strlen($propValue??'')<=$maxLen)return true;
return false;
}
public function validatePropMinLength(int $minLen, $propValue){
if (strlen($propValue??'')>=$minLen)return true;
return false;
}
public function validatePropOptions(array $options, $value){
return in_array($value, $options);
}
/**
* Validate the php data type. Declare `p-type="array"` or another type.
*
* @param $type can be array, string, number, or int (@TODO others)
* @param $value the value to validate
*
* @return if the value is the given type. For type-checks not yet implemented, return false
*/
public function validatePropPType(string $type, mixed $value): bool {
switch ($type){
case "array":
return is_array($value);
case "string":
return is_string($value);
case "number":
return is_numeric($value);
case "int":
// form submissions are always strings (right?), so we can't use `is_int()`
$intval = (int)$value;
$intstring = (string)$intval;
//var_dump($value);
//var_dump($intval);
//var_dump($intstring);
if ($intstring===$value)return true;
return false;
}
return false;
}
/**
* Validate that user input does not contain any html
*
* @param $attribute_value "false" to disable this validation test. All other values are ignored and enable the test.
* @param $user_input_value the value submitted by the user.
*
* @return if the user input value is identical to `strip_tags($user_input_value)`, return true. Otherwise false.
*/
public function validatePropPNohtml(string $attribute_value, mixed $user_input_value){
if ($attribute_value==='false')return true;
if (!is_string($user_input_value) || $user_input_value === strip_tags($user_input_value)) return true;
return false;
}
/**
* Validate that user input is numerically greater than the target value. User value MUST be numeric (uses p-type=number validation)
*
* @param $attribute_value "false" to disable this validation test. All other values are ignored and enable the test.
* @param $user_input_value the value submitted by the user.
*
* @return if the user input value is identical to `strip_tags($user_input_value)`, return true. Otherwise false.
*/
public function validatePropPGreaterthan(string $target_value, mixed $user_input_value){
if (!$this->validatePropPType('number', $user_input_value))return false;
if ($user_input_value > $target_value) return true;
return false;
}
}