first version
This commit is contained in:
467
kirby/src/Toolkit/V.php
Executable file
467
kirby/src/Toolkit/V.php
Executable file
@@ -0,0 +1,467 @@
|
||||
<?php
|
||||
|
||||
namespace Kirby\Toolkit;
|
||||
|
||||
use Exception;
|
||||
use Kirby\Image\Image;
|
||||
use Kirby\Toolkit\Str;
|
||||
use ReflectionFunction;
|
||||
|
||||
/**
|
||||
* A set of validator methods
|
||||
*
|
||||
* @package Kirby Toolkit
|
||||
* @author Bastian Allgeier <bastian@getkirby.com>
|
||||
* @link http://getkirby.com
|
||||
* @copyright Bastian Allgeier
|
||||
* @license MIT
|
||||
*/
|
||||
class V
|
||||
{
|
||||
|
||||
/**
|
||||
* An array with all installed validators
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $validators = [];
|
||||
|
||||
/**
|
||||
* Validates the given input with all passed rules
|
||||
* and returns an array with all error messages.
|
||||
* The array will be empty if the input is valid
|
||||
*
|
||||
* @param mixed $input
|
||||
* @param array $rules
|
||||
* @param array $messages
|
||||
* @return array
|
||||
*/
|
||||
public static function errors($input, array $rules, $messages = []): array
|
||||
{
|
||||
$errors = static::value($input, $rules, $messages, false);
|
||||
|
||||
return $errors === true ? [] : $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a useful error message for the given validator
|
||||
* and the arguments. This is used mainly internally
|
||||
* to create error messages
|
||||
*
|
||||
* @param string $validatorName
|
||||
* @param mixed ...$params
|
||||
* @return string|null
|
||||
*/
|
||||
public static function message(string $validatorName, ...$params): ?string
|
||||
{
|
||||
$validatorName = strtolower($validatorName);
|
||||
$translationKey = 'error.validation.' . $validatorName;
|
||||
$validators = array_change_key_case(static::$validators);
|
||||
$validator = $validators[$validatorName] ?? null;
|
||||
|
||||
if ($validator === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reflection = new ReflectionFunction($validator);
|
||||
$arguments = [];
|
||||
|
||||
|
||||
foreach ($reflection->getParameters() as $index => $parameter) {
|
||||
$value = $params[$index] ?? null;
|
||||
|
||||
if (is_array($value) === true) {
|
||||
$value = implode(', ', $value);
|
||||
}
|
||||
|
||||
$arguments[$parameter->getName()] = $value;
|
||||
}
|
||||
|
||||
return I18n::template($translationKey, 'The "' . $validatorName . '" validation failed', $arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the list of all validators
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function validators(): array
|
||||
{
|
||||
return static::$validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single value against
|
||||
* a set of rules, using all registered
|
||||
* validators
|
||||
*
|
||||
* @param mixed $value
|
||||
* @param array $rules
|
||||
* @param array $messages
|
||||
* @param boolean $fail
|
||||
* @return boolean|array
|
||||
*/
|
||||
public static function value($value, array $rules, array $messages = [], bool $fail = true)
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $validatorName => $validatorOptions) {
|
||||
if (is_int($validatorName)) {
|
||||
$validatorName = $validatorOptions;
|
||||
$validatorOptions = [];
|
||||
}
|
||||
|
||||
if (is_array($validatorOptions) === false) {
|
||||
$validatorOptions = [$validatorOptions];
|
||||
}
|
||||
|
||||
$validatorName = strtolower($validatorName);
|
||||
|
||||
if (static::$validatorName($value, ...$validatorOptions) === false) {
|
||||
$message = $messages[$validatorName] ?? static::message($validatorName, $value, ...$validatorOptions);
|
||||
$errors[$validatorName] = $message;
|
||||
|
||||
if ($fail === true) {
|
||||
throw new Exception($message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return empty($errors) === true ? true : $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an input array against
|
||||
* a set of rules, using all registered
|
||||
* validators
|
||||
*
|
||||
* @param array $input
|
||||
* @param array $rules
|
||||
* @return boolean
|
||||
*/
|
||||
public static function input(array $input, array $rules): bool
|
||||
{
|
||||
foreach ($rules as $fieldName => $fieldRules) {
|
||||
$fieldValue = $input[$fieldName] ?? null;
|
||||
|
||||
// first check for required fields
|
||||
if (($fieldRules['required'] ?? false) === true && $fieldValue === null) {
|
||||
throw new Exception(sprintf('The "%s" field is missing', $fieldName));
|
||||
}
|
||||
|
||||
// remove the required rule
|
||||
unset($fieldRules['required']);
|
||||
|
||||
// skip validation for empty fields
|
||||
if ($fieldValue === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
V::value($fieldValue, $fieldRules);
|
||||
} catch (Exception $e) {
|
||||
throw new Exception(sprintf($e->getMessage() . ' for field "%s"', $fieldName));
|
||||
}
|
||||
|
||||
static::value($fieldValue, $fieldRules);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls an installed validator and passes all arguments
|
||||
*
|
||||
* @param string $method
|
||||
* @param array $arguments
|
||||
* @return boolean
|
||||
*/
|
||||
public static function __callStatic(string $method, array $arguments): bool
|
||||
{
|
||||
$method = strtolower($method);
|
||||
$validators = array_change_key_case(static::$validators);
|
||||
|
||||
// check for missing validators
|
||||
if (isset($validators[$method]) === false) {
|
||||
throw new Exception('The validator does not exist: ' . $method);
|
||||
}
|
||||
|
||||
return call_user_func_array($validators[$method], $arguments);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default set of validators
|
||||
*/
|
||||
V::$validators = [
|
||||
/**
|
||||
* Valid: `'yes' | true | 1 | 'on'`
|
||||
*/
|
||||
'accepted' => function ($value): bool {
|
||||
return V::in($value, [1, true, 'yes', 'true', '1', 'on'], true) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid: `a-z | A-Z`
|
||||
*/
|
||||
'alpha' => function ($value): bool {
|
||||
return V::match($value, '/^([a-z])+$/i') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid: `a-z | A-Z | 0-9`
|
||||
*/
|
||||
'alphanum' => function ($value): bool {
|
||||
return V::match($value, '/^[a-z0-9]+$/i') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for numbers within the given range
|
||||
*/
|
||||
'between' => function ($value, $min, $max): bool {
|
||||
return V::min($value, $min) === true &&
|
||||
V::max($value, $max) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the given string contains the given value
|
||||
*/
|
||||
'contains' => function ($value, $needle): bool {
|
||||
return Str::contains($value, $needle);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid date
|
||||
*/
|
||||
'date' => function ($value): bool {
|
||||
$date = date_parse($value);
|
||||
return ($date !== false &&
|
||||
$date['error_count'] === 0 &&
|
||||
$date['warning_count'] === 0);
|
||||
},
|
||||
|
||||
/**
|
||||
* Valid: `'no' | false | 0 | 'off'`
|
||||
*/
|
||||
'denied' => function ($value): bool {
|
||||
return V::in($value, [0, false, 'no', 'false', '0', 'off'], true) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a value, which does not equal the given value
|
||||
*/
|
||||
'different' => function ($value, $other, $strict = false): bool {
|
||||
if ($strict === true) {
|
||||
return $value !== $other;
|
||||
}
|
||||
return $value != $other;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for valid email addresses
|
||||
*/
|
||||
'email' => function ($value): bool {
|
||||
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the given string ends with the given value
|
||||
*/
|
||||
'endsWith' => function (string $value, string $end): bool {
|
||||
return Str::endsWith($value, $end);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid filename
|
||||
*/
|
||||
'filename' => function ($value): bool {
|
||||
return V::match($value, '/^[a-z0-9@._-]+$/i') === true &&
|
||||
V::min($value, 2) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value exists in a list of given values
|
||||
*/
|
||||
'in' => function ($value, array $in, bool $strict = false): bool {
|
||||
return in_array($value, $in, $strict) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid integer
|
||||
*/
|
||||
'integer' => function ($value, bool $strict = false): bool {
|
||||
if ($strict === true) {
|
||||
return is_int($value) === true;
|
||||
}
|
||||
return filter_var($value, FILTER_VALIDATE_INT) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid IP address
|
||||
*/
|
||||
'ip' => function ($value): bool {
|
||||
return filter_var($value, FILTER_VALIDATE_IP) !== false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value is lower than the second value
|
||||
*/
|
||||
'less' => function ($value, float $max): bool {
|
||||
return V::size($value, $max, '<') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value matches the given regular expression
|
||||
*/
|
||||
'match' => function ($value, string $pattern): bool {
|
||||
return preg_match($pattern, $value) !== 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value does not exceed the maximum value
|
||||
*/
|
||||
'max' => function ($value, float $max): bool {
|
||||
return V::size($value, $max, '<=') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value is higher than the minimum value
|
||||
*/
|
||||
'min' => function ($value, float $min): bool {
|
||||
return V::size($value, $min, '>=') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of characters in the value equals or is below the given maximum
|
||||
*/
|
||||
'maxLength' => function (string $value = null, $max): bool {
|
||||
return Str::length(trim($value)) <= $max;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of characters in the value equals or is greater than the given minimum
|
||||
*/
|
||||
'minLength' => function (string $value = null, $min): bool {
|
||||
return Str::length(trim($value)) >= $min;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of words in the value equals or is below the given maximum
|
||||
*/
|
||||
'maxWords' => function (string $value = null, $max): bool {
|
||||
return V::max(explode(' ', trim($value)), $max) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the number of words in the value equals or is below the given maximum
|
||||
*/
|
||||
'minWords' => function (string $value = null, $min): bool {
|
||||
return V::min(explode(' ', trim($value)), $min) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the first value is higher than the second value
|
||||
*/
|
||||
'more' => function ($value, float $min): bool {
|
||||
return V::size($value, $min, '>') === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the given string does not contain the second value
|
||||
*/
|
||||
'notContains' => function ($value, $needle): bool {
|
||||
return V::contains($value, $needle) === false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the given value is not in the given list of values
|
||||
*/
|
||||
'notIn' => function ($value, $notIn): bool {
|
||||
return V::in($value, $notIn) === false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid number / numeric value (float, int, double)
|
||||
*/
|
||||
'num' => function ($value): bool {
|
||||
return is_numeric($value) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the value is present in the given array
|
||||
*/
|
||||
'required' => function ($key, array $array): bool {
|
||||
return isset($array[$key]) === true &&
|
||||
V::notIn($array[$key], [null, '', []]) === true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the first value equals the second value
|
||||
*/
|
||||
'same' => function ($value, $other, bool $strict = false): bool {
|
||||
if ($strict === true) {
|
||||
return $value === $other;
|
||||
}
|
||||
return $value == $other;
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the value has the given size
|
||||
*/
|
||||
'size' => function ($value, $size, $operator = '=='): bool {
|
||||
if (is_numeric($value) === true) {
|
||||
$count = $value;
|
||||
} elseif (is_string($value) === true) {
|
||||
$count = Str::length(trim($value));
|
||||
} elseif (is_array($value) === true) {
|
||||
$count = count($value);
|
||||
} elseif (is_object($value) === true) {
|
||||
if ($value instanceof \Countable) {
|
||||
$count = count($value);
|
||||
} elseif (method_exists($value, 'count') === true) {
|
||||
$count = $value->count();
|
||||
} else {
|
||||
throw new Exception('$value is an uncountable object');
|
||||
}
|
||||
} else {
|
||||
throw new Exception('$value is of type without size');
|
||||
}
|
||||
|
||||
switch ($operator) {
|
||||
case '<':
|
||||
return $count < $size;
|
||||
case '>':
|
||||
return $count > $size;
|
||||
case '<=':
|
||||
return $count <= $size;
|
||||
case '>=':
|
||||
return $count >= $size;
|
||||
default:
|
||||
return $count == $size;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks that the string starts with the given start value
|
||||
*/
|
||||
'startsWith' => function (string $value, string $start): bool {
|
||||
return Str::startsWith($value, $start);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for valid time
|
||||
*/
|
||||
'time' => function ($value): bool {
|
||||
return V::date($value);
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks for a valid Url
|
||||
*/
|
||||
'url' => function ($value): bool {
|
||||
// In search for the perfect regular expression: https://mathiasbynens.be/demo/url-regex
|
||||
$regex = '_^(?:(?:https?|ftp)://)(?:\S+(?::\S*)?@)?(?:(?!10(?:\.\d{1,3}){3})(?!127(?:\.\d{1,3}){3})(?!169\.254(?:\.\d{1,3}){2})(?!192\.168(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)(?:\.(?:[a-z\x{00a1}-\x{ffff}0-9]+-?)*[a-z\x{00a1}-\x{ffff}0-9]+)*(?:\.(?:[a-z\x{00a1}-\x{ffff}]{2,})))(?::\d{2,5})?(?:/[^\s]*)?$_iu';
|
||||
return preg_match($regex, $value) !== 0;
|
||||
}
|
||||
];
|
Reference in New Issue
Block a user