add validator, error handler
This commit is contained in:
parent
fc391879d0
commit
cc4aa43c0e
444
errorhandler.php
Normal file
444
errorhandler.php
Normal file
@ -0,0 +1,444 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ErrorHandler manages error pages and exception handling
|
||||
*/
|
||||
class ErrorHandler
|
||||
{
|
||||
private array $handlers = [];
|
||||
private $defaultHandler = null;
|
||||
private bool $debug = false;
|
||||
|
||||
/**
|
||||
* __construct creates a new ErrorHandler
|
||||
*/
|
||||
public function __construct(bool $debug = false)
|
||||
{
|
||||
$this->debug = $debug;
|
||||
$this->registerDefaultHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* register registers a custom error handler for a specific status code
|
||||
*/
|
||||
public function register(int $status, callable $handler): void
|
||||
{
|
||||
$this->handlers[$status] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* setDefaultHandler sets the default error handler for unregistered status codes
|
||||
*/
|
||||
public function setDefaultHandler(callable $handler): void
|
||||
{
|
||||
$this->defaultHandler = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* handle handles an error with the appropriate handler
|
||||
*/
|
||||
public function handle(Context $context, int $status, string $message = '', ?Exception $exception = null): void
|
||||
{
|
||||
if (isset($this->handlers[$status])) {
|
||||
$handler = $this->handlers[$status];
|
||||
$handler($context, $status, $message, $exception);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->defaultHandler) {
|
||||
($this->defaultHandler)($context, $status, $message, $exception);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->renderDefaultError($context, $status, $message, $exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* handleException handles uncaught exceptions
|
||||
*/
|
||||
public function handleException(Context $context, Exception $exception): void
|
||||
{
|
||||
$status = $this->getStatusFromException($exception);
|
||||
$message = $this->debug ? $exception->getMessage() : $this->getDefaultMessage($status);
|
||||
|
||||
if ($this->debug) {
|
||||
error_log($exception->getMessage() . "\n" . $exception->getTraceAsString());
|
||||
}
|
||||
|
||||
$this->handle($context, $status, $message, $exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* getStatusFromException determines HTTP status from exception type
|
||||
*/
|
||||
private function getStatusFromException(Exception $exception): int
|
||||
{
|
||||
if ($exception instanceof HttpException) {
|
||||
return $exception->getStatusCode();
|
||||
}
|
||||
|
||||
return match(get_class($exception)) {
|
||||
'InvalidArgumentException' => 400,
|
||||
'UnauthorizedException' => 401,
|
||||
'ForbiddenException' => 403,
|
||||
'NotFoundException' => 404,
|
||||
'MethodNotAllowedException' => 405,
|
||||
'ValidationException' => 422,
|
||||
default => 500
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* registerDefaultHandlers registers built-in error handlers
|
||||
*/
|
||||
private function registerDefaultHandlers(): void
|
||||
{
|
||||
// 404 Not Found
|
||||
$this->register(404, function(Context $context, int $status, string $message) {
|
||||
$accept = $context->request->header('accept') ?? '';
|
||||
|
||||
if (str_contains($accept, 'application/json')) {
|
||||
$context->json(['error' => $message ?: 'Not Found'], 404);
|
||||
} else {
|
||||
$html = $this->render404Page($message);
|
||||
$context->html($html, 404);
|
||||
}
|
||||
});
|
||||
|
||||
// 500 Internal Server Error
|
||||
$this->register(500, function(Context $context, int $status, string $message, ?Exception $exception) {
|
||||
$accept = $context->request->header('accept') ?? '';
|
||||
|
||||
if (str_contains($accept, 'application/json')) {
|
||||
$response = ['error' => $message ?: 'Internal Server Error'];
|
||||
if ($this->debug && $exception) {
|
||||
$response['trace'] = $exception->getTrace();
|
||||
}
|
||||
$context->json($response, 500);
|
||||
} else {
|
||||
$html = $this->render500Page($message, $exception);
|
||||
$context->html($html, 500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* renderDefaultError renders a generic error response
|
||||
*/
|
||||
private function renderDefaultError(Context $context, int $status, string $message, ?Exception $exception): void
|
||||
{
|
||||
$message = $message ?: $this->getDefaultMessage($status);
|
||||
$accept = $context->request->header('accept') ?? '';
|
||||
|
||||
if (str_contains($accept, 'application/json')) {
|
||||
$response = ['error' => $message];
|
||||
if ($this->debug && $exception) {
|
||||
$response['trace'] = $exception->getTrace();
|
||||
}
|
||||
$context->json($response, $status);
|
||||
} else {
|
||||
$html = $this->renderErrorPage($status, $message, $exception);
|
||||
$context->html($html, $status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getDefaultMessage gets default message for status code
|
||||
*/
|
||||
private function getDefaultMessage(int $status): string
|
||||
{
|
||||
return match($status) {
|
||||
400 => 'Bad Request',
|
||||
401 => 'Unauthorized',
|
||||
403 => 'Forbidden',
|
||||
404 => 'Not Found',
|
||||
405 => 'Method Not Allowed',
|
||||
422 => 'Unprocessable Entity',
|
||||
429 => 'Too Many Requests',
|
||||
500 => 'Internal Server Error',
|
||||
502 => 'Bad Gateway',
|
||||
503 => 'Service Unavailable',
|
||||
default => 'Error'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* render404Page renders a 404 error page
|
||||
*/
|
||||
private function render404Page(string $message): string
|
||||
{
|
||||
$message = htmlspecialchars($message ?: 'Not Found');
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>404 - Not Found</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 6rem;
|
||||
margin: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
p {
|
||||
color: #666;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>404</h1>
|
||||
<h2>{$message}</h2>
|
||||
<p>The page you are looking for could not be found.</p>
|
||||
<a href="/">Go to Homepage</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* render500Page renders a 500 error page
|
||||
*/
|
||||
private function render500Page(string $message, ?Exception $exception): string
|
||||
{
|
||||
$message = htmlspecialchars($message ?: 'Internal Server Error');
|
||||
$debugInfo = '';
|
||||
|
||||
if ($this->debug && $exception) {
|
||||
$exceptionClass = get_class($exception);
|
||||
$file = htmlspecialchars($exception->getFile());
|
||||
$line = $exception->getLine();
|
||||
$trace = htmlspecialchars($exception->getTraceAsString());
|
||||
|
||||
$debugInfo = <<<HTML
|
||||
<div class="debug-info">
|
||||
<h3>Debug Information</h3>
|
||||
<p><strong>Exception:</strong> {$exceptionClass}</p>
|
||||
<p><strong>File:</strong> {$file}:{$line}</p>
|
||||
<pre>{$trace}</pre>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>500 - Internal Server Error</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 6rem;
|
||||
margin: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
p {
|
||||
color: #666;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
.debug-info {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
.debug-info h3 {
|
||||
margin-top: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
.debug-info pre {
|
||||
background: #f8f8f8;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>500</h1>
|
||||
<h2>{$message}</h2>
|
||||
<p>Something went wrong on our end. Please try again later.</p>
|
||||
{$debugInfo}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* renderErrorPage renders a generic error page
|
||||
*/
|
||||
private function renderErrorPage(int $status, string $message, ?Exception $exception): string
|
||||
{
|
||||
$message = htmlspecialchars($message);
|
||||
$debugInfo = '';
|
||||
|
||||
if ($this->debug && $exception) {
|
||||
$exceptionClass = get_class($exception);
|
||||
$file = htmlspecialchars($exception->getFile());
|
||||
$line = $exception->getLine();
|
||||
$trace = htmlspecialchars($exception->getTraceAsString());
|
||||
|
||||
$debugInfo = <<<HTML
|
||||
<div class="debug-info">
|
||||
<h3>Debug Information</h3>
|
||||
<p><strong>Exception:</strong> {$exceptionClass}</p>
|
||||
<p><strong>File:</strong> {$file}:{$line}</p>
|
||||
<pre>{$trace}</pre>
|
||||
</div>
|
||||
HTML;
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>{$status} - {$message}</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.error-container {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 6rem;
|
||||
margin: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.5rem;
|
||||
margin: 1rem 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
.debug-info {
|
||||
margin-top: 2rem;
|
||||
padding: 1rem;
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-align: left;
|
||||
}
|
||||
.debug-info h3 {
|
||||
margin-top: 0;
|
||||
color: #e74c3c;
|
||||
}
|
||||
.debug-info pre {
|
||||
background: #f8f8f8;
|
||||
padding: 1rem;
|
||||
overflow-x: auto;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="error-container">
|
||||
<h1>{$status}</h1>
|
||||
<h2>{$message}</h2>
|
||||
{$debugInfo}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HttpException base class for HTTP exceptions
|
||||
*/
|
||||
class HttpException extends Exception
|
||||
{
|
||||
protected int $statusCode;
|
||||
|
||||
public function __construct(int $statusCode, string $message = '', int $code = 0, Exception $previous = null)
|
||||
{
|
||||
$this->statusCode = $statusCode;
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
public function getStatusCode(): int
|
||||
{
|
||||
return $this->statusCode;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ValidationException for validation errors
|
||||
*/
|
||||
class ValidationException extends HttpException
|
||||
{
|
||||
private array $errors;
|
||||
|
||||
public function __construct(array $errors, string $message = 'Validation failed')
|
||||
{
|
||||
$this->errors = $errors;
|
||||
parent::__construct(422, $message);
|
||||
}
|
||||
|
||||
public function getErrors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
}
|
||||
355
validator.php
Normal file
355
validator.php
Normal file
@ -0,0 +1,355 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Validator provides input validation with chainable rules
|
||||
*/
|
||||
class Validator
|
||||
{
|
||||
private array $errors = [];
|
||||
private array $data = [];
|
||||
private array $rules = [];
|
||||
private array $messages = [];
|
||||
|
||||
/**
|
||||
* Custom validation rules registry
|
||||
*/
|
||||
private static array $customRules = [];
|
||||
|
||||
/**
|
||||
* validate performs validation on data with given rules
|
||||
*/
|
||||
public function validate(array $data, array $rules, array $messages = []): bool
|
||||
{
|
||||
$this->data = $data;
|
||||
$this->rules = $rules;
|
||||
$this->messages = $messages;
|
||||
$this->errors = [];
|
||||
|
||||
foreach ($rules as $field => $fieldRules) {
|
||||
$this->validateField($field, $fieldRules);
|
||||
}
|
||||
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* validateField validates a single field against its rules
|
||||
*/
|
||||
private function validateField(string $field, string|array $rules): void
|
||||
{
|
||||
$rules = is_string($rules) ? explode('|', $rules) : $rules;
|
||||
$value = $this->getValue($field);
|
||||
|
||||
foreach ($rules as $rule) {
|
||||
$this->applyRule($field, $value, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* getValue gets a value from data using dot notation
|
||||
*/
|
||||
private function getValue(string $field): mixed
|
||||
{
|
||||
$keys = explode('.', $field);
|
||||
$value = $this->data;
|
||||
|
||||
foreach ($keys as $key) {
|
||||
if (!is_array($value) || !array_key_exists($key, $value)) {
|
||||
return null;
|
||||
}
|
||||
$value = $value[$key];
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* applyRule applies a single validation rule
|
||||
*/
|
||||
private function applyRule(string $field, mixed $value, string $rule): void
|
||||
{
|
||||
$parts = explode(':', $rule, 2);
|
||||
$ruleName = $parts[0];
|
||||
$parameters = isset($parts[1]) ? explode(',', $parts[1]) : [];
|
||||
|
||||
$passes = match($ruleName) {
|
||||
'required' => $this->validateRequired($value),
|
||||
'email' => $this->validateEmail($value),
|
||||
'url' => $this->validateUrl($value),
|
||||
'alpha' => $this->validateAlpha($value),
|
||||
'alphaNum' => $this->validateAlphaNum($value),
|
||||
'numeric' => $this->validateNumeric($value),
|
||||
'integer' => $this->validateInteger($value),
|
||||
'float' => $this->validateFloat($value),
|
||||
'boolean' => $this->validateBoolean($value),
|
||||
'array' => $this->validateArray($value),
|
||||
'json' => $this->validateJson($value),
|
||||
'date' => $this->validateDate($value),
|
||||
'min' => $this->validateMin($value, $parameters[0] ?? 0),
|
||||
'max' => $this->validateMax($value, $parameters[0] ?? 0),
|
||||
'between' => $this->validateBetween($value, $parameters[0] ?? 0, $parameters[1] ?? 0),
|
||||
'length' => $this->validateLength($value, $parameters[0] ?? 0),
|
||||
'in' => $this->validateIn($value, $parameters),
|
||||
'notIn' => $this->validateNotIn($value, $parameters),
|
||||
'regex' => $this->validateRegex($value, $parameters[0] ?? ''),
|
||||
'confirmed' => $this->validateConfirmed($field, $value),
|
||||
'unique' => $this->validateUnique($value, $parameters),
|
||||
'exists' => $this->validateExists($value, $parameters),
|
||||
default => $this->applyCustomRule($ruleName, $value, $parameters)
|
||||
};
|
||||
|
||||
if (!$passes) {
|
||||
$this->addError($field, $ruleName, $parameters);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validation rule methods
|
||||
*/
|
||||
private function validateRequired(mixed $value): bool
|
||||
{
|
||||
if ($value === null) return false;
|
||||
if (is_string($value) && trim($value) === '') return false;
|
||||
if (is_array($value) && empty($value)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateEmail(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
private function validateUrl(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||
}
|
||||
|
||||
private function validateAlpha(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
return ctype_alpha($value);
|
||||
}
|
||||
|
||||
private function validateAlphaNum(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
return ctype_alnum($value);
|
||||
}
|
||||
|
||||
private function validateNumeric(mixed $value): bool
|
||||
{
|
||||
return is_numeric($value);
|
||||
}
|
||||
|
||||
private function validateInteger(mixed $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_INT) !== false;
|
||||
}
|
||||
|
||||
private function validateFloat(mixed $value): bool
|
||||
{
|
||||
return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
|
||||
}
|
||||
|
||||
private function validateBoolean(mixed $value): bool
|
||||
{
|
||||
return in_array($value, [true, false, 0, 1, '0', '1', 'true', 'false'], true);
|
||||
}
|
||||
|
||||
private function validateArray(mixed $value): bool
|
||||
{
|
||||
return is_array($value);
|
||||
}
|
||||
|
||||
private function validateJson(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
json_decode($value);
|
||||
return json_last_error() === JSON_ERROR_NONE;
|
||||
}
|
||||
|
||||
private function validateDate(mixed $value): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
$date = date_parse($value);
|
||||
return $date['error_count'] === 0 && $date['warning_count'] === 0;
|
||||
}
|
||||
|
||||
private function validateMin(mixed $value, mixed $min): bool
|
||||
{
|
||||
if (is_numeric($value)) return $value >= $min;
|
||||
if (is_string($value)) return strlen($value) >= $min;
|
||||
if (is_array($value)) return count($value) >= $min;
|
||||
return false;
|
||||
}
|
||||
|
||||
private function validateMax(mixed $value, mixed $max): bool
|
||||
{
|
||||
if (is_numeric($value)) return $value <= $max;
|
||||
if (is_string($value)) return strlen($value) <= $max;
|
||||
if (is_array($value)) return count($value) <= $max;
|
||||
return false;
|
||||
}
|
||||
|
||||
private function validateBetween(mixed $value, mixed $min, mixed $max): bool
|
||||
{
|
||||
return $this->validateMin($value, $min) && $this->validateMax($value, $max);
|
||||
}
|
||||
|
||||
private function validateLength(mixed $value, mixed $length): bool
|
||||
{
|
||||
if (is_string($value)) return strlen($value) == $length;
|
||||
if (is_array($value)) return count($value) == $length;
|
||||
return false;
|
||||
}
|
||||
|
||||
private function validateIn(mixed $value, array $values): bool
|
||||
{
|
||||
return in_array($value, $values, true);
|
||||
}
|
||||
|
||||
private function validateNotIn(mixed $value, array $values): bool
|
||||
{
|
||||
return !in_array($value, $values, true);
|
||||
}
|
||||
|
||||
private function validateRegex(mixed $value, string $pattern): bool
|
||||
{
|
||||
if (!is_string($value)) return false;
|
||||
return preg_match($pattern, $value) === 1;
|
||||
}
|
||||
|
||||
private function validateConfirmed(string $field, mixed $value): bool
|
||||
{
|
||||
$confirmField = $field . '_confirmation';
|
||||
return $value === $this->getValue($confirmField);
|
||||
}
|
||||
|
||||
private function validateUnique(mixed $value, array $parameters): bool
|
||||
{
|
||||
// Placeholder for database uniqueness check
|
||||
// Would require database connection in real implementation
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateExists(mixed $value, array $parameters): bool
|
||||
{
|
||||
// Placeholder for database existence check
|
||||
// Would require database connection in real implementation
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* applyCustomRule applies a custom validation rule
|
||||
*/
|
||||
private function applyCustomRule(string $ruleName, mixed $value, array $parameters): bool
|
||||
{
|
||||
if (!isset(self::$customRules[$ruleName])) {
|
||||
return true; // Unknown rules pass by default
|
||||
}
|
||||
|
||||
return call_user_func(self::$customRules[$ruleName], $value, $parameters, $this->data);
|
||||
}
|
||||
|
||||
/**
|
||||
* addError adds a validation error
|
||||
*/
|
||||
private function addError(string $field, string $rule, array $parameters = []): void
|
||||
{
|
||||
$message = $this->messages["$field.$rule"]
|
||||
?? $this->messages[$rule]
|
||||
?? $this->getDefaultMessage($field, $rule, $parameters);
|
||||
|
||||
if (!isset($this->errors[$field])) {
|
||||
$this->errors[$field] = [];
|
||||
}
|
||||
|
||||
$this->errors[$field][] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* getDefaultMessage gets a default error message
|
||||
*/
|
||||
private function getDefaultMessage(string $field, string $rule, array $parameters): string
|
||||
{
|
||||
$fieldName = str_replace('_', ' ', $field);
|
||||
|
||||
return match($rule) {
|
||||
'required' => "The {$fieldName} field is required.",
|
||||
'email' => "The {$fieldName} must be a valid email address.",
|
||||
'url' => "The {$fieldName} must be a valid URL.",
|
||||
'alpha' => "The {$fieldName} may only contain letters.",
|
||||
'alphaNum' => "The {$fieldName} may only contain letters and numbers.",
|
||||
'numeric' => "The {$fieldName} must be a number.",
|
||||
'integer' => "The {$fieldName} must be an integer.",
|
||||
'float' => "The {$fieldName} must be a float.",
|
||||
'boolean' => "The {$fieldName} must be a boolean.",
|
||||
'array' => "The {$fieldName} must be an array.",
|
||||
'json' => "The {$fieldName} must be valid JSON.",
|
||||
'date' => "The {$fieldName} must be a valid date.",
|
||||
'min' => "The {$fieldName} must be at least {$parameters[0]}.",
|
||||
'max' => "The {$fieldName} must not be greater than {$parameters[0]}.",
|
||||
'between' => "The {$fieldName} must be between {$parameters[0]} and {$parameters[1]}.",
|
||||
'length' => "The {$fieldName} must be exactly {$parameters[0]} characters.",
|
||||
'in' => "The selected {$fieldName} is invalid.",
|
||||
'notIn' => "The selected {$fieldName} is invalid.",
|
||||
'regex' => "The {$fieldName} format is invalid.",
|
||||
'confirmed' => "The {$fieldName} confirmation does not match.",
|
||||
'unique' => "The {$fieldName} has already been taken.",
|
||||
'exists' => "The selected {$fieldName} is invalid.",
|
||||
default => "The {$fieldName} is invalid."
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* errors returns all validation errors
|
||||
*/
|
||||
public function errors(): array
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* failed checks if validation failed
|
||||
*/
|
||||
public function failed(): bool
|
||||
{
|
||||
return !empty($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* passed checks if validation passed
|
||||
*/
|
||||
public function passed(): bool
|
||||
{
|
||||
return empty($this->errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* firstError gets the first error message
|
||||
*/
|
||||
public function firstError(string $field = null): ?string
|
||||
{
|
||||
if ($field) {
|
||||
return $this->errors[$field][0] ?? null;
|
||||
}
|
||||
|
||||
foreach ($this->errors as $errors) {
|
||||
if (!empty($errors)) {
|
||||
return $errors[0];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* extend adds a custom validation rule
|
||||
*/
|
||||
public static function extend(string $name, callable $callback): void
|
||||
{
|
||||
self::$customRules[$name] = $callback;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user