<?php
namespace PHPFUI\ORM;
abstract class Validator
{
public static array $dateSeparators = ['-', '.', '_', ':', '/'];
public static array $validators = [];
protected string $currentField = '';
protected array $currentFieldDefinitions = [];
protected bool $currentNot = false;
protected array $currentParameters = [];
protected bool $currentRequired = false;
protected array $fieldDefinitions = [];
private array $errors = [];
public function __construct(protected \PHPFUI\ORM\Record $record, protected ?\PHPFUI\ORM\Record $originalRecord = null)
{
$this->fieldDefinitions = $this->record->getFields();
}
public function getErrors() : array
{
return $this->errors;
}
public function validate(string $optionalMethod = '') : bool
{
$this->errors = [];
if ($optionalMethod && \method_exists($this, $optionalMethod))
{
$this->errors = $this->{$optionalMethod}();
return empty($this->errors);
}
foreach ($this->fieldDefinitions as $field => $fieldDefinitions)
{
$this->currentField = $field;
$errors = $this->getFieldErrors($this->record->{$field}, static::$validators[$field] ?? [], $fieldDefinitions);
if ($errors)
{
$this->errors[$field] = $errors;
}
}
return empty($this->errors);
}
protected function testIt(bool $condition, string $token, array $values = []) : string
{
$a = (int)$condition;
$b = (int)$this->currentNot;
if ($condition xor $this->currentNot)
{
return '';
}
$token = '.validator.' . ($this->currentNot ? 'not.' : '') . $token;
return \PHPFUI\ORM::trans($token, $values);
}
private function getFieldErrors(mixed $value, array $validators, array $fieldDefinitions) : array
{
$errors = [];
if (! \count($validators))
{
return $errors;
}
$length = \strlen("{$value}");
$this->currentRequired = false;
if (\in_array('required', $validators))
{
$this->currentRequired = true;
if (! $length)
{
$errors[] = \PHPFUI\ORM::trans('.validator.required');
return $errors;
}
}
elseif (! $length)
{
return $errors;
}
$orErrors = [];
foreach ($validators as $validator)
{
$orValidators = \explode('|', (string)$validator);
if (\count($orValidators) > 1)
{
$orErrors = [];
foreach ($orValidators as $validator)
{
$error = $this->validateRule($validator, $value, $fieldDefinitions);
if ($error)
{
$orErrors = \array_merge($orErrors, $error);
}
else
{
$orErrors = [];
break;
}
}
}
else
{
$errors = \array_merge($errors, $this->validateRule($validator, $value, $fieldDefinitions));
}
}
return \array_merge($errors, $orErrors);
}
private function validate_alpha(mixed $value) : string
{
return $this->testIt(\ctype_alpha((string)$value), 'alpha', ['value' => $value]);
}
private function validate_alpha_numeric(mixed $value) : string
{
return $this->testIt(\ctype_alnum((string)$value), 'alnum', ['value' => $value]);
}
private function validate_bool(mixed $value) : string
{
return $this->testIt(\ctype_digit((string)$value) && (0 == $value || 1 == $value), 'bool', ['value' => $value]);
}
private function validate_card(string $number) : string
{
$number = \preg_replace('/\D/', '', (string)$number);
$number_length = \strlen($number);
$parity = $number_length % 2;
$total = 0;
for ($i = 0; $i < $number_length; ++$i)
{
$digit = (int)$number[$i];
if ($i % 2 == $parity)
{
$digit *= 2;
if ($digit > 9)
{
$digit -= 9;
}
}
$total += $digit;
}
return $this->testIt(0 == $total % 10, 'card', ['value' => $number]);
}
private function validate_color(mixed $value) : string
{
$len = 0;
$testValue = '#' == $value[0] ? \substr((string)$value, 1) : $value;
if (\ctype_xdigit((string)$testValue))
{
$len = \strlen((string)$testValue);
}
return $this->testIt(3 == $len || 6 == $len, 'color', ['value' => $value]);
}
private function validate_contains(mixed $value) : string
{
$valid = false;
foreach ($this->currentParameters as $text)
{
$valid |= \str_contains($value, $text);
}
return $this->testIt($valid, 'contains', ['value' => $value, 'set' => \implode(',', $this->currentParameters)]);
}
private function validate_cvv(mixed $value) : string
{
$int = (int)$value;
return $this->testIt($int >= 100 && $int <= 9999, 'cvv', ['value' => $value]);
}
private function validate_date(mixed $value) : string
{
$year = 0;
$month = 1;
$day = 2;
$parts = \explode('/', \str_replace(self::$dateSeparators, '/', (string)$value));
if (! $this->currentRequired && ! \array_sum($parts))
{
return '';
}
return $this->testIt(\checkdate((int)($parts[$month] ?? 0), (int)($parts[$day] ?? 0), (int)($parts[$year] ?? 0)), 'date', ['value' => $value]);
}
private function validate_dateISO(mixed $value) : string
{
$year = 0;
$month = 1;
$day = 2;
$parts = \explode('-', (string)$value);
$year = \sprintf('%04d', (int)($parts[$year] ?? 0));
$month = \sprintf('%02d', (int)($parts[$month] ?? 0));
$day = \sprintf('%02d', (int)($parts[$day] ?? 0));
return $this->testIt(4 == \strlen($year) && 2 == \strlen($month) && 2 == \strlen($day) && \checkdate((int)$month, (int)$day, (int)$year), 'dateISO', ['value' => $value]);
}
private function validate_datetime(mixed $value) : string
{
if (\strpos((string)$value, 'T'))
{
$parts = \explode('T', (string)$value);
}
else
{
$parts = \explode(' ', (string)$value);
}
$error = $this->validate_date($parts[0]);
if ($error)
{
return $error;
}
return $this->validate_time($parts[1] ?? '');
}
private function validate_day_month_year(mixed $value) : string
{
$year = 2;
$month = 1;
$day = 0;
$parts = \explode('/', \str_replace(self::$dateSeparators, '/', (string)$value));
if (! $this->currentRequired && ! \array_sum($parts))
{
return '';
}
return $this->testIt(\checkdate((int)($parts[$month] ?? 0), (int)($parts[$day] ?? 0), (int)($parts[$year] ?? 0)), 'day_month_year', ['value' => $value]);
}
private function validate_domain(mixed $value) : string
{
return $this->testIt(false !== \filter_var($value, \FILTER_VALIDATE_DOMAIN, \FILTER_FLAG_HOSTNAME), 'domain', ['value' => $value]);
}
private function validate_email(mixed $value) : string
{
return $this->testIt(false !== \filter_var($value, \FILTER_VALIDATE_EMAIL), 'email', ['value' => $value]);
}
private function validate_ends_with(mixed $value) : string
{
$valid = false;
foreach ($this->currentParameters as $end)
{
$valid |= \str_ends_with($value, $end);
}
return $this->testIt($valid, 'ends_with', ['value' => $value, 'set' => \implode(',', $this->currentParameters)]);
}
private function validate_enum(mixed $value) : string
{
$valueUC = \strtoupper((string)$value);
$parametersUC = [];
foreach ($this->currentParameters as $enum)
{
$parametersUC[] = \strtoupper((string)$enum);
}
return $this->testIt(\in_array($valueUC, $parametersUC), 'enum', ['value' => $value, 'valid' => \implode(',', $this->currentParameters)]);
}
private function validate_enum_exact(mixed $value) : string
{
return $this->testIt(\in_array($value, $this->currentParameters), 'enum', ['value' => $value, 'valid' => \implode(',', $this->currentParameters)]);
}
private function validate_eq_field(mixed $value) : string
{
$field = $this->currentParameters[0] ?? '';
$compare = $this->record[$field];
return $this->testIt(empty($compare) || $value == $compare, 'eq_field', ['value' => $value, 'field' => $field, 'compare' => $compare]);
}
private function validate_equal(mixed $value) : string
{
$required = $this->currentParameters[0] ?? '';
return $this->testIt($required == $value, 'equal', ['value' => $value, 'required' => $required]);
}
private function validate_gt_field(mixed $value) : string
{
$field = $this->currentParameters[0] ?? '';
$compare = $this->record[$field];
return $this->testIt(empty($compare) || $value > $compare, 'gt_field', ['value' => $value, 'field' => $field, 'compare' => $compare]);
}
private function validate_gte_field(mixed $value) : string
{
$field = $this->currentParameters[0] ?? '';
$compare = $this->record[$field];
return $this->testIt(empty($compare) || $value >= $compare, 'gte_field', ['value' => $value, 'field' => $field, 'compare' => $compare]);
}
private function validate_icontains(mixed $value) : string
{
$valid = false;
$test = \strtolower($value);
foreach ($this->currentParameters as $text)
{
$valid |= \str_contains($test, \strtolower($text));
}
return $this->testIt($valid, 'icontains', ['value' => $value, 'set' => \implode(',', $this->currentParameters)]);
}
private function validate_iends_with(mixed $value) : string
{
$valid = false;
$test = \strtolower($value);
foreach ($this->currentParameters as $end)
{
$valid |= \str_ends_with($test, \strtolower($end));
}
return $this->testIt($valid, 'iends_with', ['value' => $value, 'set' => \implode(',', $this->currentParameters)]);
}
private function validate_integer(mixed $value) : string
{
return $this->testIt(false !== \filter_var($value, \FILTER_VALIDATE_INT), 'integer', ['value' => $value]);
}
private function validate_istarts_with(mixed $value) : string
{
$valid = false;
$test = \strtolower($value);
foreach ($this->currentParameters as $start)
{
$valid |= \str_starts_with($test, \strtolower($start));
}
return $this->testIt($valid, 'istarts_with', ['value' => $value, 'set' => \implode(',', $this->currentParameters)]);
}
private function validate_lt_field(mixed $value) : string
{
$field = $this->currentParameters[0] ?? '';
$compare = $this->record[$field];
return $this->testIt(empty($compare) || $value < $compare, 'lt_field', ['value' => $value, 'field' => $field, 'compare' => $compare]);
}
private function validate_lte_field(mixed $value) : string
{
$field = $this->currentParameters[0] ?? '';
$compare = $this->record[$field];
return $this->testIt(empty($compare) || $value <= $compare, 'lte_field', ['value' => $value, 'field' => $field, 'compare' => $compare]);
}
private function validate_maxlength(mixed $value) : string
{
$length = $this->currentParameters[0] ?? $this->currentFieldDefinitions[\PHPFUI\ORM\Record::LENGTH_INDEX];
return $this->testIt(\strlen((string)$value) <= $length, 'maxlength', ['value' => $value, 'length' => $length]);
}
private function validate_maxvalue(mixed $value) : string
{
if (! isset($this->currentParameters[0]))
{
return '';
}
return $this->testIt($this->currentParameters[0] >= $value, 'maxvalue', ['value' => $value, 'max' => $this->currentParameters[0]]);
}
private function validate_minlength(mixed $value) : string
{
$length = $this->currentParameters[0] ?? $this->currentFieldDefinitions[\PHPFUI\ORM\Record::LENGTH_INDEX];
return $this->testIt(\strlen((string)$value) >= $length, 'minlength', ['value' => $value, 'length' => $length]);
}
private function validate_minvalue(mixed $value) : string
{
if (! isset($this->currentParameters[0]))
{
return '';
}
return $this->testIt($this->currentParameters[0] <= $value, 'minvalue', ['value' => $value, 'min' => $this->currentParameters[0]]);
}
private function validate_month_day_year(mixed $value) : string
{
$year = 2;
$month = 0;
$day = 1;
$parts = \explode('/', \str_replace(self::$dateSeparators, '/', (string)$value));
if (! $this->currentRequired && ! \array_sum($parts))
{
return '';
}
return $this->testIt(\checkdate((int)($parts[$month] ?? 0), (int)($parts[$day] ?? 0), (int)($parts[$year] ?? 0)), 'month_day_year', ['value' => $value]);
}
private function validate_month_year(mixed $value) : string
{
$year = 1;
$month = 0;
$day = 1;
$parts = \explode('/', \str_replace(self::$dateSeparators, '/', (string)$value));
return $this->testIt(\checkdate((int)($parts[$month] ?? 0), $day, (int)($parts[$year] ?? 0)), 'month_year', ['value' => $value]);
}
private function validate_neq_field(mixed $value) : string
{
$field = $this->currentParameters[0] ?? '';
$compare = $this->record[$field];
return $this->testIt(empty($compare) || $value != $compare, 'neq_field', ['value' => $value, 'field' => $field, 'compare' => $compare]);
}
private function validate_not_equal(mixed $value) : string
{
$required = $this->currentParameters[0] ?? '';
return $this->testIt($required != $value, 'not_equal', ['value' => $value, 'required' => $required]);
}
private function validate_number(mixed $value) : string
{
return $this->testIt(false !== \filter_var($value, \FILTER_VALIDATE_FLOAT), 'number', ['value' => $value]);
}
private function validate_required(mixed $value) : string
{
return $this->testIt(\strlen("{$value}") > 0, 'required');
}
private function validate_starts_with(mixed $value) : string
{
$valid = false;
foreach ($this->currentParameters as $start)
{
$valid |= \str_starts_with($value, $start);
}
return $this->testIt($valid, 'starts_with', ['value' => $value, 'set' => \implode(',', $this->currentParameters)]);
}
private function validate_time(mixed $value) : string
{
$hours = ['H', 'h', 'G', 'g', ];
$tails = [':i:s', ':i', '', ];
$meridian = ['A', 'a', ''];
foreach ($hours as $hour)
{
foreach ($tails as $tail)
{
foreach ($meridian as $ampm)
{
$format = $hour . $tail . $ampm;
$t = \DateTime::createFromFormat($format, $value);
if ($t && ($t->format($format) === $value))
{
return '';
}
}
}
}
return \PHPFUI\ORM::trans('.validator.time', ['value' => $value]);
}
private function validate_unique(mixed $value) : string
{
$class = '\\' . \PHPFUI\ORM::$tableNamespace . '\\' . $this->record->getTableName();
$table = new $class();
$condition = new \PHPFUI\ORM\Condition();
$primaryKeys = $this->record->getPrimaryKeys();
if (1 == \count($primaryKeys))
{
$primaryKey = $primaryKeys[0];
$condition->and($primaryKey, $this->record->{$primaryKey}, new \PHPFUI\ORM\Operator\NotEqual());
}
$field = $this->currentField;
$condition->and($field, $this->record->{$field});
while (\count($this->currentParameters))
{
$field = \array_shift($this->currentParameters);
if (isset($this->fieldDefinitions[$field]))
{
$value = $this->record->{$field};
if (\count($this->currentParameters))
{
$next = \array_shift($this->currentParameters);
if (isset($this->fieldDefinitions[$next]))
{
\array_unshift($this->currentParameters, $next);
}
else
{
$value = $next;
}
}
$condition->and($field, $value);
}
else
{
throw new \Exception("{$field} is not a field of {$this->record->getTableName()}");
}
}
$table->setWhere($condition);
return $this->testIt(0 == (\is_countable($table) ? \count($table) : 0), 'unique', ['value' => $value]);
}
private function validate_url(mixed $value) : string
{
return $this->testIt(false !== \filter_var($value, \FILTER_VALIDATE_URL), 'url', ['value' => $value]);
}
private function validate_website(mixed $value) : string
{
$parts = \explode('://', \strtolower((string)$value));
$error = 2 != \count($parts) || ! \in_array($parts[0], ['http', 'https']);
return $this->testIt(! $error && false !== \filter_var($value, \FILTER_VALIDATE_URL), 'website', ['value' => $value]);
}
private function validate_year_month(mixed $value) : string
{
$year = 0;
$month = 1;
$day = 1;
$parts = \explode('/', \str_replace(self::$dateSeparators, '/', (string)$value));
return $this->testIt(\checkdate((int)($parts[$month] ?? 0), $day, (int)($parts[$year] ?? 0)), 'year_month', ['value' => $value]);
}
private function validateRule(string $validator, mixed $value, array $fieldDefinitions) : array
{
$this->currentFieldDefinitions = $fieldDefinitions;
$this->currentNot = false;
if ('!' == $validator[0])
{
$this->currentNot = true;
$validator = \substr($validator, 1);
}
$parts = \explode(':', (string)$validator);
$this->currentParameters = $errors = [];
if (\count($parts) > 1)
{
$this->currentParameters = \explode(',', $parts[1]);
}
$validator = $parts[0];
$method = 'validate_' . $validator;
if (\method_exists($this, $method))
{
$error = $this->{$method}($value);
if ($error)
{
$errors[] = $error;
}
}
else
{
throw new \Exception("Validator {$validator} (validate_{$validator} method) not found in class " . self::class);
}
return $errors;
}
}