<?php
declare(strict_types=1);
namespace App\Services;
use BadMethodCallException;
use Countable;
use Psr\Http\Message\UploadedFileInterface;
/**
* Class Validation data
*
* @license Code and contributions have MIT License
* @link https://visavi.net
* @author Alexander Grigorev <[email protected]>
*
* @method $this required(array|string $key, ?string $label = null)
* @method $this length(array|string $key, int $min, int $max, ?string $label = null)
* @method $this minLength(array|string $key, int $length, ?string $label = null)
* @method $this maxLength(array|string $key, int $length, ?string $label = null)
* @method $this range(array|string $key, int|float $min, int|float $max, ?string $label = null)
* @method $this gt(array|string $key, int|float $num, ?string $label = null)
* @method $this gte(array|string $key, int|float $num, ?string $label = null)
* @method $this lt(array|string $key, int|float $num, ?string $label = null)
* @method $this lte(array|string $key, int|float $num, ?string $label = null)
* @method $this same(string $key, mixed $value, string $label = null)
* @method $this notSame(string $key, mixed $value, string $label = null)
* @method $this equal(string $key1, string $key2, ?string $label = null)
* @method $this notEqual(string $key1, string $key2, ?string $label = null)
* @method $this empty(array|string $key, ?string $label = null)
* @method $this notEmpty(array|string $key, ?string $label = null)
* @method $this in(array|string $key, array $haystack, ?string $label = null)
* @method $this notIn(array|string $key, array $haystack, ?string $label = null)
* @method $this regex(array|string $key, string $pattern, ?string $label = null)
* @method $this url(array|string $key, ?string $label = null)
* @method $this email(array|string $key, ?string $label = null)
* @method $this ip(array|string $key, ?string $label = null)
* @method $this phone(array|string $key, ?string $label = null)
* @method $this boolean(array|string $key, ?string $label = null)
* @method $this file(string $key, array $rules)
* @method $this add(string $key, callable $callable, string $label)
* @method $this custom(bool $compare, array|string $label)
*
*/
class Validator
{
private array $rules;
private array $input;
private array $errors = [];
private array $data = [
'required' => 'Поле %s является обязательным',
'length' => [
'between' => 'Количество символов в поле %s должно быть от %d до %d',
'min' => 'Количество символов в поле %s должно быть не меньше %d',
'max' => 'Количество символов в поле %s должно быть не больше %d',
],
'range' => 'Значение поля %s должно быть между %d и %d',
'gt' => 'Значение поля %s должно быть больше %d',
'gte' => 'Значение поля %s должно быть больше или равно %d',
'lt' => 'Значение поля %s должно быть меньше %d',
'lte' => 'Значение поля %s должно быть меньше или равно %d',
'equal' => 'Значения полей %s и %s должны совпадать',
'notEqual' => 'Значения полей %s и %s должны различаться',
'same' => 'Значения поля %s должно быть равным %s',
'notSame' => 'Значения поля %s должно быть не равным %s',
'empty' => 'Значение поля %s должно быть пустым',
'notEmpty' => 'Значение поля %s не должно быть пустым',
'in' => 'Значение поля %s ошибочно',
'notIn' => 'Значение поля %s ошибочно',
'regex' => 'Значение поля %s имеет ошибочный формат',
'url' => 'Значение поля %s содержит недействительный URL',
'email' => 'Значение поля %s содержит недействительный email',
'ip' => 'Значение поля %s содержит недействительный IP-адрес',
'phone' => 'Значение поля %s содержит недействительный номер телефона',
'file' => [
'error' => 'Ошибка загрузки файла',
'extension' => 'Поле %s должно быть файлом одного из следующих типов: %s',
'size_max' => 'Размер файла в поле %s должен быть не больше %s',
'weight_min' => 'Размер изображения в поле %s не должен быть меньше %s px',
'weight_max' => 'Размер изображения в поле %s не должен быть больше %s px',
'weight_empty' => 'Размер изображения в поле %s слишком маленький!',
],
'boolean' => 'Поле %s должно быть логического типа',
];
private array $images = ['jpg', 'jpeg', 'gif', 'png', 'bmp', 'webp'];
/**
* Call
*
* @param string $name
* @param array $arguments
*
* @return $this
*/
public function __call(string $name, array $arguments): self
{
$keys = (array) $arguments[0];
foreach ($keys as $key) {
$arguments[0] = $key;
$this->rules[$key][$name] = $arguments;
}
return $this;
}
/**
* Возвращает успешность валидации
*
* @param array $input
*
* @return bool
*/
public function isValid(array $input): bool
{
$this->input = $input;
foreach ($this->rules as $rules) {
foreach ($rules as $rule => $params) {
$method = $rule . 'Rule';
if (! method_exists($this, $method)) {
throw new BadMethodCallException(sprintf('%s() called undefined method. Method "%s" does not exist', __METHOD__, $rule));
}
$this->$method(...$params);
}
}
return empty($this->errors);
}
/**
* Required
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function requiredRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if ($this->blank($input)) {
$this->addError($field, sprintf($label ?? $this->data['required'], $field));
}
}
return $this;
}
/**
* Length
*
* @param array|string $key
* @param int $min
* @param int $max
* @param string|null $label
*
* @return $this
*/
private function lengthRule(array|string $key, int $min, int $max, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (mb_strlen((string) $input, 'UTF-8') < $min || mb_strlen((string) $input, 'UTF-8') > $max) {
$this->addError($field, sprintf($label ?? $this->data['length']['between'], $field, $min, $max));
}
}
return $this;
}
/**
* Min length
*
* @param array|string $key
* @param int $length
* @param string|null $label
*
* @return $this
*/
private function minLengthRule(array|string $key, int $length, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (mb_strlen((string) $input, 'UTF-8') < $length) {
$this->addError($field, sprintf($label ?? $this->data['length']['min'], $field, $length));
}
}
return $this;
}
/**
* Max length
*
* @param array|string $key
* @param int $length
* @param string|null $label
*
* @return $this
*/
private function maxLengthRule(array|string $key, int $length, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (mb_strlen((string) $input, 'UTF-8') > $length) {
$this->addError($field, sprintf($label ?? $this->data['length']['max'], $field, $length));
}
}
return $this;
}
/**
* Range
*
* @param array|string $key
* @param int|float $min
* @param int|float $max
* @param string|null $label
*
* @return $this
*/
private function rangeRule(array|string $key, int|float $min, int|float $max, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if ($input < $min || $input > $max) {
$this->addError($field, sprintf($label ?? $this->data['range'], $field, $min, $max));
}
}
return $this;
}
/**
* Greater than
*
* @param array|string $key
* @param int|float $num
* @param string|null $label
*
* @return $this
*/
private function gtRule(array|string $key, int|float $num, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if ($input <= $num) {
$this->addError($field, sprintf($label ?? $this->data['gt'], $field, $num));
}
}
return $this;
}
/**
* Greater than or equal
*
* @param array|string $key
* @param int|float $num
* @param string|null $label
*
* @return $this
*/
private function gteRule(array|string $key, int|float $num, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if ($input < $num) {
$this->addError($field, sprintf($label ?? $this->data['gte'], $field, $num));
}
}
return $this;
}
/**
* Less than
*
* @param array|string $key
* @param int|float $num
* @param string|null $label
*
* @return $this
*/
private function ltRule(array|string $key, int|float $num, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if ($input >= $num) {
$this->addError($field, sprintf($label ?? $this->data['lt'], $field, $num));
}
}
return $this;
}
/**
* Less than or equal
*
* @param array|string $key
* @param int|float $num
* @param string|null $label
*
* @return $this
*/
private function lteRule(array|string $key, int|float $num, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if ($input > $num) {
$this->addError($field, sprintf($label ?? $this->data['lte'], $field, $num));
}
}
return $this;
}
/**
* Same rule
*
* @param string $key
* @param mixed $value
* @param string|null $label
*
* @return $this
*/
private function sameRule(string $key, mixed $value, ?string $label = null): self
{
$input = $this->getInput($key);
if (! $this->isRequired($key) && $this->blank($input)) {
return $this;
}
if ($input !== $value) {
$this->addError($key, sprintf($label ?? $this->data['same'], $key, $value));
}
return $this;
}
/**
* Not same rule
*
* @param string $key
* @param mixed $value
* @param string|null $label
*
* @return $this
*/
private function notSameRule(string $key, mixed $value, ?string $label = null): self
{
$input = $this->getInput($key);
if (! $this->isRequired($key) && $this->blank($input)) {
return $this;
}
if ($input === $value) {
$this->addError($key, sprintf($label ?? $this->data['notSame'], $key, $value));
}
return $this;
}
/**
* Equal
*
* @param string $key1
* @param string $key2
* @param string|null $label
*
* @return $this
*/
private function equalRule(string $key1, string $key2, ?string $label = null): self
{
$input1 = $this->getInput($key1);
$input2 = $this->getInput($key2);
if (! $this->isRequired($key1) && $this->blank($input1)) {
return $this;
}
if ($input1 !== $input2) {
$this->addError($key1, sprintf($label ?? $this->data['equal'], $key1, $key2));
}
return $this;
}
/**
* Not equal
*
* @param string $key1
* @param string $key2
* @param string|null $label
*
* @return $this
*/
private function notEqualRule(string $key1, string $key2, ?string $label = null): self
{
$input1 = $this->getInput($key1);
$input2 = $this->getInput($key2);
if (! $this->isRequired($key1) && $this->blank($input1)) {
return $this;
}
if ($input1 === $input2) {
$this->addError($key1, sprintf($label ?? $this->data['notEqual'], $key1, $key2));
}
return $this;
}
/**
* Empty
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function emptyRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (! $this->blank($input)) {
$this->addError($field, sprintf($label ?? $this->data['empty'], $field));
}
}
return $this;
}
/**
* Not empty
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function notEmptyRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if ($this->blank($input)) {
$this->addError($field, sprintf($label ?? $this->data['notEmpty'], $field));
}
}
return $this;
}
/**
* In
*
* @param array|string $key
* @param array $haystack
* @param string|null $label
*
* @return $this
*/
private function inRule(array|string $key, array $haystack, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (! in_array($input, $haystack, true)) {
$this->addError($field, sprintf($label ?? $this->data['in'], $field));
}
}
return $this;
}
/**
* Not in
*
* @param array|string $key
* @param array $haystack
* @param string|null $label
*
* @return $this
*/
private function notInRule(array|string $key, array $haystack, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (in_array($input, $haystack, true)) {
$this->addError($field, sprintf($label ?? $this->data['notIn'], $field));
}
}
return $this;
}
/**
* Regex
*
* @param array|string $key
* @param string $pattern
* @param string|null $label
*
* @return $this
*/
private function regexRule(array|string $key, string $pattern, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (! preg_match($pattern, $input)) {
$this->addError($field, sprintf($label ?? $this->data['regex'], $field));
}
}
return $this;
}
/**
* Check url
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function urlRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (! preg_match('|^https?://([а-яa-z0-9_\-.])+(\.([а-яa-z0-9/\-?_=#])+)+$|iu', $input)) {
$this->addError($field, sprintf($label ?? $this->data['url'], $field));
}
}
return $this;
}
/**
* Check email
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function emailRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (filter_var($input, FILTER_VALIDATE_EMAIL) === false) {
$this->addError($field, sprintf($label ?? $this->data['email'], $field));
}
}
return $this;
}
/**
* Check IP address
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function ipRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (filter_var($input, FILTER_VALIDATE_IP) === false) {
$this->addError($field, sprintf($label ?? $this->data['ip'], $field));
}
}
return $this;
}
/**
* Check phone
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function phoneRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (! preg_match('#^\d{8,13}$#', $input)) {
$this->addError($field, sprintf($label ?? $this->data['phone'], $field));
}
}
return $this;
}
/**
* Boolean rule
*
* @param array|string $key
* @param string|null $label
*
* @return $this
*/
private function booleanRule(array|string $key, ?string $label = null): self
{
$key = (array) $key;
foreach ($key as $field) {
$input = $this->getInput($field);
if (! $this->isRequired($field) && $this->blank($input)) {
return $this;
}
if (! in_array($input, [1, '1', 0, '0', true, false], true)) {
$this->addError($field, sprintf($label ?? $this->data['boolean'], $field));
}
}
return $this;
}
/**
* Add rule
*
* @param string $key
* @param callable $callable
* @param string $label
*
* @return $this
*/
private function addRule(string $key, callable $callable, string $label): self
{
$input = $this->getInput($key);
if (! $callable($input)) {
$this->addError($key, $label);
}
return $this;
}
/**
* Custom rule
*
* @param bool $compare
* @param array|string $label
*
* @return $this
*/
private function customRule(bool $compare, array|string $label): self
{
$key = '';
if (is_array($label)) {
$key = key($label);
$label = $label[$key];
}
if (! $compare) {
$this->addError($key, $label);
}
return $this;
}
/**
* Проверяет файл
*
* @param string $key
* @param array $rules
*
* @return $this
*/
private function fileRule(string $key, array $rules): self
{
$input = $this->getInput($key);
if (! $this->isRequired($key) && $this->blank($input)) {
return $this;
}
if (! $input instanceof UploadedFileInterface) {
$this->addError($key, sprintf($this->data['file']['error'], $key));
return $this;
}
if ($input->getError() !== UPLOAD_ERR_OK) {
$this->addError($key, $this->getUploadErrorByCode($input->getError()));
return $this;
}
if (empty($rules['extensions'])) {
$rules['extensions'] = $this->images;
}
$extension = strtolower(pathinfo($input->getClientFilename(), PATHINFO_EXTENSION));
if (! in_array($extension, $rules['extensions'], true)) {
$this->addError($key, sprintf($this->data['file']['extension'], $key, implode(', ', $rules['extensions'])));
}
if (isset($rules['size_max']) && $input->getSize() > $rules['size_max']) {
$this->addError($key, sprintf($this->data['file']['size_max'], $key, formatSize($rules['size_max'])));
}
if (in_array($extension, $this->images, true)) {
[$width, $height] = getimagesize($input->getFilePath());
if (! empty($rules['weight_max'])) {
if ($width > $rules['weight_max'] || $height > $rules['weight_max']) {
$this->addError($key, sprintf($this->data['file']['weight_max'], $key, $rules['weight_max']));
}
}
if (! empty($rules['weight_min'])) {
if ($width < $rules['weight_min'] || $height < $rules['weight_min']) {
$this->addError($key, sprintf($this->data['file']['weight_min'], $key, $rules['weight_min']));
}
} elseif (empty($width) || empty($height)) {
$this->addError($key, sprintf($this->data['file']['weight_empty'], $key));
}
}
return $this;
}
/**
* Add error
*
* @param string $key Field name
* @param string $label Text error
*
* @return void
*/
public function addError(string $key, string $label): void
{
if (isset($this->errors[$key])) {
$this->errors[] = $label;
} else {
$this->errors[$key] = $label;
}
}
/**
* Get errors
*
* @return array
*/
public function getErrors(): array
{
return $this->errors;
}
/**
* Determine if the given value is "blank".
*
* @param mixed $value
* @return bool
*/
private function blank(mixed $value): bool
{
if (is_null($value)) {
return true;
}
if (is_string($value)) {
return trim($value) === '';
}
if (is_numeric($value) || is_bool($value)) {
return false;
}
if ($value instanceof Countable) {
return count($value) === 0;
}
if ($value instanceof UploadedFileInterface) {
return $value->getError() === UPLOAD_ERR_NO_FILE;
}
return empty($value);
}
/**
* Is required
*
* @param string $key
* @return bool
*/
private function isRequired(string $key): bool
{
return isset($this->rules[$key]['required']);
}
/**
* Get input
*
* @param string $key
* @param mixed|null $default
*
* @return mixed
*/
private function getInput(string $key, mixed $default = null): mixed
{
return $this->input[$key] ?? $default;
}
/**
* Get upload error by code
*
* @param int $code
*
* @return string
*/
private function getUploadErrorByCode(int $code): string
{
return match ($code) {
UPLOAD_ERR_INI_SIZE => 'Размер файла превысил значение upload_max_filesize',
UPLOAD_ERR_FORM_SIZE => 'Размер файла превысил значение MAX_FILE_SIZE',
UPLOAD_ERR_PARTIAL => 'Загруженный файл был загружен только частично',
UPLOAD_ERR_NO_FILE => 'Файл не был загружен',
UPLOAD_ERR_NO_TMP_DIR => 'Отсутствует временная папка',
UPLOAD_ERR_CANT_WRITE => 'Не удалось записать файл на диск',
UPLOAD_ERR_EXTENSION => 'Модуль PHP остановил загрузку файла',
default => 'Неизвестная ошибка загрузки',
};
}
}