421 lines
12 KiB
PHP
421 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Libs;
|
|
|
|
use Exception;
|
|
|
|
/**
|
|
* Validator - DuckBrain
|
|
*
|
|
* Complementary library to the Request library.
|
|
* Simplifies value verification.
|
|
*
|
|
* It has the ability to verify both individual rules and in batches.
|
|
*
|
|
* |----------+--------------------------------------------------------|
|
|
* | Rule | Description |
|
|
* |----------+--------------------------------------------------------|
|
|
* | not | Negates the next rule. Ex: not:float |
|
|
* | exists | Is required; must be defined and can be empty |
|
|
* | required | Is required; must be defined and not empty |
|
|
* | number | Is numeric |
|
|
* | int | Is an integer |
|
|
* | float | Is a float |
|
|
* | bool | Is a boolean |
|
|
* | email | Is an email |
|
|
* | enum | Is in a list of values. Ex: enum:admin,user,guest |
|
|
* | url | Is a valid URL |
|
|
* |----------+--------------------------------------------------------|
|
|
*
|
|
* Rule lists are separated by |, e.g., required|email
|
|
*
|
|
* @author KJ
|
|
* @website https://kj2.me
|
|
* @license MIT
|
|
*/
|
|
class Validator
|
|
{
|
|
/**
|
|
* Stores the last failed rule.
|
|
*
|
|
* @var string
|
|
*/
|
|
public static string $lastFailed = '';
|
|
|
|
/**
|
|
* Validates a list of rules against the properties of an object.
|
|
*
|
|
* @param array $rulesList The list of rules.
|
|
* @param Neuron $haystack The object whose properties will be validated.
|
|
*
|
|
* @return bool Returns true only if all rules are met, and false as soon as one fails.
|
|
*/
|
|
public static function validateList(array $rulesList, Neuron $haystack): bool
|
|
{
|
|
foreach ($rulesList as $target => $rules) {
|
|
$rules = preg_split('/\|/', $rules);
|
|
foreach ($rules as $rule) {
|
|
if (static::checkRule($haystack->{$target}, $rule)) {
|
|
continue;
|
|
}
|
|
static::$lastFailed = $target . '.' . $rule;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Splits a single rule string into its name and its raw argument string.
|
|
*
|
|
* The rule name is everything before the first ':'. The remainder is
|
|
* returned verbatim (it is NOT split further here) so that rules whose
|
|
* arguments legitimately contain ':' or ',' - such as regex or mimes -
|
|
* are preserved intact. The caller decides how to split the arguments.
|
|
*
|
|
* @param string $rule The rule to parse. Ex: "regex:/^a,b$/" or "enum:a,b".
|
|
*
|
|
* @return array A two element array: [name, rawArguments]. When the rule
|
|
* has no parameters, rawArguments is an empty string.
|
|
*/
|
|
public static function parseRule(string $rule): array
|
|
{
|
|
$separator = strpos($rule, ':');
|
|
|
|
if ($separator === false) {
|
|
return [$rule, ''];
|
|
}
|
|
|
|
return [substr($rule, 0, $separator), substr($rule, $separator + 1)];
|
|
}
|
|
|
|
/**
|
|
* Checks if a rule is met.
|
|
*
|
|
* @param mixed $subject The value to verify.
|
|
* @param string $rule The rule to test.
|
|
*
|
|
* @return bool
|
|
* @throws Exception If the rule is not callable.
|
|
*/
|
|
public static function checkRule(mixed $subject, string $rule): bool
|
|
{
|
|
[$name, $rawArguments] = static::parseRule($rule);
|
|
|
|
$method = [static::class, $name];
|
|
$arguments = array_merge([$subject], static::splitArguments($name, $rawArguments));
|
|
|
|
if (is_callable($method)) {
|
|
return call_user_func_array($method, $arguments);
|
|
}
|
|
|
|
throw new Exception('Bad rule: "' . $name . '"');
|
|
}
|
|
|
|
/**
|
|
* Splits a rule's raw argument string into the arguments to pass to the
|
|
* rule's method (the subject is not included).
|
|
*
|
|
* Most rules take a comma-separated list of values (e.g. "enum:a,b").
|
|
* A few rules receive an argument that must be kept intact because it can
|
|
* legitimately contain commas or colons: "regex" (a pattern) and "not"
|
|
* (a sub-rule that is itself parsed recursively). An empty argument string
|
|
* yields no extra arguments.
|
|
*
|
|
* @param string $name The rule name.
|
|
* @param string $raw The raw argument string as returned by parseRule().
|
|
*
|
|
* @return array The list of arguments to pass after the subject.
|
|
*/
|
|
private static function splitArguments(string $name, string $raw): array
|
|
{
|
|
if ($raw === '') {
|
|
return [];
|
|
}
|
|
|
|
$intactRules = ['regex', 'not'];
|
|
|
|
if (in_array($name, $intactRules, true)) {
|
|
return [$raw];
|
|
}
|
|
|
|
return explode(',', $raw);
|
|
}
|
|
|
|
/**
|
|
* Verifies the rule in a negative way.
|
|
*
|
|
* @param mixed $subject The value to verify.
|
|
* @param mixed $rule The rule to test.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function not(mixed $subject, ...$rule): bool
|
|
{
|
|
return !static::checkRule($subject, join(':', $rule));
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is defined/exists.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function exists(mixed $subject): bool
|
|
{
|
|
return isset($subject);
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is defined and not empty.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function required(mixed $subject): bool
|
|
{
|
|
return isset($subject) && !empty($subject);
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is numeric.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function number(mixed $subject): bool
|
|
{
|
|
return is_numeric($subject);
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is an integer.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function int(mixed $subject): bool
|
|
{
|
|
return filter_var($subject, FILTER_VALIDATE_INT) !== false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is a float.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function float(mixed $subject): bool
|
|
{
|
|
return filter_var($subject, FILTER_VALIDATE_FLOAT) !== false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is a boolean.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function bool(mixed $subject): bool
|
|
{
|
|
return filter_var($subject, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE) !== null;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is a string.
|
|
*
|
|
* Note this is the strict type check: a PHP integer (e.g. 42) or an array
|
|
* fails, while any string - including a numeric string like "42" - passes.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function string(mixed $subject): bool
|
|
{
|
|
return is_string($subject);
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is an array.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function array(mixed $subject): bool
|
|
{
|
|
return is_array($subject);
|
|
}
|
|
|
|
/**
|
|
* Measures a value for the size-based rules (min/max/between/size).
|
|
*
|
|
* A numeric value is measured by its number, a plain string by its
|
|
* character length, and an array by its element count. This single helper
|
|
* keeps the size semantics consistent and is where the file case (kilobytes)
|
|
* is added later.
|
|
*
|
|
* @param mixed $subject The value to measure.
|
|
*
|
|
* @return float
|
|
*/
|
|
private static function measure(mixed $subject): float
|
|
{
|
|
if (is_numeric($subject)) {
|
|
return (float) $subject;
|
|
}
|
|
|
|
if (is_string($subject)) {
|
|
return (float) (function_exists('mb_strlen') ? mb_strlen($subject) : strlen($subject));
|
|
}
|
|
|
|
if (is_array($subject)) {
|
|
return (float) count($subject);
|
|
}
|
|
|
|
return 0.0;
|
|
}
|
|
|
|
/**
|
|
* Checks if the measured value is greater than or equal to a minimum.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
* @param mixed $min The minimum length, number or count.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function min(mixed $subject, mixed $min): bool
|
|
{
|
|
return static::measure($subject) >= (float) $min;
|
|
}
|
|
|
|
/**
|
|
* Checks if the measured value is less than or equal to a maximum.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
* @param mixed $max The maximum length, number or count.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function max(mixed $subject, mixed $max): bool
|
|
{
|
|
return static::measure($subject) <= (float) $max;
|
|
}
|
|
|
|
/**
|
|
* Checks if the measured value is within an inclusive range.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
* @param mixed $min The lower bound.
|
|
* @param mixed $max The upper bound.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function between(mixed $subject, mixed $min, mixed $max): bool
|
|
{
|
|
$size = static::measure($subject);
|
|
|
|
return $size >= (float) $min && $size <= (float) $max;
|
|
}
|
|
|
|
/**
|
|
* Checks if the measured value is exactly a given size.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
* @param mixed $size The expected length, number or count.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function size(mixed $subject, mixed $size): bool
|
|
{
|
|
return static::measure($subject) === (float) $size;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value matches a regular expression.
|
|
*
|
|
* The pattern is received verbatim (delimiters included), thanks to
|
|
* splitArguments() treating "regex" as an intact argument, so patterns
|
|
* that contain ':' or ',' - such as "/^a,b$/" - work as expected.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
* @param string $pattern A PCRE pattern including its delimiters.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function regex(mixed $subject, mixed $pattern): bool
|
|
{
|
|
return preg_match((string) $pattern, (string) $subject) === 1;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is a parseable date.
|
|
*
|
|
* Any format accepted by DateTime is valid (e.g. "2026-09-03", an ISO
|
|
* datetime, or a timestamp string). Empty and non-scalar values fail.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function date(mixed $subject): bool
|
|
{
|
|
if (!is_scalar($subject) || trim((string) $subject) === '') {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
new \DateTime((string) $subject);
|
|
|
|
return true;
|
|
} catch (Exception $e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is a valid email address.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function email(mixed $subject): bool
|
|
{
|
|
return filter_var($subject, FILTER_VALIDATE_EMAIL) !== false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is a valid URL.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function url(mixed $subject): bool
|
|
{
|
|
return filter_var($subject, FILTER_VALIDATE_URL) !== false;
|
|
}
|
|
|
|
/**
|
|
* Checks if the value is present in a list of allowed values.
|
|
*
|
|
* @param mixed $subject The value to check.
|
|
* @param mixed ...$values A variable number of allowed values.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public static function enum(mixed $subject, ...$values): bool
|
|
{
|
|
return in_array($subject, $values);
|
|
}
|
|
}
|