tab indentation
This commit is contained in:
parent
5cc24441d0
commit
65bfec8078
562
Context.php
562
Context.php
@ -8,326 +8,326 @@ require_once __DIR__ . '/Validator.php';
|
|||||||
*/
|
*/
|
||||||
class Context
|
class Context
|
||||||
{
|
{
|
||||||
public Request $request;
|
public Request $request;
|
||||||
public Response $response;
|
public Response $response;
|
||||||
public Session $session;
|
public Session $session;
|
||||||
public array $state = [];
|
public array $state = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __construct creates a new Context with request and response
|
* __construct creates a new Context with request and response
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->request = new Request();
|
$this->request = new Request();
|
||||||
$this->response = new Response();
|
$this->response = new Response();
|
||||||
$this->session = new Session();
|
$this->session = new Session();
|
||||||
$this->session->start();
|
$this->session->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set stores a value in the context state
|
* set stores a value in the context state
|
||||||
*/
|
*/
|
||||||
public function set(string $key, mixed $value): void
|
public function set(string $key, mixed $value): void
|
||||||
{
|
{
|
||||||
$this->state[$key] = $value;
|
$this->state[$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get retrieves a value from the context state
|
* get retrieves a value from the context state
|
||||||
*/
|
*/
|
||||||
public function get(string $key): mixed
|
public function get(string $key): mixed
|
||||||
{
|
{
|
||||||
return $this->state[$key] ?? null;
|
return $this->state[$key] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* json sends a JSON response
|
* json sends a JSON response
|
||||||
*/
|
*/
|
||||||
public function json(mixed $data, int $status = 200): void
|
public function json(mixed $data, int $status = 200): void
|
||||||
{
|
{
|
||||||
$this->response->json($data, $status)->send();
|
$this->response->json($data, $status)->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* text sends a plain text response
|
* text sends a plain text response
|
||||||
*/
|
*/
|
||||||
public function text(string $text, int $status = 200): void
|
public function text(string $text, int $status = 200): void
|
||||||
{
|
{
|
||||||
$this->response->text($text, $status)->send();
|
$this->response->text($text, $status)->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* html sends an HTML response
|
* html sends an HTML response
|
||||||
*/
|
*/
|
||||||
public function html(string $html, int $status = 200): void
|
public function html(string $html, int $status = 200): void
|
||||||
{
|
{
|
||||||
$this->response->html($html, $status)->send();
|
$this->response->html($html, $status)->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* redirect sends a redirect response
|
* redirect sends a redirect response
|
||||||
*/
|
*/
|
||||||
public function redirect(string $url, int $status = 302): void
|
public function redirect(string $url, int $status = 302): void
|
||||||
{
|
{
|
||||||
$this->response->redirect($url, $status)->send();
|
$this->response->redirect($url, $status)->send();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* error sends an error response with appropriate content type
|
* error sends an error response with appropriate content type
|
||||||
*/
|
*/
|
||||||
public function error(int $status, string $message = ''): void
|
public function error(int $status, string $message = ''): void
|
||||||
{
|
{
|
||||||
$messages = [
|
$messages = [
|
||||||
400 => 'Bad Request',
|
400 => 'Bad Request',
|
||||||
401 => 'Unauthorized',
|
401 => 'Unauthorized',
|
||||||
403 => 'Forbidden',
|
403 => 'Forbidden',
|
||||||
404 => 'Not Found',
|
404 => 'Not Found',
|
||||||
405 => 'Method Not Allowed',
|
405 => 'Method Not Allowed',
|
||||||
500 => 'Internal Server Error',
|
500 => 'Internal Server Error',
|
||||||
502 => 'Bad Gateway',
|
502 => 'Bad Gateway',
|
||||||
503 => 'Service Unavailable'
|
503 => 'Service Unavailable'
|
||||||
];
|
];
|
||||||
|
|
||||||
$message = $message ?: ($messages[$status] ?? 'Error');
|
$message = $message ?: ($messages[$status] ?? 'Error');
|
||||||
|
|
||||||
$this->response->status($status);
|
$this->response->status($status);
|
||||||
|
|
||||||
if ($this->request->header('accept') && str_contains($this->request->header('accept'), 'application/json')) {
|
if ($this->request->header('accept') && str_contains($this->request->header('accept'), 'application/json')) {
|
||||||
$this->json(['error' => $message], $status);
|
$this->json(['error' => $message], $status);
|
||||||
} else {
|
} else {
|
||||||
$this->text($message, $status);
|
$this->text($message, $status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validate validates input data against rules
|
* validate validates input data against rules
|
||||||
*/
|
*/
|
||||||
public function validate(array $data, array $rules, array $messages = []): Validator
|
public function validate(array $data, array $rules, array $messages = []): Validator
|
||||||
{
|
{
|
||||||
$validator = new Validator();
|
$validator = new Validator();
|
||||||
$validator->validate($data, $rules, $messages);
|
$validator->validate($data, $rules, $messages);
|
||||||
|
|
||||||
if ($validator->failed()) {
|
if ($validator->failed()) {
|
||||||
throw new ValidationException($validator->errors());
|
throw new ValidationException($validator->errors());
|
||||||
}
|
}
|
||||||
|
|
||||||
return $validator;
|
return $validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validateRequest validates request data against rules
|
* validateRequest validates request data against rules
|
||||||
*/
|
*/
|
||||||
public function validateRequest(array $rules, array $messages = []): Validator
|
public function validateRequest(array $rules, array $messages = []): Validator
|
||||||
{
|
{
|
||||||
$data = $this->request->all();
|
$data = $this->request->all();
|
||||||
return $this->validate($data, $rules, $messages);
|
return $this->validate($data, $rules, $messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* header returns the value of a request header
|
* header returns the value of a request header
|
||||||
*/
|
*/
|
||||||
public function header(string $name): ?string
|
public function header(string $name): ?string
|
||||||
{
|
{
|
||||||
return $this->request->header($name);
|
return $this->request->header($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* input returns a form input value (POST data)
|
* input returns a form input value (POST data)
|
||||||
*/
|
*/
|
||||||
public function input(string $name, mixed $default = null): mixed
|
public function input(string $name, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->request->input($name, $default);
|
return $this->request->input($name, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* query returns a query parameter value
|
* query returns a query parameter value
|
||||||
*/
|
*/
|
||||||
public function query(string $name, mixed $default = null): mixed
|
public function query(string $name, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->request->query($name, $default);
|
return $this->request->query($name, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* jsonValue returns a value from JSON body
|
* jsonValue returns a value from JSON body
|
||||||
*/
|
*/
|
||||||
public function jsonValue(string $name, mixed $default = null): mixed
|
public function jsonValue(string $name, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->request->jsonValue($name, $default);
|
return $this->request->jsonValue($name, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* param returns a route parameter by integer index
|
* param returns a route parameter by integer index
|
||||||
*/
|
*/
|
||||||
public function param(int $index, mixed $default = null): mixed
|
public function param(int $index, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->request->param($index, $default);
|
return $this->request->param($index, $default);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* all returns all input data merged from all sources
|
* all returns all input data merged from all sources
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function all(): array
|
||||||
{
|
{
|
||||||
return $this->request->all();
|
return $this->request->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only returns only specified keys from input
|
* only returns only specified keys from input
|
||||||
*/
|
*/
|
||||||
public function only(array $keys): array
|
public function only(array $keys): array
|
||||||
{
|
{
|
||||||
return $this->request->only($keys);
|
return $this->request->only($keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* except returns all input except specified keys
|
* except returns all input except specified keys
|
||||||
*/
|
*/
|
||||||
public function except(array $keys): array
|
public function except(array $keys): array
|
||||||
{
|
{
|
||||||
return $this->request->except($keys);
|
return $this->request->except($keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* has checks if input key exists
|
* has checks if input key exists
|
||||||
*/
|
*/
|
||||||
public function has(string $key): bool
|
public function has(string $key): bool
|
||||||
{
|
{
|
||||||
return $this->request->has($key);
|
return $this->request->has($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cookie returns the value of a request cookie
|
* cookie returns the value of a request cookie
|
||||||
*/
|
*/
|
||||||
public function cookie(string $name): ?string
|
public function cookie(string $name): ?string
|
||||||
{
|
{
|
||||||
return $this->request->cookie($name);
|
return $this->request->cookie($name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* expectsJson checks if request expects JSON response
|
* expectsJson checks if request expects JSON response
|
||||||
*/
|
*/
|
||||||
public function expectsJson(): bool
|
public function expectsJson(): bool
|
||||||
{
|
{
|
||||||
return $this->request->expectsJson();
|
return $this->request->expectsJson();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isAjax checks if request is AJAX
|
* isAjax checks if request is AJAX
|
||||||
*/
|
*/
|
||||||
public function isAjax(): bool
|
public function isAjax(): bool
|
||||||
{
|
{
|
||||||
return $this->request->isAjax();
|
return $this->request->isAjax();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ip returns the client IP address
|
* ip returns the client IP address
|
||||||
*/
|
*/
|
||||||
public function ip(): string
|
public function ip(): string
|
||||||
{
|
{
|
||||||
return $this->request->ip();
|
return $this->request->ip();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* userAgent returns the user agent string
|
* userAgent returns the user agent string
|
||||||
*/
|
*/
|
||||||
public function userAgent(): string
|
public function userAgent(): string
|
||||||
{
|
{
|
||||||
return $this->request->userAgent();
|
return $this->request->userAgent();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* referer returns the referer URL
|
* referer returns the referer URL
|
||||||
*/
|
*/
|
||||||
public function referer(): ?string
|
public function referer(): ?string
|
||||||
{
|
{
|
||||||
return $this->request->referer();
|
return $this->request->referer();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isSecure checks if request is over HTTPS
|
* isSecure checks if request is over HTTPS
|
||||||
*/
|
*/
|
||||||
public function isSecure(): bool
|
public function isSecure(): bool
|
||||||
{
|
{
|
||||||
return $this->request->isSecure();
|
return $this->request->isSecure();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* url returns the full URL of the request
|
* url returns the full URL of the request
|
||||||
*/
|
*/
|
||||||
public function url(): string
|
public function url(): string
|
||||||
{
|
{
|
||||||
return $this->request->url();
|
return $this->request->url();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fullUrl returns the URL with query string
|
* fullUrl returns the URL with query string
|
||||||
*/
|
*/
|
||||||
public function fullUrl(): string
|
public function fullUrl(): string
|
||||||
{
|
{
|
||||||
return $this->request->fullUrl();
|
return $this->request->fullUrl();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is checks if the request path matches a pattern
|
* is checks if the request path matches a pattern
|
||||||
*/
|
*/
|
||||||
public function is(string $pattern): bool
|
public function is(string $pattern): bool
|
||||||
{
|
{
|
||||||
return $this->request->is($pattern);
|
return $this->request->is($pattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* contentType returns the request content type without charset
|
* contentType returns the request content type without charset
|
||||||
*/
|
*/
|
||||||
public function contentType(): string
|
public function contentType(): string
|
||||||
{
|
{
|
||||||
return $this->request->contentType();
|
return $this->request->contentType();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* status sets the HTTP status code for the response
|
* status sets the HTTP status code for the response
|
||||||
*/
|
*/
|
||||||
public function status(int $code): Response
|
public function status(int $code): Response
|
||||||
{
|
{
|
||||||
return $this->response->status($code);
|
return $this->response->status($code);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* setHeader sets a header for the response
|
* setHeader sets a header for the response
|
||||||
*/
|
*/
|
||||||
public function setHeader(string $name, string $value): Response
|
public function setHeader(string $name, string $value): Response
|
||||||
{
|
{
|
||||||
return $this->response->header($name, $value);
|
return $this->response->header($name, $value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* setCookie sets a cookie with the given name, value, and options
|
* setCookie sets a cookie with the given name, value, and options
|
||||||
*/
|
*/
|
||||||
public function setCookie(string $name, string $value, array $options = []): Response
|
public function setCookie(string $name, string $value, array $options = []): Response
|
||||||
{
|
{
|
||||||
return $this->response->cookie($name, $value, $options);
|
return $this->response->cookie($name, $value, $options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* write adds content to the response body
|
* write adds content to the response body
|
||||||
*/
|
*/
|
||||||
public function write(string $content): Response
|
public function write(string $content): Response
|
||||||
{
|
{
|
||||||
return $this->response->write($content);
|
return $this->response->write($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* end ends the response with optional content
|
* end ends the response with optional content
|
||||||
*/
|
*/
|
||||||
public function end(string $content = ''): void
|
public function end(string $content = ''): void
|
||||||
{
|
{
|
||||||
$this->response->end($content);
|
$this->response->end($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* send sends the response to the client
|
* send sends the response to the client
|
||||||
*/
|
*/
|
||||||
public function send(): void
|
public function send(): void
|
||||||
{
|
{
|
||||||
$this->response->send();
|
$this->response->send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
476
EXAMPLES.md
476
EXAMPLES.md
@ -11,11 +11,11 @@ require_once 'Web.php';
|
|||||||
$app = new Web(debug: true);
|
$app = new Web(debug: true);
|
||||||
|
|
||||||
$app->get('/', function(Context $context) {
|
$app->get('/', function(Context $context) {
|
||||||
return 'Hello World!';
|
return 'Hello World!';
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->get('/users/:id', function(Context $context, $id) {
|
$app->get('/users/:id', function(Context $context, $id) {
|
||||||
return ['user_id' => $id, 'name' => 'John Doe'];
|
return ['user_id' => $id, 'name' => 'John Doe'];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
@ -32,9 +32,9 @@ $app = new Web(debug: true);
|
|||||||
|
|
||||||
// Add global middleware
|
// Add global middleware
|
||||||
$app->use(function(Context $context, callable $next) {
|
$app->use(function(Context $context, callable $next) {
|
||||||
// Pre-processing
|
// Pre-processing
|
||||||
$next();
|
$next();
|
||||||
// Post-processing
|
// Post-processing
|
||||||
});
|
});
|
||||||
|
|
||||||
// Define routes
|
// Define routes
|
||||||
@ -47,17 +47,17 @@ $app->head('/path', $handler);
|
|||||||
|
|
||||||
// Route groups
|
// Route groups
|
||||||
$app->group('/api', function(Web $app) {
|
$app->group('/api', function(Web $app) {
|
||||||
$app->get('/users', $usersHandler);
|
$app->get('/users', $usersHandler);
|
||||||
$app->post('/users', $createUserHandler);
|
$app->post('/users', $createUserHandler);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Custom error handlers
|
// Custom error handlers
|
||||||
$app->setErrorHandler(404, function(Context $context) {
|
$app->setErrorHandler(404, function(Context $context) {
|
||||||
$context->json(['error' => 'Custom 404'], 404);
|
$context->json(['error' => 'Custom 404'], 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->setDefaultErrorHandler(function(Context $context, int $status, string $message, ?Exception $e) {
|
$app->setDefaultErrorHandler(function(Context $context, int $status, string $message, ?Exception $e) {
|
||||||
$context->json(['error' => $message, 'status' => $status], $status);
|
$context->json(['error' => $message, 'status' => $status], $status);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start the application
|
// Start the application
|
||||||
@ -78,9 +78,9 @@ $result = $router->lookup('GET', '/posts/123');
|
|||||||
// Returns: ['code' => 200, 'handler' => $handler, 'params' => ['123']]
|
// Returns: ['code' => 200, 'handler' => $handler, 'params' => ['123']]
|
||||||
|
|
||||||
// Route patterns
|
// Route patterns
|
||||||
'/users/:id' // matches /users/123, /users/abc
|
'/users/:id' // matches /users/123, /users/abc
|
||||||
'/posts/:slug/edit' // matches /posts/my-post/edit
|
'/posts/:slug/edit' // matches /posts/my-post/edit
|
||||||
'/' // matches root path
|
'/' // matches root path
|
||||||
```
|
```
|
||||||
|
|
||||||
## Context Examples
|
## Context Examples
|
||||||
@ -89,27 +89,27 @@ $result = $router->lookup('GET', '/posts/123');
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
// Get input from specific sources - explicit and predictable
|
// Get input from specific sources - explicit and predictable
|
||||||
$name = $context->input('name', 'default'); // POST data only
|
$name = $context->input('name', 'default'); // POST data only
|
||||||
$search = $context->query('search', ''); // Query params only
|
$search = $context->query('search', ''); // Query params only
|
||||||
$data = $context->jsonValue('data', []); // JSON body only
|
$data = $context->jsonValue('data', []); // JSON body only
|
||||||
$id = $context->param(0, 0); // First route parameter
|
$id = $context->param(0, 0); // First route parameter
|
||||||
|
|
||||||
// Examples with route parameters by index
|
// Examples with route parameters by index
|
||||||
// Route: /users/:userId/posts/:postId
|
// Route: /users/:userId/posts/:postId
|
||||||
// URL: /users/123/posts/456
|
// URL: /users/123/posts/456
|
||||||
|
|
||||||
$userId = $context->param(0); // "123" (first parameter)
|
$userId = $context->param(0); // "123" (first parameter)
|
||||||
$postId = $context->param(1); // "456" (second parameter)
|
$postId = $context->param(1); // "456" (second parameter)
|
||||||
|
|
||||||
// Examples with conflicting parameter names in different sources
|
// Examples with conflicting parameter names in different sources
|
||||||
// URL: /users/123?name=query_name
|
// URL: /users/123?name=query_name
|
||||||
// POST: name=post_name
|
// POST: name=post_name
|
||||||
// JSON: {"name": "json_name"}
|
// JSON: {"name": "json_name"}
|
||||||
|
|
||||||
$routeId = $context->param(0); // "123" from route (first param)
|
$routeId = $context->param(0); // "123" from route (first param)
|
||||||
$queryName = $context->query('name'); // "query_name" from URL
|
$queryName = $context->query('name'); // "query_name" from URL
|
||||||
$postName = $context->input('name'); // "post_name" from form
|
$postName = $context->input('name'); // "post_name" from form
|
||||||
$jsonName = $context->jsonValue('name'); // "json_name" from JSON
|
$jsonName = $context->jsonValue('name'); // "json_name" from JSON
|
||||||
|
|
||||||
// Get all input data merged from all sources (route params override all)
|
// Get all input data merged from all sources (route params override all)
|
||||||
$all = $context->all();
|
$all = $context->all();
|
||||||
@ -118,7 +118,7 @@ $data = $context->except(['password']); // all except specified keys
|
|||||||
|
|
||||||
// Check if input exists (checks all sources)
|
// Check if input exists (checks all sources)
|
||||||
if ($context->has('email')) {
|
if ($context->has('email')) {
|
||||||
// handle email
|
// handle email
|
||||||
}
|
}
|
||||||
|
|
||||||
// Request headers and cookies
|
// Request headers and cookies
|
||||||
@ -133,15 +133,15 @@ $contentType = $context->contentType();
|
|||||||
|
|
||||||
// Request checks
|
// Request checks
|
||||||
if ($context->expectsJson()) {
|
if ($context->expectsJson()) {
|
||||||
// return JSON response
|
// return JSON response
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($context->isAjax()) {
|
if ($context->isAjax()) {
|
||||||
// handle AJAX request
|
// handle AJAX request
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($context->isSecure()) {
|
if ($context->isSecure()) {
|
||||||
// HTTPS request
|
// HTTPS request
|
||||||
}
|
}
|
||||||
|
|
||||||
// URL helpers
|
// URL helpers
|
||||||
@ -150,7 +150,7 @@ $fullUrl = $context->fullUrl(); // URL with query string
|
|||||||
|
|
||||||
// Path matching
|
// Path matching
|
||||||
if ($context->is('api/*')) {
|
if ($context->is('api/*')) {
|
||||||
// matches API routes
|
// matches API routes
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -191,13 +191,13 @@ $user = $context->get('user');
|
|||||||
```php
|
```php
|
||||||
// Validate request data
|
// Validate request data
|
||||||
$validator = $context->validateRequest([
|
$validator = $context->validateRequest([
|
||||||
'email' => 'required|email',
|
'email' => 'required|email',
|
||||||
'name' => 'required|string|min:2'
|
'name' => 'required|string|min:2'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Validate any data
|
// Validate any data
|
||||||
$validator = $context->validate($data, [
|
$validator = $context->validate($data, [
|
||||||
'field' => 'required|string'
|
'field' => 'required|string'
|
||||||
], ['field.required' => 'Custom error message']);
|
], ['field.required' => 'Custom error message']);
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -207,23 +207,23 @@ $validator = $context->validate($data, [
|
|||||||
$request = new Request();
|
$request = new Request();
|
||||||
|
|
||||||
// Basic properties
|
// Basic properties
|
||||||
$request->method; // GET, POST, etc.
|
$request->method; // GET, POST, etc.
|
||||||
$request->uri; // /path?query=value
|
$request->uri; // /path?query=value
|
||||||
$request->path; // /path
|
$request->path; // /path
|
||||||
$request->query; // query=value
|
$request->query; // query=value
|
||||||
$request->body; // raw request body
|
$request->body; // raw request body
|
||||||
|
|
||||||
// Parsed data
|
// Parsed data
|
||||||
$request->queryParams; // parsed query parameters
|
$request->queryParams; // parsed query parameters
|
||||||
$request->postData; // parsed POST data
|
$request->postData; // parsed POST data
|
||||||
$request->params; // route parameters (set by router)
|
$request->params; // route parameters (set by router)
|
||||||
|
|
||||||
// Input methods
|
// Input methods
|
||||||
$value = $request->input('key', 'default'); // POST data only
|
$value = $request->input('key', 'default'); // POST data only
|
||||||
$value = $request->query('key', 'default'); // Query params only
|
$value = $request->query('key', 'default'); // Query params only
|
||||||
$value = $request->jsonValue('key', 'default'); // JSON body only
|
$value = $request->jsonValue('key', 'default'); // JSON body only
|
||||||
$value = $request->param(0, 'default'); // Route params by index
|
$value = $request->param(0, 'default'); // Route params by index
|
||||||
$all = $request->all(); // all input merged
|
$all = $request->all(); // all input merged
|
||||||
$subset = $request->only(['key1', 'key2']);
|
$subset = $request->only(['key1', 'key2']);
|
||||||
$subset = $request->except(['password']);
|
$subset = $request->except(['password']);
|
||||||
$exists = $request->has('key');
|
$exists = $request->has('key');
|
||||||
@ -257,22 +257,22 @@ $response = new Response();
|
|||||||
|
|
||||||
// Set status and headers
|
// Set status and headers
|
||||||
$response->status(201)
|
$response->status(201)
|
||||||
->header('Content-Type', 'application/json')
|
->header('Content-Type', 'application/json')
|
||||||
->header('X-Custom', 'value');
|
->header('X-Custom', 'value');
|
||||||
|
|
||||||
// Content methods
|
// Content methods
|
||||||
$response->json($data, 201); // JSON with status
|
$response->json($data, 201); // JSON with status
|
||||||
$response->text('Hello', 200); // Plain text
|
$response->text('Hello', 200); // Plain text
|
||||||
$response->html('<h1>Hi</h1>', 200); // HTML
|
$response->html('<h1>Hi</h1>', 200); // HTML
|
||||||
$response->redirect('/login', 302); // Redirect
|
$response->redirect('/login', 302); // Redirect
|
||||||
|
|
||||||
// Cookies
|
// Cookies
|
||||||
$response->cookie('name', 'value', [
|
$response->cookie('name', 'value', [
|
||||||
'expires' => time() + 3600,
|
'expires' => time() + 3600,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'secure' => true,
|
'secure' => true,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Strict'
|
'samesite' => 'Strict'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Manual building
|
// Manual building
|
||||||
@ -307,8 +307,8 @@ $valid = $session->validateCsrf($userToken);
|
|||||||
|
|
||||||
// Session management
|
// Session management
|
||||||
$session->regenerate(); // Regenerate session ID
|
$session->regenerate(); // Regenerate session ID
|
||||||
$session->destroy(); // Destroy session completely
|
$session->destroy(); // Destroy session completely
|
||||||
$session->clear(); // Clear data but keep session active
|
$session->clear(); // Clear data but keep session active
|
||||||
$all = $session->all(); // Get all session data
|
$all = $session->all(); // Get all session data
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -319,27 +319,27 @@ $validator = new Validator();
|
|||||||
|
|
||||||
// Validate data
|
// Validate data
|
||||||
$isValid = $validator->validate($data, [
|
$isValid = $validator->validate($data, [
|
||||||
'email' => 'required|email',
|
'email' => 'required|email',
|
||||||
'password' => 'required|min:8',
|
'password' => 'required|min:8',
|
||||||
'age' => 'integer|between:18,120',
|
'age' => 'integer|between:18,120',
|
||||||
'tags' => 'array',
|
'tags' => 'array',
|
||||||
'website' => 'url',
|
'website' => 'url',
|
||||||
'role' => 'in:admin,user,moderator'
|
'role' => 'in:admin,user,moderator'
|
||||||
], [
|
], [
|
||||||
'email.required' => 'Email is mandatory',
|
'email.required' => 'Email is mandatory',
|
||||||
'password.min' => 'Password must be at least 8 characters'
|
'password.min' => 'Password must be at least 8 characters'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Check results
|
// Check results
|
||||||
if ($validator->failed()) {
|
if ($validator->failed()) {
|
||||||
$errors = $validator->errors();
|
$errors = $validator->errors();
|
||||||
$firstError = $validator->firstError();
|
$firstError = $validator->firstError();
|
||||||
$fieldError = $validator->firstError('email');
|
$fieldError = $validator->firstError('email');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom validation rules
|
// Custom validation rules
|
||||||
Validator::extend('custom', function($value, $parameters, $data) {
|
Validator::extend('custom', function($value, $parameters, $data) {
|
||||||
return $value === 'custom_value';
|
return $value === 'custom_value';
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -347,9 +347,9 @@ Validator::extend('custom', function($value, $parameters, $data) {
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
$auth = new Auth($session, [
|
$auth = new Auth($session, [
|
||||||
'cookie_name' => 'remember_token',
|
'cookie_name' => 'remember_token',
|
||||||
'cookie_lifetime' => 2592000, // 30 days
|
'cookie_lifetime' => 2592000, // 30 days
|
||||||
'cookie_secure' => true
|
'cookie_secure' => true
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
@ -376,10 +376,10 @@ $auth->clear(); // Clear all auth data
|
|||||||
|
|
||||||
```php
|
```php
|
||||||
$user = new User([
|
$user = new User([
|
||||||
'id' => 1,
|
'id' => 1,
|
||||||
'username' => 'john',
|
'username' => 'john',
|
||||||
'email' => 'john@example.com',
|
'email' => 'john@example.com',
|
||||||
'role' => 'admin'
|
'role' => 'admin'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Properties
|
// Properties
|
||||||
@ -391,8 +391,8 @@ $user->role;
|
|||||||
// Methods
|
// Methods
|
||||||
$identifier = $user->getIdentifier(); // email or username
|
$identifier = $user->getIdentifier(); // email or username
|
||||||
$id = $user->getId();
|
$id = $user->getId();
|
||||||
$array = $user->toArray(); // with all data
|
$array = $user->toArray(); // with all data
|
||||||
$safe = $user->toSafeArray(); // without sensitive data
|
$safe = $user->toSafeArray(); // without sensitive data
|
||||||
```
|
```
|
||||||
|
|
||||||
## Auth Middleware Examples
|
## Auth Middleware Examples
|
||||||
@ -430,11 +430,11 @@ $errorHandler = new ErrorHandler(debug: true);
|
|||||||
|
|
||||||
// Register custom error handlers
|
// Register custom error handlers
|
||||||
$errorHandler->register(404, function(Context $context, int $status, string $message) {
|
$errorHandler->register(404, function(Context $context, int $status, string $message) {
|
||||||
$context->json(['error' => 'Custom not found'], 404);
|
$context->json(['error' => 'Custom not found'], 404);
|
||||||
});
|
});
|
||||||
|
|
||||||
$errorHandler->setDefaultHandler(function(Context $context, int $status, string $message, ?Exception $e) {
|
$errorHandler->setDefaultHandler(function(Context $context, int $status, string $message, ?Exception $e) {
|
||||||
$context->json(['error' => $message, 'code' => $status], $status);
|
$context->json(['error' => $message, 'code' => $status], $status);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle errors manually
|
// Handle errors manually
|
||||||
@ -451,46 +451,46 @@ throw new ValidationException($validator->errors(), 'Validation failed');
|
|||||||
```php
|
```php
|
||||||
// Basic middleware
|
// Basic middleware
|
||||||
$app->use(function(Context $context, callable $next) {
|
$app->use(function(Context $context, callable $next) {
|
||||||
// Pre-processing
|
// Pre-processing
|
||||||
$start = microtime(true);
|
$start = microtime(true);
|
||||||
|
|
||||||
$next(); // Call next middleware/handler
|
$next(); // Call next middleware/handler
|
||||||
|
|
||||||
// Post-processing
|
// Post-processing
|
||||||
$duration = microtime(true) - $start;
|
$duration = microtime(true) - $start;
|
||||||
$context->setHeader('X-Response-Time', $duration . 'ms');
|
$context->setHeader('X-Response-Time', $duration . 'ms');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Conditional middleware
|
// Conditional middleware
|
||||||
$app->use(function(Context $context, callable $next) {
|
$app->use(function(Context $context, callable $next) {
|
||||||
if ($context->is('api/*')) {
|
if ($context->is('api/*')) {
|
||||||
$context->setHeader('Content-Type', 'application/json');
|
$context->setHeader('Content-Type', 'application/json');
|
||||||
}
|
}
|
||||||
$next();
|
$next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Logging middleware
|
// Logging middleware
|
||||||
$app->use(function(Context $context, callable $next) {
|
$app->use(function(Context $context, callable $next) {
|
||||||
$method = $context->request->method;
|
$method = $context->request->method;
|
||||||
$path = $context->request->path;
|
$path = $context->request->path;
|
||||||
$ip = $context->ip();
|
$ip = $context->ip();
|
||||||
|
|
||||||
error_log("[$method] $path from $ip");
|
error_log("[$method] $path from $ip");
|
||||||
$next();
|
$next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// CORS middleware
|
// CORS middleware
|
||||||
$app->use(function(Context $context, callable $next) {
|
$app->use(function(Context $context, callable $next) {
|
||||||
$context->setHeader('Access-Control-Allow-Origin', '*');
|
$context->setHeader('Access-Control-Allow-Origin', '*');
|
||||||
$context->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
$context->setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||||
$context->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
$context->setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||||
|
|
||||||
if ($context->request->method === 'OPTIONS') {
|
if ($context->request->method === 'OPTIONS') {
|
||||||
$context->status(200)->send();
|
$context->status(200)->send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$next();
|
$next();
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -499,62 +499,62 @@ $app->use(function(Context $context, callable $next) {
|
|||||||
```php
|
```php
|
||||||
// Simple handler
|
// Simple handler
|
||||||
$app->get('/hello', function(Context $context) {
|
$app->get('/hello', function(Context $context) {
|
||||||
return 'Hello World!';
|
return 'Hello World!';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handler with parameters
|
// Handler with parameters
|
||||||
$app->get('/users/:id/posts/:slug', function(Context $context, $userId, $slug) {
|
$app->get('/users/:id/posts/:slug', function(Context $context, $userId, $slug) {
|
||||||
// Parameters are passed as function arguments in order
|
// Parameters are passed as function arguments in order
|
||||||
// Or access via index: $userId = $context->param(0), $slug = $context->param(1)
|
// Or access via index: $userId = $context->param(0), $slug = $context->param(1)
|
||||||
return [
|
return [
|
||||||
'user_id' => $userId,
|
'user_id' => $userId,
|
||||||
'post_slug' => $slug,
|
'post_slug' => $slug,
|
||||||
'data' => $context->all()
|
'data' => $context->all()
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handler return types
|
// Handler return types
|
||||||
$app->get('/json', fn($context) => ['key' => 'value']); // Auto JSON
|
$app->get('/json', fn($context) => ['key' => 'value']); // Auto JSON
|
||||||
$app->get('/text', fn($context) => 'Plain text'); // Auto text
|
$app->get('/text', fn($context) => 'Plain text'); // Auto text
|
||||||
$app->get('/response', fn($context) => $context->json([])); // Manual response
|
$app->get('/response', fn($context) => $context->json([])); // Manual response
|
||||||
|
|
||||||
// Complex handler with validation
|
// Complex handler with validation
|
||||||
$app->post('/users', function(Context $context) {
|
$app->post('/users', function(Context $context) {
|
||||||
$context->validateRequest([
|
$context->validateRequest([
|
||||||
'name' => 'required|string|min:2',
|
'name' => 'required|string|min:2',
|
||||||
'email' => 'required|email',
|
'email' => 'required|email',
|
||||||
'age' => 'integer|min:18'
|
'age' => 'integer|min:18'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$userData = $context->only(['name', 'email', 'age']);
|
$userData = $context->only(['name', 'email', 'age']);
|
||||||
|
|
||||||
// Save user logic here
|
// Save user logic here
|
||||||
$userId = saveUser($userData);
|
$userId = saveUser($userData);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'message' => 'User created successfully',
|
'message' => 'User created successfully',
|
||||||
'user_id' => $userId
|
'user_id' => $userId
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// File upload handler
|
// File upload handler
|
||||||
$app->post('/upload', function(Context $context) {
|
$app->post('/upload', function(Context $context) {
|
||||||
if (!isset($_FILES['file'])) {
|
if (!isset($_FILES['file'])) {
|
||||||
$context->error(400, 'No file uploaded');
|
$context->error(400, 'No file uploaded');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$file = $_FILES['file'];
|
$file = $_FILES['file'];
|
||||||
|
|
||||||
if ($file['error'] !== UPLOAD_ERR_OK) {
|
if ($file['error'] !== UPLOAD_ERR_OK) {
|
||||||
$context->error(400, 'Upload failed');
|
$context->error(400, 'Upload failed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle file upload logic
|
// Handle file upload logic
|
||||||
$filename = handleFileUpload($file);
|
$filename = handleFileUpload($file);
|
||||||
|
|
||||||
return ['filename' => $filename];
|
return ['filename' => $filename];
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -574,142 +574,142 @@ $authMiddleware = new AuthMiddleware($auth);
|
|||||||
|
|
||||||
// Global middleware
|
// Global middleware
|
||||||
$app->use(function(Context $context, callable $next) {
|
$app->use(function(Context $context, callable $next) {
|
||||||
$context->setHeader('X-Framework', 'Web');
|
$context->setHeader('X-Framework', 'Web');
|
||||||
$next();
|
$next();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auth routes
|
// Auth routes
|
||||||
$app->post('/login', function(Context $context) use ($auth) {
|
$app->post('/login', function(Context $context) use ($auth) {
|
||||||
$context->validateRequest([
|
$context->validateRequest([
|
||||||
'email' => 'required|email',
|
'email' => 'required|email',
|
||||||
'password' => 'required|min:6'
|
'password' => 'required|min:6'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$email = $context->input('email');
|
$email = $context->input('email');
|
||||||
$password = $context->input('password');
|
$password = $context->input('password');
|
||||||
|
|
||||||
// Verify credentials externally, then login
|
// Verify credentials externally, then login
|
||||||
if (verifyCredentials($email, $password)) {
|
if (verifyCredentials($email, $password)) {
|
||||||
$userData = getUserData($email);
|
$userData = getUserData($email);
|
||||||
$auth->login($userData, $context->input('remember'));
|
$auth->login($userData, $context->input('remember'));
|
||||||
return ['message' => 'Logged in successfully'];
|
return ['message' => 'Logged in successfully'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$context->error(401, 'Invalid credentials');
|
$context->error(401, 'Invalid credentials');
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->post('/logout', function(Context $context) use ($auth) {
|
$app->post('/logout', function(Context $context) use ($auth) {
|
||||||
$auth->logout();
|
$auth->logout();
|
||||||
return ['message' => 'Logged out'];
|
return ['message' => 'Logged out'];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Protected routes
|
// Protected routes
|
||||||
$app->group('/api', function(Web $app) use ($authMiddleware) {
|
$app->group('/api', function(Web $app) use ($authMiddleware) {
|
||||||
$app->use($authMiddleware->requireAuth());
|
$app->use($authMiddleware->requireAuth());
|
||||||
$app->use($authMiddleware->rateLimit(100, 1));
|
$app->use($authMiddleware->rateLimit(100, 1));
|
||||||
|
|
||||||
$app->get('/profile', function(Context $context) {
|
$app->get('/profile', function(Context $context) {
|
||||||
return $context->get('user')->toSafeArray();
|
return $context->get('user')->toSafeArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->put('/profile', function(Context $context) {
|
$app->put('/profile', function(Context $context) {
|
||||||
$context->validateRequest([
|
$context->validateRequest([
|
||||||
'name' => 'required|string|min:2',
|
'name' => 'required|string|min:2',
|
||||||
'email' => 'required|email'
|
'email' => 'required|email'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update profile logic here
|
// Update profile logic here
|
||||||
return ['message' => 'Profile updated'];
|
return ['message' => 'Profile updated'];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->get('/posts', function(Context $context) {
|
$app->get('/posts', function(Context $context) {
|
||||||
$page = $context->input('page', 1);
|
$page = $context->input('page', 1);
|
||||||
$limit = $context->input('limit', 10);
|
$limit = $context->input('limit', 10);
|
||||||
|
|
||||||
// Get posts with pagination
|
// Get posts with pagination
|
||||||
$posts = getPosts($page, $limit);
|
$posts = getPosts($page, $limit);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'posts' => $posts,
|
'posts' => $posts,
|
||||||
'pagination' => [
|
'pagination' => [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'limit' => $limit
|
'limit' => $limit
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->post('/posts', function(Context $context) {
|
$app->post('/posts', function(Context $context) {
|
||||||
$context->validateRequest([
|
$context->validateRequest([
|
||||||
'title' => 'required|string|min:5',
|
'title' => 'required|string|min:5',
|
||||||
'content' => 'required|string|min:10',
|
'content' => 'required|string|min:10',
|
||||||
'tags' => 'array'
|
'tags' => 'array'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$postData = $context->only(['title', 'content', 'tags']);
|
$postData = $context->only(['title', 'content', 'tags']);
|
||||||
$postData['user_id'] = $context->get('user')->id;
|
$postData['user_id'] = $context->get('user')->id;
|
||||||
|
|
||||||
$postId = createPost($postData);
|
$postId = createPost($postData);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'message' => 'Post created successfully',
|
'message' => 'Post created successfully',
|
||||||
'post_id' => $postId
|
'post_id' => $postId
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Admin routes
|
// Admin routes
|
||||||
$app->group('/admin', function(Web $app) use ($authMiddleware) {
|
$app->group('/admin', function(Web $app) use ($authMiddleware) {
|
||||||
$app->use($authMiddleware->requireRole('admin'));
|
$app->use($authMiddleware->requireRole('admin'));
|
||||||
|
|
||||||
$app->get('/users', function(Context $context) {
|
$app->get('/users', function(Context $context) {
|
||||||
return ['users' => getAllUsers()];
|
return ['users' => getAllUsers()];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->delete('/users/:id', function(Context $context, $userId) {
|
$app->delete('/users/:id', function(Context $context, $userId) {
|
||||||
deleteUser($userId);
|
deleteUser($userId);
|
||||||
return ['message' => 'User deleted successfully'];
|
return ['message' => 'User deleted successfully'];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->get('/stats', function(Context $context) {
|
$app->get('/stats', function(Context $context) {
|
||||||
return [
|
return [
|
||||||
'total_users' => getUserCount(),
|
'total_users' => getUserCount(),
|
||||||
'total_posts' => getPostCount(),
|
'total_posts' => getPostCount(),
|
||||||
'active_sessions' => getActiveSessionCount()
|
'active_sessions' => getActiveSessionCount()
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Public API routes
|
// Public API routes
|
||||||
$app->group('/public', function(Web $app) {
|
$app->group('/public', function(Web $app) {
|
||||||
$app->get('/posts', function(Context $context) {
|
$app->get('/posts', function(Context $context) {
|
||||||
$posts = getPublicPosts();
|
$posts = getPublicPosts();
|
||||||
return ['posts' => $posts];
|
return ['posts' => $posts];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->get('/posts/:id', function(Context $context, $postId) {
|
$app->get('/posts/:id', function(Context $context, $postId) {
|
||||||
$post = getPublicPost($postId);
|
$post = getPublicPost($postId);
|
||||||
if (!$post) {
|
if (!$post) {
|
||||||
$context->error(404, 'Post not found');
|
$context->error(404, 'Post not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
return ['post' => $post];
|
return ['post' => $post];
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Error handlers
|
// Error handlers
|
||||||
$app->setErrorHandler(404, function(Context $context) {
|
$app->setErrorHandler(404, function(Context $context) {
|
||||||
if ($context->expectsJson()) {
|
if ($context->expectsJson()) {
|
||||||
$context->json(['error' => 'Endpoint not found'], 404);
|
$context->json(['error' => 'Endpoint not found'], 404);
|
||||||
} else {
|
} else {
|
||||||
$context->html('<h1>Page Not Found</h1>', 404);
|
$context->html('<h1>Page Not Found</h1>', 404);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->setErrorHandler(429, function(Context $context) {
|
$app->setErrorHandler(429, function(Context $context) {
|
||||||
$context->json([
|
$context->json([
|
||||||
'error' => 'Rate limit exceeded',
|
'error' => 'Rate limit exceeded',
|
||||||
'message' => 'Please slow down your requests'
|
'message' => 'Please slow down your requests'
|
||||||
], 429);
|
], 429);
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
@ -721,21 +721,21 @@ $app->run();
|
|||||||
```php
|
```php
|
||||||
// Simple test setup
|
// Simple test setup
|
||||||
function testRoute($method, $path, $data = []) {
|
function testRoute($method, $path, $data = []) {
|
||||||
$_SERVER['REQUEST_METHOD'] = $method;
|
$_SERVER['REQUEST_METHOD'] = $method;
|
||||||
$_SERVER['REQUEST_URI'] = $path;
|
$_SERVER['REQUEST_URI'] = $path;
|
||||||
|
|
||||||
if ($method === 'POST' && $data) {
|
if ($method === 'POST' && $data) {
|
||||||
$_POST = $data;
|
$_POST = $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
$app = new Web();
|
$app = new Web();
|
||||||
// Setup your routes...
|
// Setup your routes...
|
||||||
|
|
||||||
ob_start();
|
ob_start();
|
||||||
$app->run();
|
$app->run();
|
||||||
$output = ob_get_clean();
|
$output = ob_get_clean();
|
||||||
|
|
||||||
return $output;
|
return $output;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test examples
|
// Test examples
|
||||||
|
|||||||
728
ErrorHandler.php
728
ErrorHandler.php
@ -5,404 +5,404 @@
|
|||||||
*/
|
*/
|
||||||
class ErrorHandler
|
class ErrorHandler
|
||||||
{
|
{
|
||||||
private array $handlers = [];
|
private array $handlers = [];
|
||||||
private $defaultHandler = null;
|
private $defaultHandler = null;
|
||||||
private bool $debug = false;
|
private bool $debug = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __construct creates a new ErrorHandler
|
* __construct creates a new ErrorHandler
|
||||||
*/
|
*/
|
||||||
public function __construct(bool $debug = false)
|
public function __construct(bool $debug = false)
|
||||||
{
|
{
|
||||||
$this->debug = $debug;
|
$this->debug = $debug;
|
||||||
$this->registerDefaultHandlers();
|
$this->registerDefaultHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* register registers a custom error handler for a specific status code
|
* register registers a custom error handler for a specific status code
|
||||||
*/
|
*/
|
||||||
public function register(int $status, callable $handler): void
|
public function register(int $status, callable $handler): void
|
||||||
{
|
{
|
||||||
$this->handlers[$status] = $handler;
|
$this->handlers[$status] = $handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* setDefaultHandler sets the default error handler for unregistered status codes
|
* setDefaultHandler sets the default error handler for unregistered status codes
|
||||||
*/
|
*/
|
||||||
public function setDefaultHandler(callable $handler): void
|
public function setDefaultHandler(callable $handler): void
|
||||||
{
|
{
|
||||||
$this->defaultHandler = $handler;
|
$this->defaultHandler = $handler;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handle handles an error with the appropriate handler
|
* handle handles an error with the appropriate handler
|
||||||
*/
|
*/
|
||||||
public function handle(Context $context, int $status, string $message = '', ?Exception $exception = null): void
|
public function handle(Context $context, int $status, string $message = '', ?Exception $exception = null): void
|
||||||
{
|
{
|
||||||
if (isset($this->handlers[$status])) {
|
if (isset($this->handlers[$status])) {
|
||||||
$handler = $this->handlers[$status];
|
$handler = $this->handlers[$status];
|
||||||
$handler($context, $status, $message, $exception);
|
$handler($context, $status, $message, $exception);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->defaultHandler) {
|
if ($this->defaultHandler) {
|
||||||
($this->defaultHandler)($context, $status, $message, $exception);
|
($this->defaultHandler)($context, $status, $message, $exception);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->renderDefaultError($context, $status, $message, $exception);
|
$this->renderDefaultError($context, $status, $message, $exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* handleException handles uncaught exceptions
|
* handleException handles uncaught exceptions
|
||||||
*/
|
*/
|
||||||
public function handleException(Context $context, Exception $exception): void
|
public function handleException(Context $context, Exception $exception): void
|
||||||
{
|
{
|
||||||
$status = $this->getStatusFromException($exception);
|
$status = $this->getStatusFromException($exception);
|
||||||
$message = $this->debug ? $exception->getMessage() : $this->getDefaultMessage($status);
|
$message = $this->debug ? $exception->getMessage() : $this->getDefaultMessage($status);
|
||||||
|
|
||||||
if ($this->debug) {
|
if ($this->debug) {
|
||||||
error_log($exception->getMessage() . "\n" . $exception->getTraceAsString());
|
error_log($exception->getMessage() . "\n" . $exception->getTraceAsString());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->handle($context, $status, $message, $exception);
|
$this->handle($context, $status, $message, $exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getStatusFromException determines HTTP status from exception type
|
* getStatusFromException determines HTTP status from exception type
|
||||||
*/
|
*/
|
||||||
private function getStatusFromException(Exception $exception): int
|
private function getStatusFromException(Exception $exception): int
|
||||||
{
|
{
|
||||||
if ($exception instanceof HttpException) {
|
if ($exception instanceof HttpException) {
|
||||||
return $exception->getStatusCode();
|
return $exception->getStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
return match(get_class($exception)) {
|
return match(get_class($exception)) {
|
||||||
'InvalidArgumentException' => 400,
|
'InvalidArgumentException' => 400,
|
||||||
'UnauthorizedException' => 401,
|
'UnauthorizedException' => 401,
|
||||||
'ForbiddenException' => 403,
|
'ForbiddenException' => 403,
|
||||||
'NotFoundException' => 404,
|
'NotFoundException' => 404,
|
||||||
'MethodNotAllowedException' => 405,
|
'MethodNotAllowedException' => 405,
|
||||||
'ValidationException' => 422,
|
'ValidationException' => 422,
|
||||||
default => 500
|
default => 500
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* registerDefaultHandlers registers built-in error handlers
|
* registerDefaultHandlers registers built-in error handlers
|
||||||
*/
|
*/
|
||||||
private function registerDefaultHandlers(): void
|
private function registerDefaultHandlers(): void
|
||||||
{
|
{
|
||||||
// 404 Not Found
|
// 404 Not Found
|
||||||
$this->register(404, function(Context $context, int $status, string $message) {
|
$this->register(404, function(Context $context, int $status, string $message) {
|
||||||
$accept = $context->request->header('accept') ?? '';
|
$accept = $context->request->header('accept') ?? '';
|
||||||
|
|
||||||
if (str_contains($accept, 'application/json')) {
|
if (str_contains($accept, 'application/json')) {
|
||||||
$context->json(['error' => $message ?: 'Not Found'], 404);
|
$context->json(['error' => $message ?: 'Not Found'], 404);
|
||||||
} else {
|
} else {
|
||||||
$html = $this->render404Page($message);
|
$html = $this->render404Page($message);
|
||||||
$context->html($html, 404);
|
$context->html($html, 404);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 500 Internal Server Error
|
// 500 Internal Server Error
|
||||||
$this->register(500, function(Context $context, int $status, string $message, ?Exception $exception) {
|
$this->register(500, function(Context $context, int $status, string $message, ?Exception $exception) {
|
||||||
$accept = $context->request->header('accept') ?? '';
|
$accept = $context->request->header('accept') ?? '';
|
||||||
|
|
||||||
if (str_contains($accept, 'application/json')) {
|
if (str_contains($accept, 'application/json')) {
|
||||||
$response = ['error' => $message ?: 'Internal Server Error'];
|
$response = ['error' => $message ?: 'Internal Server Error'];
|
||||||
if ($this->debug && $exception) {
|
if ($this->debug && $exception) {
|
||||||
$response['trace'] = $exception->getTrace();
|
$response['trace'] = $exception->getTrace();
|
||||||
}
|
}
|
||||||
$context->json($response, 500);
|
$context->json($response, 500);
|
||||||
} else {
|
} else {
|
||||||
$html = $this->render500Page($message, $exception);
|
$html = $this->render500Page($message, $exception);
|
||||||
$context->html($html, 500);
|
$context->html($html, 500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* renderDefaultError renders a generic error response
|
* renderDefaultError renders a generic error response
|
||||||
*/
|
*/
|
||||||
private function renderDefaultError(Context $context, int $status, string $message, ?Exception $exception): void
|
private function renderDefaultError(Context $context, int $status, string $message, ?Exception $exception): void
|
||||||
{
|
{
|
||||||
$message = $message ?: $this->getDefaultMessage($status);
|
$message = $message ?: $this->getDefaultMessage($status);
|
||||||
$accept = $context->request->header('accept') ?? '';
|
$accept = $context->request->header('accept') ?? '';
|
||||||
|
|
||||||
if (str_contains($accept, 'application/json')) {
|
if (str_contains($accept, 'application/json')) {
|
||||||
$response = ['error' => $message];
|
$response = ['error' => $message];
|
||||||
if ($this->debug && $exception) {
|
if ($this->debug && $exception) {
|
||||||
$response['trace'] = $exception->getTrace();
|
$response['trace'] = $exception->getTrace();
|
||||||
}
|
}
|
||||||
$context->json($response, $status);
|
$context->json($response, $status);
|
||||||
} else {
|
} else {
|
||||||
$html = $this->renderErrorPage($status, $message, $exception);
|
$html = $this->renderErrorPage($status, $message, $exception);
|
||||||
$context->html($html, $status);
|
$context->html($html, $status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getDefaultMessage gets default message for status code
|
* getDefaultMessage gets default message for status code
|
||||||
*/
|
*/
|
||||||
private function getDefaultMessage(int $status): string
|
private function getDefaultMessage(int $status): string
|
||||||
{
|
{
|
||||||
return match($status) {
|
return match($status) {
|
||||||
400 => 'Bad Request',
|
400 => 'Bad Request',
|
||||||
401 => 'Unauthorized',
|
401 => 'Unauthorized',
|
||||||
403 => 'Forbidden',
|
403 => 'Forbidden',
|
||||||
404 => 'Not Found',
|
404 => 'Not Found',
|
||||||
405 => 'Method Not Allowed',
|
405 => 'Method Not Allowed',
|
||||||
422 => 'Unprocessable Entity',
|
422 => 'Unprocessable Entity',
|
||||||
429 => 'Too Many Requests',
|
429 => 'Too Many Requests',
|
||||||
500 => 'Internal Server Error',
|
500 => 'Internal Server Error',
|
||||||
502 => 'Bad Gateway',
|
502 => 'Bad Gateway',
|
||||||
503 => 'Service Unavailable',
|
503 => 'Service Unavailable',
|
||||||
default => 'Error'
|
default => 'Error'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* render404Page renders a 404 error page
|
* render404Page renders a 404 error page
|
||||||
*/
|
*/
|
||||||
private function render404Page(string $message): string
|
private function render404Page(string $message): string
|
||||||
{
|
{
|
||||||
$message = htmlspecialchars($message ?: 'Not Found');
|
$message = htmlspecialchars($message ?: 'Not Found');
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>404 - Not Found</title>
|
<title>404 - Not Found</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.error-container {
|
.error-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
a {
|
a {
|
||||||
color: #3498db;
|
color: #3498db;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
a:hover {
|
a:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<h2>{$message}</h2>
|
<h2>{$message}</h2>
|
||||||
<p>The page you are looking for could not be found.</p>
|
<p>The page you are looking for could not be found.</p>
|
||||||
<a href="/">Go to Homepage</a>
|
<a href="/">Go to Homepage</a>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* render500Page renders a 500 error page
|
* render500Page renders a 500 error page
|
||||||
*/
|
*/
|
||||||
private function render500Page(string $message, ?Exception $exception): string
|
private function render500Page(string $message, ?Exception $exception): string
|
||||||
{
|
{
|
||||||
$message = htmlspecialchars($message ?: 'Internal Server Error');
|
$message = htmlspecialchars($message ?: 'Internal Server Error');
|
||||||
$debugInfo = '';
|
$debugInfo = '';
|
||||||
|
|
||||||
if ($this->debug && $exception) {
|
if ($this->debug && $exception) {
|
||||||
$exceptionClass = get_class($exception);
|
$exceptionClass = get_class($exception);
|
||||||
$file = htmlspecialchars($exception->getFile());
|
$file = htmlspecialchars($exception->getFile());
|
||||||
$line = $exception->getLine();
|
$line = $exception->getLine();
|
||||||
$trace = htmlspecialchars($exception->getTraceAsString());
|
$trace = htmlspecialchars($exception->getTraceAsString());
|
||||||
|
|
||||||
$debugInfo = <<<HTML
|
$debugInfo = <<<HTML
|
||||||
<div class="debug-info">
|
<div class="debug-info">
|
||||||
<h3>Debug Information</h3>
|
<h3>Debug Information</h3>
|
||||||
<p><strong>Exception:</strong> {$exceptionClass}</p>
|
<p><strong>Exception:</strong> {$exceptionClass}</p>
|
||||||
<p><strong>File:</strong> {$file}:{$line}</p>
|
<p><strong>File:</strong> {$file}:{$line}</p>
|
||||||
<pre>{$trace}</pre>
|
<pre>{$trace}</pre>
|
||||||
</div>
|
</div>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>500 - Internal Server Error</title>
|
<title>500 - Internal Server Error</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.error-container {
|
.error-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
p {
|
p {
|
||||||
color: #666;
|
color: #666;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
.debug-info {
|
.debug-info {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.debug-info h3 {
|
.debug-info h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
}
|
}
|
||||||
.debug-info pre {
|
.debug-info pre {
|
||||||
background: #f8f8f8;
|
background: #f8f8f8;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<h1>500</h1>
|
<h1>500</h1>
|
||||||
<h2>{$message}</h2>
|
<h2>{$message}</h2>
|
||||||
<p>Something went wrong on our end. Please try again later.</p>
|
<p>Something went wrong on our end. Please try again later.</p>
|
||||||
{$debugInfo}
|
{$debugInfo}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* renderErrorPage renders a generic error page
|
* renderErrorPage renders a generic error page
|
||||||
*/
|
*/
|
||||||
private function renderErrorPage(int $status, string $message, ?Exception $exception): string
|
private function renderErrorPage(int $status, string $message, ?Exception $exception): string
|
||||||
{
|
{
|
||||||
$message = htmlspecialchars($message);
|
$message = htmlspecialchars($message);
|
||||||
$debugInfo = '';
|
$debugInfo = '';
|
||||||
|
|
||||||
if ($this->debug && $exception) {
|
if ($this->debug && $exception) {
|
||||||
$exceptionClass = get_class($exception);
|
$exceptionClass = get_class($exception);
|
||||||
$file = htmlspecialchars($exception->getFile());
|
$file = htmlspecialchars($exception->getFile());
|
||||||
$line = $exception->getLine();
|
$line = $exception->getLine();
|
||||||
$trace = htmlspecialchars($exception->getTraceAsString());
|
$trace = htmlspecialchars($exception->getTraceAsString());
|
||||||
|
|
||||||
$debugInfo = <<<HTML
|
$debugInfo = <<<HTML
|
||||||
<div class="debug-info">
|
<div class="debug-info">
|
||||||
<h3>Debug Information</h3>
|
<h3>Debug Information</h3>
|
||||||
<p><strong>Exception:</strong> {$exceptionClass}</p>
|
<p><strong>Exception:</strong> {$exceptionClass}</p>
|
||||||
<p><strong>File:</strong> {$file}:{$line}</p>
|
<p><strong>File:</strong> {$file}:{$line}</p>
|
||||||
<pre>{$trace}</pre>
|
<pre>{$trace}</pre>
|
||||||
</div>
|
</div>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <<<HTML
|
return <<<HTML
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{$status} - {$message}</title>
|
<title>{$status} - {$message}</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
background: #f5f5f5;
|
background: #f5f5f5;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
.error-container {
|
.error-container {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 2rem;
|
padding: 2rem;
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
}
|
}
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 6rem;
|
font-size: 6rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
}
|
}
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
}
|
}
|
||||||
.debug-info {
|
.debug-info {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
.debug-info h3 {
|
.debug-info h3 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #e74c3c;
|
color: #e74c3c;
|
||||||
}
|
}
|
||||||
.debug-info pre {
|
.debug-info pre {
|
||||||
background: #f8f8f8;
|
background: #f8f8f8;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="error-container">
|
<div class="error-container">
|
||||||
<h1>{$status}</h1>
|
<h1>{$status}</h1>
|
||||||
<h2>{$message}</h2>
|
<h2>{$message}</h2>
|
||||||
{$debugInfo}
|
{$debugInfo}
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
HTML;
|
HTML;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -410,18 +410,18 @@ HTML;
|
|||||||
*/
|
*/
|
||||||
class HttpException extends Exception
|
class HttpException extends Exception
|
||||||
{
|
{
|
||||||
protected int $statusCode;
|
protected int $statusCode;
|
||||||
|
|
||||||
public function __construct(int $statusCode, string $message = '', int $code = 0, Exception $previous = null)
|
public function __construct(int $statusCode, string $message = '', int $code = 0, Exception $previous = null)
|
||||||
{
|
{
|
||||||
$this->statusCode = $statusCode;
|
$this->statusCode = $statusCode;
|
||||||
parent::__construct($message, $code, $previous);
|
parent::__construct($message, $code, $previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getStatusCode(): int
|
public function getStatusCode(): int
|
||||||
{
|
{
|
||||||
return $this->statusCode;
|
return $this->statusCode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -429,16 +429,16 @@ class HttpException extends Exception
|
|||||||
*/
|
*/
|
||||||
class ValidationException extends HttpException
|
class ValidationException extends HttpException
|
||||||
{
|
{
|
||||||
private array $errors;
|
private array $errors;
|
||||||
|
|
||||||
public function __construct(array $errors, string $message = 'Validation failed')
|
public function __construct(array $errors, string $message = 'Validation failed')
|
||||||
{
|
{
|
||||||
$this->errors = $errors;
|
$this->errors = $errors;
|
||||||
parent::__construct(422, $message);
|
parent::__construct(422, $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getErrors(): array
|
public function getErrors(): array
|
||||||
{
|
{
|
||||||
return $this->errors;
|
return $this->errors;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,36 +4,36 @@
|
|||||||
* HTTPMethod represents an HTTP method
|
* HTTPMethod represents an HTTP method
|
||||||
*/
|
*/
|
||||||
enum HTTPMethod: string {
|
enum HTTPMethod: string {
|
||||||
case GET = 'GET';
|
case GET = 'GET';
|
||||||
case POST = 'POST';
|
case POST = 'POST';
|
||||||
case PUT = 'PUT';
|
case PUT = 'PUT';
|
||||||
case DELETE = 'DELETE';
|
case DELETE = 'DELETE';
|
||||||
case PATCH = 'PATCH';
|
case PATCH = 'PATCH';
|
||||||
case OPTIONS = 'OPTIONS';
|
case OPTIONS = 'OPTIONS';
|
||||||
case HEAD = 'HEAD';
|
case HEAD = 'HEAD';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fromString converts a string to an HTTPMethod
|
* fromString converts a string to an HTTPMethod
|
||||||
*/
|
*/
|
||||||
public static function fromString(string $method): self
|
public static function fromString(string $method): self
|
||||||
{
|
{
|
||||||
return match(strtoupper($method)) {
|
return match(strtoupper($method)) {
|
||||||
'GET' => self::GET,
|
'GET' => self::GET,
|
||||||
'POST' => self::POST,
|
'POST' => self::POST,
|
||||||
'PUT' => self::PUT,
|
'PUT' => self::PUT,
|
||||||
'DELETE' => self::DELETE,
|
'DELETE' => self::DELETE,
|
||||||
'PATCH' => self::PATCH,
|
'PATCH' => self::PATCH,
|
||||||
'OPTIONS' => self::OPTIONS,
|
'OPTIONS' => self::OPTIONS,
|
||||||
'HEAD' => self::HEAD,
|
'HEAD' => self::HEAD,
|
||||||
default => self::GET
|
default => self::GET
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* toString returns the string representation of the method
|
* toString returns the string representation of the method
|
||||||
*/
|
*/
|
||||||
public function toString(): string
|
public function toString(): string
|
||||||
{
|
{
|
||||||
return $this->value;
|
return $this->value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
10
README.md
10
README.md
@ -11,11 +11,11 @@ require_once 'Web.php';
|
|||||||
$app = new Web(debug: true);
|
$app = new Web(debug: true);
|
||||||
|
|
||||||
$app->get('/', function(Context $context) {
|
$app->get('/', function(Context $context) {
|
||||||
return 'Hello World!';
|
return 'Hello World!';
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->get('/users/:id', function(Context $context, $id) {
|
$app->get('/users/:id', function(Context $context, $id) {
|
||||||
return ['user_id' => $id, 'name' => 'John Doe'];
|
return ['user_id' => $id, 'name' => 'John Doe'];
|
||||||
});
|
});
|
||||||
|
|
||||||
$app->run();
|
$app->run();
|
||||||
@ -112,10 +112,10 @@ run(): void
|
|||||||
### Context Class
|
### Context Class
|
||||||
```php
|
```php
|
||||||
// Request helpers
|
// Request helpers
|
||||||
input(string $name, mixed $default = null): mixed // POST data
|
input(string $name, mixed $default = null): mixed // POST data
|
||||||
query(string $name, mixed $default = null): mixed // Query params
|
query(string $name, mixed $default = null): mixed // Query params
|
||||||
jsonValue(string $name, mixed $default = null): mixed // JSON body
|
jsonValue(string $name, mixed $default = null): mixed // JSON body
|
||||||
param(int $index, mixed $default = null): mixed // Route params by index
|
param(int $index, mixed $default = null): mixed // Route params by index
|
||||||
all(): array
|
all(): array
|
||||||
only(array $keys): array
|
only(array $keys): array
|
||||||
except(array $keys): array
|
except(array $keys): array
|
||||||
|
|||||||
452
Request.php
452
Request.php
@ -5,265 +5,265 @@
|
|||||||
*/
|
*/
|
||||||
class Request
|
class Request
|
||||||
{
|
{
|
||||||
public string $method;
|
public string $method;
|
||||||
public string $uri;
|
public string $uri;
|
||||||
public string $path;
|
public string $path;
|
||||||
public string $query;
|
public string $query;
|
||||||
public array $headers;
|
public array $headers;
|
||||||
public string $body;
|
public string $body;
|
||||||
public array $params = [];
|
public array $params = [];
|
||||||
public array $queryParams = [];
|
public array $queryParams = [];
|
||||||
public array $postData = [];
|
public array $postData = [];
|
||||||
public array $cookies;
|
public array $cookies;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* __construct creates a new Request from PHP globals
|
* __construct creates a new Request from PHP globals
|
||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
$this->method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
|
||||||
$this->uri = $_SERVER['REQUEST_URI'] ?? '/';
|
$this->uri = $_SERVER['REQUEST_URI'] ?? '/';
|
||||||
|
|
||||||
$urlParts = parse_url($this->uri);
|
$urlParts = parse_url($this->uri);
|
||||||
$this->path = $urlParts['path'] ?? '/';
|
$this->path = $urlParts['path'] ?? '/';
|
||||||
$this->query = $urlParts['query'] ?? '';
|
$this->query = $urlParts['query'] ?? '';
|
||||||
|
|
||||||
parse_str($this->query, $this->queryParams);
|
parse_str($this->query, $this->queryParams);
|
||||||
|
|
||||||
$this->headers = $this->parseHeaders();
|
$this->headers = $this->parseHeaders();
|
||||||
$this->body = file_get_contents('php://input') ?: '';
|
$this->body = file_get_contents('php://input') ?: '';
|
||||||
$this->cookies = $_COOKIE ?? [];
|
$this->cookies = $_COOKIE ?? [];
|
||||||
|
|
||||||
if ($this->method === 'POST' && $this->contentType() === 'application/x-www-form-urlencoded') {
|
if ($this->method === 'POST' && $this->contentType() === 'application/x-www-form-urlencoded') {
|
||||||
parse_str($this->body, $this->postData);
|
parse_str($this->body, $this->postData);
|
||||||
} elseif ($this->method === 'POST' && str_contains($this->contentType(), 'multipart/form-data')) {
|
} elseif ($this->method === 'POST' && str_contains($this->contentType(), 'multipart/form-data')) {
|
||||||
$this->postData = $_POST ?? [];
|
$this->postData = $_POST ?? [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* parseHeaders extracts HTTP headers from $_SERVER
|
* parseHeaders extracts HTTP headers from $_SERVER
|
||||||
*/
|
*/
|
||||||
private function parseHeaders(): array
|
private function parseHeaders(): array
|
||||||
{
|
{
|
||||||
$headers = [];
|
$headers = [];
|
||||||
foreach ($_SERVER as $key => $value) {
|
foreach ($_SERVER as $key => $value) {
|
||||||
if (str_starts_with($key, 'HTTP_')) {
|
if (str_starts_with($key, 'HTTP_')) {
|
||||||
$header = str_replace('_', '-', substr($key, 5));
|
$header = str_replace('_', '-', substr($key, 5));
|
||||||
$headers[strtolower($header)] = $value;
|
$headers[strtolower($header)] = $value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($_SERVER['CONTENT_TYPE'])) $headers['content-type'] = $_SERVER['CONTENT_TYPE'];
|
if (isset($_SERVER['CONTENT_TYPE'])) $headers['content-type'] = $_SERVER['CONTENT_TYPE'];
|
||||||
if (isset($_SERVER['CONTENT_LENGTH'])) $headers['content-length'] = $_SERVER['CONTENT_LENGTH'];
|
if (isset($_SERVER['CONTENT_LENGTH'])) $headers['content-length'] = $_SERVER['CONTENT_LENGTH'];
|
||||||
|
|
||||||
return $headers;
|
return $headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* header returns the value of the specified header
|
* header returns the value of the specified header
|
||||||
*/
|
*/
|
||||||
public function header(string $name): ?string
|
public function header(string $name): ?string
|
||||||
{
|
{
|
||||||
return $this->headers[strtolower($name)] ?? null;
|
return $this->headers[strtolower($name)] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* contentType returns the content type without charset
|
* contentType returns the content type without charset
|
||||||
*/
|
*/
|
||||||
public function contentType(): string
|
public function contentType(): string
|
||||||
{
|
{
|
||||||
$contentType = $this->header('content-type') ?? '';
|
$contentType = $this->header('content-type') ?? '';
|
||||||
return explode(';', $contentType)[0];
|
return explode(';', $contentType)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* json decodes the request body as JSON
|
* json decodes the request body as JSON
|
||||||
*/
|
*/
|
||||||
public function json(): mixed
|
public function json(): mixed
|
||||||
{
|
{
|
||||||
return json_decode($this->body, true);
|
return json_decode($this->body, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* cookie returns the value of the specified cookie
|
* cookie returns the value of the specified cookie
|
||||||
*/
|
*/
|
||||||
public function cookie(string $name): ?string
|
public function cookie(string $name): ?string
|
||||||
{
|
{
|
||||||
return $this->cookies[$name] ?? null;
|
return $this->cookies[$name] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* param returns a route parameter by integer index
|
* param returns a route parameter by integer index
|
||||||
*/
|
*/
|
||||||
public function param(int $index, mixed $default = null): mixed
|
public function param(int $index, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->params[$index] ?? $default;
|
return $this->params[$index] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* input returns a form input value (POST data)
|
* input returns a form input value (POST data)
|
||||||
*/
|
*/
|
||||||
public function input(string $name, mixed $default = null): mixed
|
public function input(string $name, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->postData[$name] ?? $default;
|
return $this->postData[$name] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* query returns a query parameter value
|
* query returns a query parameter value
|
||||||
*/
|
*/
|
||||||
public function query(string $name, mixed $default = null): mixed
|
public function query(string $name, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->queryParams[$name] ?? $default;
|
return $this->queryParams[$name] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* jsonValue returns a value from JSON body
|
* jsonValue returns a value from JSON body
|
||||||
*/
|
*/
|
||||||
public function jsonValue(string $name, mixed $default = null): mixed
|
public function jsonValue(string $name, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
if ($this->contentType() !== 'application/json') {
|
if ($this->contentType() !== 'application/json') {
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
$json = $this->json();
|
$json = $this->json();
|
||||||
if (!is_array($json)) {
|
if (!is_array($json)) {
|
||||||
return $default;
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $json[$name] ?? $default;
|
return $json[$name] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* all returns all input data merged from all sources
|
* all returns all input data merged from all sources
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function all(): array
|
||||||
{
|
{
|
||||||
$data = array_merge($this->queryParams, $this->postData, $this->params);
|
$data = array_merge($this->queryParams, $this->postData, $this->params);
|
||||||
|
|
||||||
if ($this->contentType() === 'application/json') {
|
if ($this->contentType() === 'application/json') {
|
||||||
$json = $this->json();
|
$json = $this->json();
|
||||||
if (is_array($json)) {
|
if (is_array($json)) {
|
||||||
$data = array_merge($data, $json);
|
$data = array_merge($data, $json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* only returns only specified keys from input
|
* only returns only specified keys from input
|
||||||
*/
|
*/
|
||||||
public function only(array $keys): array
|
public function only(array $keys): array
|
||||||
{
|
{
|
||||||
$all = $this->all();
|
$all = $this->all();
|
||||||
return array_intersect_key($all, array_flip($keys));
|
return array_intersect_key($all, array_flip($keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* except returns all input except specified keys
|
* except returns all input except specified keys
|
||||||
*/
|
*/
|
||||||
public function except(array $keys): array
|
public function except(array $keys): array
|
||||||
{
|
{
|
||||||
$all = $this->all();
|
$all = $this->all();
|
||||||
return array_diff_key($all, array_flip($keys));
|
return array_diff_key($all, array_flip($keys));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* has checks if input key exists
|
* has checks if input key exists
|
||||||
*/
|
*/
|
||||||
public function has(string $key): bool
|
public function has(string $key): bool
|
||||||
{
|
{
|
||||||
return $this->input($key) !== null;
|
return $this->input($key) !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* expectsJson checks if request expects JSON response
|
* expectsJson checks if request expects JSON response
|
||||||
*/
|
*/
|
||||||
public function expectsJson(): bool
|
public function expectsJson(): bool
|
||||||
{
|
{
|
||||||
$accept = $this->header('accept') ?? '';
|
$accept = $this->header('accept') ?? '';
|
||||||
return str_contains($accept, 'application/json') ||
|
return str_contains($accept, 'application/json') ||
|
||||||
str_contains($accept, 'text/json') ||
|
str_contains($accept, 'text/json') ||
|
||||||
$this->header('x-requested-with') === 'XMLHttpRequest';
|
$this->header('x-requested-with') === 'XMLHttpRequest';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isAjax checks if request is AJAX
|
* isAjax checks if request is AJAX
|
||||||
*/
|
*/
|
||||||
public function isAjax(): bool
|
public function isAjax(): bool
|
||||||
{
|
{
|
||||||
return $this->header('x-requested-with') === 'XMLHttpRequest';
|
return $this->header('x-requested-with') === 'XMLHttpRequest';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ip returns the client IP address
|
* ip returns the client IP address
|
||||||
*/
|
*/
|
||||||
public function ip(): string
|
public function ip(): string
|
||||||
{
|
{
|
||||||
// Check for proxied IPs
|
// Check for proxied IPs
|
||||||
if ($ip = $this->header('x-forwarded-for')) {
|
if ($ip = $this->header('x-forwarded-for')) {
|
||||||
return explode(',', $ip)[0];
|
return explode(',', $ip)[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($ip = $this->header('x-real-ip')) {
|
if ($ip = $this->header('x-real-ip')) {
|
||||||
return $ip;
|
return $ip;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* userAgent returns the user agent string
|
* userAgent returns the user agent string
|
||||||
*/
|
*/
|
||||||
public function userAgent(): string
|
public function userAgent(): string
|
||||||
{
|
{
|
||||||
return $this->header('user-agent') ?? '';
|
return $this->header('user-agent') ?? '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* referer returns the referer URL
|
* referer returns the referer URL
|
||||||
*/
|
*/
|
||||||
public function referer(): ?string
|
public function referer(): ?string
|
||||||
{
|
{
|
||||||
return $this->header('referer');
|
return $this->header('referer');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* isSecure checks if request is over HTTPS
|
* isSecure checks if request is over HTTPS
|
||||||
*/
|
*/
|
||||||
public function isSecure(): bool
|
public function isSecure(): bool
|
||||||
{
|
{
|
||||||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
|
||||||
($_SERVER['SERVER_PORT'] ?? 80) == 443 ||
|
($_SERVER['SERVER_PORT'] ?? 80) == 443 ||
|
||||||
($this->header('x-forwarded-proto') === 'https');
|
($this->header('x-forwarded-proto') === 'https');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* url returns the full URL of the request
|
* url returns the full URL of the request
|
||||||
*/
|
*/
|
||||||
public function url(): string
|
public function url(): string
|
||||||
{
|
{
|
||||||
$protocol = $this->isSecure() ? 'https' : 'http';
|
$protocol = $this->isSecure() ? 'https' : 'http';
|
||||||
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
$host = $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||||
return $protocol . '://' . $host . $this->uri;
|
return $protocol . '://' . $host . $this->uri;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* fullUrl returns the URL with query string
|
* fullUrl returns the URL with query string
|
||||||
*/
|
*/
|
||||||
public function fullUrl(): string
|
public function fullUrl(): string
|
||||||
{
|
{
|
||||||
return $this->url();
|
return $this->url();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* is checks if the request path matches a pattern
|
* is checks if the request path matches a pattern
|
||||||
*/
|
*/
|
||||||
public function is(string $pattern): bool
|
public function is(string $pattern): bool
|
||||||
{
|
{
|
||||||
$pattern = preg_quote($pattern, '#');
|
$pattern = preg_quote($pattern, '#');
|
||||||
$pattern = str_replace('\*', '.*', $pattern);
|
$pattern = str_replace('\*', '.*', $pattern);
|
||||||
return preg_match('#^' . $pattern . '$#', $this->path) === 1;
|
return preg_match('#^' . $pattern . '$#', $this->path) === 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
218
Response.php
218
Response.php
@ -5,128 +5,128 @@
|
|||||||
*/
|
*/
|
||||||
class Response
|
class Response
|
||||||
{
|
{
|
||||||
private int $statusCode = 200;
|
private int $statusCode = 200;
|
||||||
private array $headers = [];
|
private array $headers = [];
|
||||||
private string $body = '';
|
private string $body = '';
|
||||||
private bool $sent = false;
|
private bool $sent = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the HTTP status code for the response
|
* Set the HTTP status code for the response
|
||||||
*/
|
*/
|
||||||
public function status(int $code): Response
|
public function status(int $code): Response
|
||||||
{
|
{
|
||||||
$this->statusCode = $code;
|
$this->statusCode = $code;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a header for the response
|
* Set a header for the response
|
||||||
*/
|
*/
|
||||||
public function header(string $name, string $value): Response
|
public function header(string $name, string $value): Response
|
||||||
{
|
{
|
||||||
$this->headers[$name] = $value;
|
$this->headers[$name] = $value;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a JSON response with the given data and status code
|
* Set a JSON response with the given data and status code
|
||||||
*/
|
*/
|
||||||
public function json(mixed $data, int $status = 200): Response
|
public function json(mixed $data, int $status = 200): Response
|
||||||
{
|
{
|
||||||
$this->statusCode = $status;
|
$this->statusCode = $status;
|
||||||
$this->headers['Content-Type'] = 'application/json';
|
$this->headers['Content-Type'] = 'application/json';
|
||||||
$this->body = json_encode($data);
|
$this->body = json_encode($data);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a text response with the given text and status code
|
* Set a text response with the given text and status code
|
||||||
*/
|
*/
|
||||||
public function text(string $text, int $status = 200): Response
|
public function text(string $text, int $status = 200): Response
|
||||||
{
|
{
|
||||||
$this->statusCode = $status;
|
$this->statusCode = $status;
|
||||||
$this->headers['Content-Type'] = 'text/plain';
|
$this->headers['Content-Type'] = 'text/plain';
|
||||||
$this->body = $text;
|
$this->body = $text;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set an HTML response with the given HTML and status code
|
* Set an HTML response with the given HTML and status code
|
||||||
*/
|
*/
|
||||||
public function html(string $html, int $status = 200): Response
|
public function html(string $html, int $status = 200): Response
|
||||||
{
|
{
|
||||||
$this->statusCode = $status;
|
$this->statusCode = $status;
|
||||||
$this->headers['Content-Type'] = 'text/html; charset=utf-8';
|
$this->headers['Content-Type'] = 'text/html; charset=utf-8';
|
||||||
$this->body = $html;
|
$this->body = $html;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the given URL with the given status code
|
* Redirect to the given URL with the given status code
|
||||||
*/
|
*/
|
||||||
public function redirect(string $url, int $status = 302): Response
|
public function redirect(string $url, int $status = 302): Response
|
||||||
{
|
{
|
||||||
$this->statusCode = $status;
|
$this->statusCode = $status;
|
||||||
$this->headers['Location'] = $url;
|
$this->headers['Location'] = $url;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set a cookie with the given name, value, and options
|
* Set a cookie with the given name, value, and options
|
||||||
*/
|
*/
|
||||||
public function cookie(string $name, string $value, array $options = []): Response
|
public function cookie(string $name, string $value, array $options = []): Response
|
||||||
{
|
{
|
||||||
$options = array_merge([
|
$options = array_merge([
|
||||||
'expires' => 0,
|
'expires' => 0,
|
||||||
'path' => '/',
|
'path' => '/',
|
||||||
'domain' => '',
|
'domain' => '',
|
||||||
'secure' => false,
|
'secure' => false,
|
||||||
'httponly' => true,
|
'httponly' => true,
|
||||||
'samesite' => 'Lax'
|
'samesite' => 'Lax'
|
||||||
], $options);
|
], $options);
|
||||||
|
|
||||||
setcookie($name, $value, [
|
setcookie($name, $value, [
|
||||||
'expires' => $options['expires'],
|
'expires' => $options['expires'],
|
||||||
'path' => $options['path'],
|
'path' => $options['path'],
|
||||||
'domain' => $options['domain'],
|
'domain' => $options['domain'],
|
||||||
'secure' => $options['secure'],
|
'secure' => $options['secure'],
|
||||||
'httponly' => $options['httponly'],
|
'httponly' => $options['httponly'],
|
||||||
'samesite' => $options['samesite']
|
'samesite' => $options['samesite']
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send the response to the client
|
* Send the response to the client
|
||||||
*/
|
*/
|
||||||
public function send(): void
|
public function send(): void
|
||||||
{
|
{
|
||||||
if ($this->sent) return;
|
if ($this->sent) return;
|
||||||
|
|
||||||
http_response_code($this->statusCode);
|
http_response_code($this->statusCode);
|
||||||
|
|
||||||
foreach ($this->headers as $name => $value) header("$name: $value");
|
foreach ($this->headers as $name => $value) header("$name: $value");
|
||||||
|
|
||||||
echo $this->body;
|
echo $this->body;
|
||||||
$this->sent = true;
|
$this->sent = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Write the given content to the response body
|
* Write the given content to the response body
|
||||||
*/
|
*/
|
||||||
public function write(string $content): Response
|
public function write(string $content): Response
|
||||||
{
|
{
|
||||||
$this->body .= $content;
|
$this->body .= $content;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* End the response with the given content
|
* End the response with the given content
|
||||||
*/
|
*/
|
||||||
public function end(string $content = ''): void
|
public function end(string $content = ''): void
|
||||||
{
|
{
|
||||||
if ($content) $this->body .= $content;
|
if ($content) $this->body .= $content;
|
||||||
$this->send();
|
$this->send();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
298
Session.php
298
Session.php
@ -5,174 +5,174 @@
|
|||||||
*/
|
*/
|
||||||
class Session
|
class Session
|
||||||
{
|
{
|
||||||
private bool $started = false;
|
private bool $started = false;
|
||||||
private array $flashData = [];
|
private array $flashData = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* start initializes the session if not already started
|
* start initializes the session if not already started
|
||||||
*/
|
*/
|
||||||
public function start(): void
|
public function start(): void
|
||||||
{
|
{
|
||||||
if ($this->started || session_status() === PHP_SESSION_ACTIVE) {
|
if ($this->started || session_status() === PHP_SESSION_ACTIVE) {
|
||||||
$this->started = true;
|
$this->started = true;
|
||||||
$this->loadFlashData();
|
$this->loadFlashData();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
session_start();
|
session_start();
|
||||||
$this->started = true;
|
$this->started = true;
|
||||||
$this->loadFlashData();
|
$this->loadFlashData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get retrieves a value from the session
|
* get retrieves a value from the session
|
||||||
*/
|
*/
|
||||||
public function get(string $key, mixed $default = null): mixed
|
public function get(string $key, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
return $_SESSION[$key] ?? $default;
|
return $_SESSION[$key] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* set stores a value in the session
|
* set stores a value in the session
|
||||||
*/
|
*/
|
||||||
public function set(string $key, mixed $value): void
|
public function set(string $key, mixed $value): void
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
$_SESSION[$key] = $value;
|
$_SESSION[$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* has checks if a key exists in the session
|
* has checks if a key exists in the session
|
||||||
*/
|
*/
|
||||||
public function has(string $key): bool
|
public function has(string $key): bool
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
return isset($_SESSION[$key]);
|
return isset($_SESSION[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* remove deletes a value from the session
|
* remove deletes a value from the session
|
||||||
*/
|
*/
|
||||||
public function remove(string $key): void
|
public function remove(string $key): void
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
unset($_SESSION[$key]);
|
unset($_SESSION[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* flash sets a flash message that will be available only for the next request
|
* flash sets a flash message that will be available only for the next request
|
||||||
*/
|
*/
|
||||||
public function flash(string $key, mixed $value): void
|
public function flash(string $key, mixed $value): void
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
$_SESSION['_flash_new'][$key] = $value;
|
$_SESSION['_flash_new'][$key] = $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getFlash retrieves a flash message (available only for current request)
|
* getFlash retrieves a flash message (available only for current request)
|
||||||
*/
|
*/
|
||||||
public function getFlash(string $key, mixed $default = null): mixed
|
public function getFlash(string $key, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
return $this->flashData[$key] ?? $default;
|
return $this->flashData[$key] ?? $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* hasFlash checks if a flash message exists
|
* hasFlash checks if a flash message exists
|
||||||
*/
|
*/
|
||||||
public function hasFlash(string $key): bool
|
public function hasFlash(string $key): bool
|
||||||
{
|
{
|
||||||
return isset($this->flashData[$key]);
|
return isset($this->flashData[$key]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* csrfToken generates or retrieves the CSRF token for the session
|
* csrfToken generates or retrieves the CSRF token for the session
|
||||||
*/
|
*/
|
||||||
public function csrfToken(): string
|
public function csrfToken(): string
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
if (!isset($_SESSION['_csrf_token'])) $_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
|
if (!isset($_SESSION['_csrf_token'])) $_SESSION['_csrf_token'] = bin2hex(random_bytes(32));
|
||||||
return $_SESSION['_csrf_token'];
|
return $_SESSION['_csrf_token'];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validateCsrf validates a CSRF token
|
* validateCsrf validates a CSRF token
|
||||||
*/
|
*/
|
||||||
public function validateCsrf(string $token): bool
|
public function validateCsrf(string $token): bool
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
if (!isset($_SESSION['_csrf_token'])) return false;
|
if (!isset($_SESSION['_csrf_token'])) return false;
|
||||||
return hash_equals($_SESSION['_csrf_token'], $token);
|
return hash_equals($_SESSION['_csrf_token'], $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* regenerate regenerates the session ID for security
|
* regenerate regenerates the session ID for security
|
||||||
*/
|
*/
|
||||||
public function regenerate(bool $deleteOldSession = true): bool
|
public function regenerate(bool $deleteOldSession = true): bool
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
return session_regenerate_id($deleteOldSession);
|
return session_regenerate_id($deleteOldSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* destroy destroys the session
|
* destroy destroys the session
|
||||||
*/
|
*/
|
||||||
public function destroy(): void
|
public function destroy(): void
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
|
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
|
|
||||||
if (ini_get("session.use_cookies")) {
|
if (ini_get("session.use_cookies")) {
|
||||||
$params = session_get_cookie_params();
|
$params = session_get_cookie_params();
|
||||||
setcookie(session_name(), '', time() - 42000,
|
setcookie(session_name(), '', time() - 42000,
|
||||||
$params["path"], $params["domain"],
|
$params["path"], $params["domain"],
|
||||||
$params["secure"], $params["httponly"]
|
$params["secure"], $params["httponly"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
session_destroy();
|
session_destroy();
|
||||||
$this->started = false;
|
$this->started = false;
|
||||||
$this->flashData = [];
|
$this->flashData = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* clear removes all session data but keeps the session active
|
* clear removes all session data but keeps the session active
|
||||||
*/
|
*/
|
||||||
public function clear(): void
|
public function clear(): void
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
$_SESSION = [];
|
$_SESSION = [];
|
||||||
$this->flashData = [];
|
$this->flashData = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* all returns all session data
|
* all returns all session data
|
||||||
*/
|
*/
|
||||||
public function all(): array
|
public function all(): array
|
||||||
{
|
{
|
||||||
$this->ensureStarted();
|
$this->ensureStarted();
|
||||||
$data = $_SESSION;
|
$data = $_SESSION;
|
||||||
unset($data['_flash_old'], $data['_flash_new'], $data['_csrf_token']);
|
unset($data['_flash_old'], $data['_flash_new'], $data['_csrf_token']);
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ensureStarted ensures the session is started
|
* ensureStarted ensures the session is started
|
||||||
*/
|
*/
|
||||||
private function ensureStarted(): void
|
private function ensureStarted(): void
|
||||||
{
|
{
|
||||||
if (!$this->started) $this->start();
|
if (!$this->started) $this->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* loadFlashData loads flash messages and rotates them
|
* loadFlashData loads flash messages and rotates them
|
||||||
*/
|
*/
|
||||||
private function loadFlashData(): void
|
private function loadFlashData(): void
|
||||||
{
|
{
|
||||||
$this->flashData = $_SESSION['_flash_old'] ?? [];
|
$this->flashData = $_SESSION['_flash_old'] ?? [];
|
||||||
|
|
||||||
$_SESSION['_flash_old'] = $_SESSION['_flash_new'] ?? [];
|
$_SESSION['_flash_old'] = $_SESSION['_flash_new'] ?? [];
|
||||||
unset($_SESSION['_flash_new']);
|
unset($_SESSION['_flash_new']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
598
Validator.php
598
Validator.php
@ -5,351 +5,351 @@
|
|||||||
*/
|
*/
|
||||||
class Validator
|
class Validator
|
||||||
{
|
{
|
||||||
private array $errors = [];
|
private array $errors = [];
|
||||||
private array $data = [];
|
private array $data = [];
|
||||||
private array $rules = [];
|
private array $rules = [];
|
||||||
private array $messages = [];
|
private array $messages = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom validation rules registry
|
* Custom validation rules registry
|
||||||
*/
|
*/
|
||||||
private static array $customRules = [];
|
private static array $customRules = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validate performs validation on data with given rules
|
* validate performs validation on data with given rules
|
||||||
*/
|
*/
|
||||||
public function validate(array $data, array $rules, array $messages = []): bool
|
public function validate(array $data, array $rules, array $messages = []): bool
|
||||||
{
|
{
|
||||||
$this->data = $data;
|
$this->data = $data;
|
||||||
$this->rules = $rules;
|
$this->rules = $rules;
|
||||||
$this->messages = $messages;
|
$this->messages = $messages;
|
||||||
$this->errors = [];
|
$this->errors = [];
|
||||||
|
|
||||||
foreach ($rules as $field => $fieldRules) {
|
foreach ($rules as $field => $fieldRules) {
|
||||||
$this->validateField($field, $fieldRules);
|
$this->validateField($field, $fieldRules);
|
||||||
}
|
}
|
||||||
|
|
||||||
return empty($this->errors);
|
return empty($this->errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* validateField validates a single field against its rules
|
* validateField validates a single field against its rules
|
||||||
*/
|
*/
|
||||||
private function validateField(string $field, string|array $rules): void
|
private function validateField(string $field, string|array $rules): void
|
||||||
{
|
{
|
||||||
$rules = is_string($rules) ? explode('|', $rules) : $rules;
|
$rules = is_string($rules) ? explode('|', $rules) : $rules;
|
||||||
$value = $this->getValue($field);
|
$value = $this->getValue($field);
|
||||||
|
|
||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
$this->applyRule($field, $value, $rule);
|
$this->applyRule($field, $value, $rule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getValue gets a value from data using dot notation
|
* getValue gets a value from data using dot notation
|
||||||
*/
|
*/
|
||||||
private function getValue(string $field): mixed
|
private function getValue(string $field): mixed
|
||||||
{
|
{
|
||||||
$keys = explode('.', $field);
|
$keys = explode('.', $field);
|
||||||
$value = $this->data;
|
$value = $this->data;
|
||||||
|
|
||||||
foreach ($keys as $key) {
|
foreach ($keys as $key) {
|
||||||
if (!is_array($value) || !array_key_exists($key, $value)) {
|
if (!is_array($value) || !array_key_exists($key, $value)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
$value = $value[$key];
|
$value = $value[$key];
|
||||||
}
|
}
|
||||||
|
|
||||||
return $value;
|
return $value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* applyRule applies a single validation rule
|
* applyRule applies a single validation rule
|
||||||
*/
|
*/
|
||||||
private function applyRule(string $field, mixed $value, string $rule): void
|
private function applyRule(string $field, mixed $value, string $rule): void
|
||||||
{
|
{
|
||||||
$parts = explode(':', $rule, 2);
|
$parts = explode(':', $rule, 2);
|
||||||
$ruleName = $parts[0];
|
$ruleName = $parts[0];
|
||||||
$parameters = isset($parts[1]) ? explode(',', $parts[1]) : [];
|
$parameters = isset($parts[1]) ? explode(',', $parts[1]) : [];
|
||||||
|
|
||||||
$passes = match($ruleName) {
|
$passes = match($ruleName) {
|
||||||
'required' => $this->validateRequired($value),
|
'required' => $this->validateRequired($value),
|
||||||
'email' => $this->validateEmail($value),
|
'email' => $this->validateEmail($value),
|
||||||
'url' => $this->validateUrl($value),
|
'url' => $this->validateUrl($value),
|
||||||
'alpha' => $this->validateAlpha($value),
|
'alpha' => $this->validateAlpha($value),
|
||||||
'alphaNum' => $this->validateAlphaNum($value),
|
'alphaNum' => $this->validateAlphaNum($value),
|
||||||
'numeric' => $this->validateNumeric($value),
|
'numeric' => $this->validateNumeric($value),
|
||||||
'integer' => $this->validateInteger($value),
|
'integer' => $this->validateInteger($value),
|
||||||
'float' => $this->validateFloat($value),
|
'float' => $this->validateFloat($value),
|
||||||
'boolean' => $this->validateBoolean($value),
|
'boolean' => $this->validateBoolean($value),
|
||||||
'array' => $this->validateArray($value),
|
'array' => $this->validateArray($value),
|
||||||
'json' => $this->validateJson($value),
|
'json' => $this->validateJson($value),
|
||||||
'date' => $this->validateDate($value),
|
'date' => $this->validateDate($value),
|
||||||
'min' => $this->validateMin($value, $parameters[0] ?? 0),
|
'min' => $this->validateMin($value, $parameters[0] ?? 0),
|
||||||
'max' => $this->validateMax($value, $parameters[0] ?? 0),
|
'max' => $this->validateMax($value, $parameters[0] ?? 0),
|
||||||
'between' => $this->validateBetween($value, $parameters[0] ?? 0, $parameters[1] ?? 0),
|
'between' => $this->validateBetween($value, $parameters[0] ?? 0, $parameters[1] ?? 0),
|
||||||
'length' => $this->validateLength($value, $parameters[0] ?? 0),
|
'length' => $this->validateLength($value, $parameters[0] ?? 0),
|
||||||
'in' => $this->validateIn($value, $parameters),
|
'in' => $this->validateIn($value, $parameters),
|
||||||
'notIn' => $this->validateNotIn($value, $parameters),
|
'notIn' => $this->validateNotIn($value, $parameters),
|
||||||
'regex' => $this->validateRegex($value, $parameters[0] ?? ''),
|
'regex' => $this->validateRegex($value, $parameters[0] ?? ''),
|
||||||
'confirmed' => $this->validateConfirmed($field, $value),
|
'confirmed' => $this->validateConfirmed($field, $value),
|
||||||
'unique' => $this->validateUnique($value, $parameters),
|
'unique' => $this->validateUnique($value, $parameters),
|
||||||
'exists' => $this->validateExists($value, $parameters),
|
'exists' => $this->validateExists($value, $parameters),
|
||||||
default => $this->applyCustomRule($ruleName, $value, $parameters)
|
default => $this->applyCustomRule($ruleName, $value, $parameters)
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!$passes) {
|
if (!$passes) {
|
||||||
$this->addError($field, $ruleName, $parameters);
|
$this->addError($field, $ruleName, $parameters);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation rule methods
|
* Validation rule methods
|
||||||
*/
|
*/
|
||||||
private function validateRequired(mixed $value): bool
|
private function validateRequired(mixed $value): bool
|
||||||
{
|
{
|
||||||
if ($value === null) return false;
|
if ($value === null) return false;
|
||||||
if (is_string($value) && trim($value) === '') return false;
|
if (is_string($value) && trim($value) === '') return false;
|
||||||
if (is_array($value) && empty($value)) return false;
|
if (is_array($value) && empty($value)) return false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateEmail(mixed $value): bool
|
private function validateEmail(mixed $value): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateUrl(mixed $value): bool
|
private function validateUrl(mixed $value): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
return filter_var($value, FILTER_VALIDATE_URL) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateAlpha(mixed $value): bool
|
private function validateAlpha(mixed $value): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
return ctype_alpha($value);
|
return ctype_alpha($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateAlphaNum(mixed $value): bool
|
private function validateAlphaNum(mixed $value): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
return ctype_alnum($value);
|
return ctype_alnum($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateNumeric(mixed $value): bool
|
private function validateNumeric(mixed $value): bool
|
||||||
{
|
{
|
||||||
return is_numeric($value);
|
return is_numeric($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateInteger(mixed $value): bool
|
private function validateInteger(mixed $value): bool
|
||||||
{
|
{
|
||||||
return filter_var($value, FILTER_VALIDATE_INT) !== false;
|
return filter_var($value, FILTER_VALIDATE_INT) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateFloat(mixed $value): bool
|
private function validateFloat(mixed $value): bool
|
||||||
{
|
{
|
||||||
return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
|
return filter_var($value, FILTER_VALIDATE_FLOAT) !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateBoolean(mixed $value): bool
|
private function validateBoolean(mixed $value): bool
|
||||||
{
|
{
|
||||||
return in_array($value, [true, false, 0, 1, '0', '1', 'true', 'false'], true);
|
return in_array($value, [true, false, 0, 1, '0', '1', 'true', 'false'], true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateArray(mixed $value): bool
|
private function validateArray(mixed $value): bool
|
||||||
{
|
{
|
||||||
return is_array($value);
|
return is_array($value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateJson(mixed $value): bool
|
private function validateJson(mixed $value): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
json_decode($value);
|
json_decode($value);
|
||||||
return json_last_error() === JSON_ERROR_NONE;
|
return json_last_error() === JSON_ERROR_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateDate(mixed $value): bool
|
private function validateDate(mixed $value): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
$date = date_parse($value);
|
$date = date_parse($value);
|
||||||
return $date['error_count'] === 0 && $date['warning_count'] === 0;
|
return $date['error_count'] === 0 && $date['warning_count'] === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateMin(mixed $value, mixed $min): bool
|
private function validateMin(mixed $value, mixed $min): bool
|
||||||
{
|
{
|
||||||
if (is_numeric($value)) return $value >= $min;
|
if (is_numeric($value)) return $value >= $min;
|
||||||
if (is_string($value)) return strlen($value) >= $min;
|
if (is_string($value)) return strlen($value) >= $min;
|
||||||
if (is_array($value)) return count($value) >= $min;
|
if (is_array($value)) return count($value) >= $min;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateMax(mixed $value, mixed $max): bool
|
private function validateMax(mixed $value, mixed $max): bool
|
||||||
{
|
{
|
||||||
if (is_numeric($value)) return $value <= $max;
|
if (is_numeric($value)) return $value <= $max;
|
||||||
if (is_string($value)) return strlen($value) <= $max;
|
if (is_string($value)) return strlen($value) <= $max;
|
||||||
if (is_array($value)) return count($value) <= $max;
|
if (is_array($value)) return count($value) <= $max;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateBetween(mixed $value, mixed $min, mixed $max): bool
|
private function validateBetween(mixed $value, mixed $min, mixed $max): bool
|
||||||
{
|
{
|
||||||
return $this->validateMin($value, $min) && $this->validateMax($value, $max);
|
return $this->validateMin($value, $min) && $this->validateMax($value, $max);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateLength(mixed $value, mixed $length): bool
|
private function validateLength(mixed $value, mixed $length): bool
|
||||||
{
|
{
|
||||||
if (is_string($value)) return strlen($value) == $length;
|
if (is_string($value)) return strlen($value) == $length;
|
||||||
if (is_array($value)) return count($value) == $length;
|
if (is_array($value)) return count($value) == $length;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateIn(mixed $value, array $values): bool
|
private function validateIn(mixed $value, array $values): bool
|
||||||
{
|
{
|
||||||
return in_array($value, $values, true);
|
return in_array($value, $values, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateNotIn(mixed $value, array $values): bool
|
private function validateNotIn(mixed $value, array $values): bool
|
||||||
{
|
{
|
||||||
return !in_array($value, $values, true);
|
return !in_array($value, $values, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateRegex(mixed $value, string $pattern): bool
|
private function validateRegex(mixed $value, string $pattern): bool
|
||||||
{
|
{
|
||||||
if (!is_string($value)) return false;
|
if (!is_string($value)) return false;
|
||||||
return preg_match($pattern, $value) === 1;
|
return preg_match($pattern, $value) === 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateConfirmed(string $field, mixed $value): bool
|
private function validateConfirmed(string $field, mixed $value): bool
|
||||||
{
|
{
|
||||||
$confirmField = $field . '_confirmation';
|
$confirmField = $field . '_confirmation';
|
||||||
return $value === $this->getValue($confirmField);
|
return $value === $this->getValue($confirmField);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateUnique(mixed $value, array $parameters): bool
|
private function validateUnique(mixed $value, array $parameters): bool
|
||||||
{
|
{
|
||||||
// Placeholder for database uniqueness check
|
// Placeholder for database uniqueness check
|
||||||
// Would require database connection in real implementation
|
// Would require database connection in real implementation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateExists(mixed $value, array $parameters): bool
|
private function validateExists(mixed $value, array $parameters): bool
|
||||||
{
|
{
|
||||||
// Placeholder for database existence check
|
// Placeholder for database existence check
|
||||||
// Would require database connection in real implementation
|
// Would require database connection in real implementation
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* applyCustomRule applies a custom validation rule
|
* applyCustomRule applies a custom validation rule
|
||||||
*/
|
*/
|
||||||
private function applyCustomRule(string $ruleName, mixed $value, array $parameters): bool
|
private function applyCustomRule(string $ruleName, mixed $value, array $parameters): bool
|
||||||
{
|
{
|
||||||
if (!isset(self::$customRules[$ruleName])) {
|
if (!isset(self::$customRules[$ruleName])) {
|
||||||
return true; // Unknown rules pass by default
|
return true; // Unknown rules pass by default
|
||||||
}
|
}
|
||||||
|
|
||||||
return call_user_func(self::$customRules[$ruleName], $value, $parameters, $this->data);
|
return call_user_func(self::$customRules[$ruleName], $value, $parameters, $this->data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* addError adds a validation error
|
* addError adds a validation error
|
||||||
*/
|
*/
|
||||||
private function addError(string $field, string $rule, array $parameters = []): void
|
private function addError(string $field, string $rule, array $parameters = []): void
|
||||||
{
|
{
|
||||||
$message = $this->messages["$field.$rule"]
|
$message = $this->messages["$field.$rule"]
|
||||||
?? $this->messages[$rule]
|
?? $this->messages[$rule]
|
||||||
?? $this->getDefaultMessage($field, $rule, $parameters);
|
?? $this->getDefaultMessage($field, $rule, $parameters);
|
||||||
|
|
||||||
if (!isset($this->errors[$field])) {
|
if (!isset($this->errors[$field])) {
|
||||||
$this->errors[$field] = [];
|
$this->errors[$field] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->errors[$field][] = $message;
|
$this->errors[$field][] = $message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* getDefaultMessage gets a default error message
|
* getDefaultMessage gets a default error message
|
||||||
*/
|
*/
|
||||||
private function getDefaultMessage(string $field, string $rule, array $parameters): string
|
private function getDefaultMessage(string $field, string $rule, array $parameters): string
|
||||||
{
|
{
|
||||||
$fieldName = str_replace('_', ' ', $field);
|
$fieldName = str_replace('_', ' ', $field);
|
||||||
|
|
||||||
return match($rule) {
|
return match($rule) {
|
||||||
'required' => "The {$fieldName} field is required.",
|
'required' => "The {$fieldName} field is required.",
|
||||||
'email' => "The {$fieldName} must be a valid email address.",
|
'email' => "The {$fieldName} must be a valid email address.",
|
||||||
'url' => "The {$fieldName} must be a valid URL.",
|
'url' => "The {$fieldName} must be a valid URL.",
|
||||||
'alpha' => "The {$fieldName} may only contain letters.",
|
'alpha' => "The {$fieldName} may only contain letters.",
|
||||||
'alphaNum' => "The {$fieldName} may only contain letters and numbers.",
|
'alphaNum' => "The {$fieldName} may only contain letters and numbers.",
|
||||||
'numeric' => "The {$fieldName} must be a number.",
|
'numeric' => "The {$fieldName} must be a number.",
|
||||||
'integer' => "The {$fieldName} must be an integer.",
|
'integer' => "The {$fieldName} must be an integer.",
|
||||||
'float' => "The {$fieldName} must be a float.",
|
'float' => "The {$fieldName} must be a float.",
|
||||||
'boolean' => "The {$fieldName} must be a boolean.",
|
'boolean' => "The {$fieldName} must be a boolean.",
|
||||||
'array' => "The {$fieldName} must be an array.",
|
'array' => "The {$fieldName} must be an array.",
|
||||||
'json' => "The {$fieldName} must be valid JSON.",
|
'json' => "The {$fieldName} must be valid JSON.",
|
||||||
'date' => "The {$fieldName} must be a valid date.",
|
'date' => "The {$fieldName} must be a valid date.",
|
||||||
'min' => "The {$fieldName} must be at least {$parameters[0]}.",
|
'min' => "The {$fieldName} must be at least {$parameters[0]}.",
|
||||||
'max' => "The {$fieldName} must not be greater than {$parameters[0]}.",
|
'max' => "The {$fieldName} must not be greater than {$parameters[0]}.",
|
||||||
'between' => "The {$fieldName} must be between {$parameters[0]} and {$parameters[1]}.",
|
'between' => "The {$fieldName} must be between {$parameters[0]} and {$parameters[1]}.",
|
||||||
'length' => "The {$fieldName} must be exactly {$parameters[0]} characters.",
|
'length' => "The {$fieldName} must be exactly {$parameters[0]} characters.",
|
||||||
'in' => "The selected {$fieldName} is invalid.",
|
'in' => "The selected {$fieldName} is invalid.",
|
||||||
'notIn' => "The selected {$fieldName} is invalid.",
|
'notIn' => "The selected {$fieldName} is invalid.",
|
||||||
'regex' => "The {$fieldName} format is invalid.",
|
'regex' => "The {$fieldName} format is invalid.",
|
||||||
'confirmed' => "The {$fieldName} confirmation does not match.",
|
'confirmed' => "The {$fieldName} confirmation does not match.",
|
||||||
'unique' => "The {$fieldName} has already been taken.",
|
'unique' => "The {$fieldName} has already been taken.",
|
||||||
'exists' => "The selected {$fieldName} is invalid.",
|
'exists' => "The selected {$fieldName} is invalid.",
|
||||||
default => "The {$fieldName} is invalid."
|
default => "The {$fieldName} is invalid."
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* errors returns all validation errors
|
* errors returns all validation errors
|
||||||
*/
|
*/
|
||||||
public function errors(): array
|
public function errors(): array
|
||||||
{
|
{
|
||||||
return $this->errors;
|
return $this->errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* failed checks if validation failed
|
* failed checks if validation failed
|
||||||
*/
|
*/
|
||||||
public function failed(): bool
|
public function failed(): bool
|
||||||
{
|
{
|
||||||
return !empty($this->errors);
|
return !empty($this->errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* passed checks if validation passed
|
* passed checks if validation passed
|
||||||
*/
|
*/
|
||||||
public function passed(): bool
|
public function passed(): bool
|
||||||
{
|
{
|
||||||
return empty($this->errors);
|
return empty($this->errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* firstError gets the first error message
|
* firstError gets the first error message
|
||||||
*/
|
*/
|
||||||
public function firstError(string $field = null): ?string
|
public function firstError(string $field = null): ?string
|
||||||
{
|
{
|
||||||
if ($field) {
|
if ($field) {
|
||||||
return $this->errors[$field][0] ?? null;
|
return $this->errors[$field][0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($this->errors as $errors) {
|
foreach ($this->errors as $errors) {
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
return $errors[0];
|
return $errors[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* extend adds a custom validation rule
|
* extend adds a custom validation rule
|
||||||
*/
|
*/
|
||||||
public static function extend(string $name, callable $callback): void
|
public static function extend(string $name, callable $callback): void
|
||||||
{
|
{
|
||||||
self::$customRules[$name] = $callback;
|
self::$customRules[$name] = $callback;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
254
Web.php
254
Web.php
@ -12,158 +12,158 @@ require_once __DIR__ . '/ErrorHandler.php';
|
|||||||
*/
|
*/
|
||||||
class Web
|
class Web
|
||||||
{
|
{
|
||||||
private Router $router;
|
private Router $router;
|
||||||
private array $middleware = [];
|
private array $middleware = [];
|
||||||
private Context $context;
|
private Context $context;
|
||||||
private ErrorHandler $errorHandler;
|
private ErrorHandler $errorHandler;
|
||||||
|
|
||||||
public function __construct(bool $debug = false)
|
public function __construct(bool $debug = false)
|
||||||
{
|
{
|
||||||
$this->router = new Router();
|
$this->router = new Router();
|
||||||
$this->errorHandler = new ErrorHandler($debug);
|
$this->errorHandler = new ErrorHandler($debug);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function use(callable $middleware): self
|
public function use(callable $middleware): self
|
||||||
{
|
{
|
||||||
$this->middleware[] = $middleware;
|
$this->middleware[] = $middleware;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get(string $route, callable $handler): self
|
public function get(string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->get($route, $handler);
|
$this->router->get($route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function post(string $route, callable $handler): self
|
public function post(string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->post($route, $handler);
|
$this->router->post($route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function put(string $route, callable $handler): self
|
public function put(string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->put($route, $handler);
|
$this->router->put($route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function patch(string $route, callable $handler): self
|
public function patch(string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->patch($route, $handler);
|
$this->router->patch($route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(string $route, callable $handler): self
|
public function delete(string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->delete($route, $handler);
|
$this->router->delete($route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function head(string $route, callable $handler): self
|
public function head(string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->head($route, $handler);
|
$this->router->head($route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function route(string $method, string $route, callable $handler): self
|
public function route(string $method, string $route, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->router->add($method, $route, $handler);
|
$this->router->add($method, $route, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function group(string $prefix, callable $callback): self
|
public function group(string $prefix, callable $callback): self
|
||||||
{
|
{
|
||||||
$originalRouter = $this->router;
|
$originalRouter = $this->router;
|
||||||
$groupRouter = new Router();
|
$groupRouter = new Router();
|
||||||
|
|
||||||
$this->router = $groupRouter;
|
$this->router = $groupRouter;
|
||||||
$callback($this);
|
$callback($this);
|
||||||
|
|
||||||
foreach ($groupRouter->dump() as $path => $methods) {
|
foreach ($groupRouter->dump() as $path => $methods) {
|
||||||
$this->addGroupRoutes($originalRouter, $prefix, $path, $methods);
|
$this->addGroupRoutes($originalRouter, $prefix, $path, $methods);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->router = $originalRouter;
|
$this->router = $originalRouter;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setErrorHandler(int $status, callable $handler): self
|
public function setErrorHandler(int $status, callable $handler): self
|
||||||
{
|
{
|
||||||
$this->errorHandler->register($status, $handler);
|
$this->errorHandler->register($status, $handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setDefaultErrorHandler(callable $handler): self
|
public function setDefaultErrorHandler(callable $handler): self
|
||||||
{
|
{
|
||||||
$this->errorHandler->setDefaultHandler($handler);
|
$this->errorHandler->setDefaultHandler($handler);
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function addGroupRoutes(Router $router, string $prefix, string $path, mixed $node, string $currentPath = ''): void
|
private function addGroupRoutes(Router $router, string $prefix, string $path, mixed $node, string $currentPath = ''): void
|
||||||
{
|
{
|
||||||
if ($path !== '') $currentPath = $currentPath ? "$currentPath/$path" : $path;
|
if ($path !== '') $currentPath = $currentPath ? "$currentPath/$path" : $path;
|
||||||
|
|
||||||
if (!is_array($node)) return;
|
if (!is_array($node)) return;
|
||||||
|
|
||||||
foreach ($node as $key => $value) {
|
foreach ($node as $key => $value) {
|
||||||
if (in_array($key, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])) {
|
if (in_array($key, ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'])) {
|
||||||
$fullPath = rtrim($prefix, '/') . '/' . ltrim($currentPath, '/');
|
$fullPath = rtrim($prefix, '/') . '/' . ltrim($currentPath, '/');
|
||||||
$fullPath = str_replace(':x', ':param', $fullPath);
|
$fullPath = str_replace(':x', ':param', $fullPath);
|
||||||
$router->add($key, $fullPath, $value);
|
$router->add($key, $fullPath, $value);
|
||||||
} elseif (is_array($value)) {
|
} elseif (is_array($value)) {
|
||||||
$this->addGroupRoutes($router, $prefix, $key, $value, $currentPath);
|
$this->addGroupRoutes($router, $prefix, $key, $value, $currentPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->context = new Context();
|
$this->context = new Context();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$next = function() {
|
$next = function() {
|
||||||
$result = $this->router->lookup(
|
$result = $this->router->lookup(
|
||||||
$this->context->request->method,
|
$this->context->request->method,
|
||||||
$this->context->request->path
|
$this->context->request->path
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($result['code'] === 404) {
|
if ($result['code'] === 404) {
|
||||||
$this->errorHandler->handle($this->context, 404);
|
$this->errorHandler->handle($this->context, 404);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result['code'] === 405) {
|
if ($result['code'] === 405) {
|
||||||
$this->errorHandler->handle($this->context, 405);
|
$this->errorHandler->handle($this->context, 405);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->context->request->params = $result['params'];
|
$this->context->request->params = $result['params'];
|
||||||
|
|
||||||
$handler = $result['handler'];
|
$handler = $result['handler'];
|
||||||
$response = $handler($this->context, ...$result['params']);
|
$response = $handler($this->context, ...$result['params']);
|
||||||
|
|
||||||
if ($response instanceof Response) {
|
if ($response instanceof Response) {
|
||||||
$response->send();
|
$response->send();
|
||||||
} elseif (is_array($response) || is_object($response)) {
|
} elseif (is_array($response) || is_object($response)) {
|
||||||
$this->context->json($response);
|
$this->context->json($response);
|
||||||
} elseif (is_string($response)) {
|
} elseif (is_string($response)) {
|
||||||
$this->context->text($response);
|
$this->context->text($response);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$chain = array_reduce(
|
$chain = array_reduce(
|
||||||
array_reverse($this->middleware),
|
array_reverse($this->middleware),
|
||||||
function($next, $middleware) {
|
function($next, $middleware) {
|
||||||
return function() use ($middleware, $next) {
|
return function() use ($middleware, $next) {
|
||||||
$middleware($this->context, $next);
|
$middleware($this->context, $next);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
$next
|
$next
|
||||||
);
|
);
|
||||||
|
|
||||||
$chain();
|
$chain();
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->errorHandler->handleException($this->context, $e);
|
$this->errorHandler->handleException($this->context, $e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
410
auth/Auth.php
410
auth/Auth.php
@ -8,245 +8,237 @@ require_once __DIR__ . '/../Session.php';
|
|||||||
*/
|
*/
|
||||||
class Auth
|
class Auth
|
||||||
{
|
{
|
||||||
private Session $session;
|
private Session $session;
|
||||||
private ?User $user = null;
|
private ?User $user = null;
|
||||||
private array $config;
|
private array $config;
|
||||||
|
|
||||||
const SESSION_KEY = 'auth_user_data';
|
const SESSION_KEY = 'auth_user_data';
|
||||||
const REMEMBER_COOKIE = 'remember_token';
|
const REMEMBER_COOKIE = 'remember_token';
|
||||||
const REMEMBER_DURATION = 2592000; // 30 days in seconds
|
const REMEMBER_DURATION = 2592000; // 30 days in seconds
|
||||||
|
|
||||||
public function __construct(Session $session, array $config = [])
|
public function __construct(Session $session, array $config = [])
|
||||||
{
|
{
|
||||||
$this->session = $session;
|
$this->session = $session;
|
||||||
$this->config = array_merge([
|
$this->config = array_merge([
|
||||||
'cookie_name' => self::REMEMBER_COOKIE,
|
'cookie_name' => self::REMEMBER_COOKIE,
|
||||||
'cookie_lifetime' => self::REMEMBER_DURATION,
|
'cookie_lifetime' => self::REMEMBER_DURATION,
|
||||||
'cookie_path' => '/',
|
'cookie_path' => '/',
|
||||||
'cookie_domain' => '',
|
'cookie_domain' => '',
|
||||||
'cookie_secure' => false,
|
'cookie_secure' => false,
|
||||||
'cookie_httponly' => true,
|
'cookie_httponly' => true,
|
||||||
'cookie_samesite' => 'Lax'
|
'cookie_samesite' => 'Lax'
|
||||||
], $config);
|
], $config);
|
||||||
|
|
||||||
$this->initializeFromSession();
|
$this->initializeFromSession();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login with externally verified user data
|
* Login with externally verified user data
|
||||||
*/
|
*/
|
||||||
public function login(array $userData, bool $remember = false): void
|
public function login(array $userData, bool $remember = false): void
|
||||||
{
|
{
|
||||||
$this->user = new User($userData);
|
$this->user = new User($userData);
|
||||||
$this->session->set(self::SESSION_KEY, $userData);
|
$this->session->set(self::SESSION_KEY, $userData);
|
||||||
$this->session->regenerate();
|
$this->session->regenerate();
|
||||||
|
|
||||||
if ($remember) {
|
if ($remember) $this->createRememberToken($userData);
|
||||||
$this->createRememberToken($userData);
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Login using user data directly
|
* Login using user data directly
|
||||||
*/
|
*/
|
||||||
public function loginUser(User $user, bool $remember = false): void
|
public function loginUser(User $user, bool $remember = false): void
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->session->set(self::SESSION_KEY, $user->toArray());
|
$this->session->set(self::SESSION_KEY, $user->toArray());
|
||||||
$this->session->regenerate();
|
$this->session->regenerate();
|
||||||
|
|
||||||
if ($remember) {
|
if ($remember) $this->createRememberToken($user->toArray());
|
||||||
$this->createRememberToken($user->toArray());
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout the current user
|
* Logout the current user
|
||||||
*/
|
*/
|
||||||
public function logout(): void
|
public function logout(): void
|
||||||
{
|
{
|
||||||
$this->user = null;
|
$this->user = null;
|
||||||
$this->session->remove(self::SESSION_KEY);
|
$this->session->remove(self::SESSION_KEY);
|
||||||
$this->session->regenerate();
|
$this->session->regenerate();
|
||||||
$this->clearRememberCookie();
|
$this->clearRememberCookie();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user is authenticated
|
* Check if user is authenticated
|
||||||
*/
|
*/
|
||||||
public function check(): bool
|
public function check(): bool
|
||||||
{
|
{
|
||||||
return $this->user() !== null;
|
return $this->user() !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user is guest (not authenticated)
|
* Check if user is guest (not authenticated)
|
||||||
*/
|
*/
|
||||||
public function guest(): bool
|
public function guest(): bool
|
||||||
{
|
{
|
||||||
return !$this->check();
|
return !$this->check();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the currently authenticated user
|
* Get the currently authenticated user
|
||||||
*/
|
*/
|
||||||
public function user(): ?User
|
public function user(): ?User
|
||||||
{
|
{
|
||||||
if ($this->user) {
|
if ($this->user) return $this->user;
|
||||||
return $this->user;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to load from session
|
// Try to load from session
|
||||||
$userData = $this->session->get(self::SESSION_KEY);
|
$userData = $this->session->get(self::SESSION_KEY);
|
||||||
if ($userData) {
|
if ($userData) {
|
||||||
$this->user = new User($userData);
|
$this->user = new User($userData);
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to load from remember cookie
|
// Try to load from remember cookie
|
||||||
$userData = $this->getUserDataFromRememberCookie();
|
$userData = $this->getUserDataFromRememberCookie();
|
||||||
if ($userData) {
|
if ($userData) {
|
||||||
$this->user = new User($userData);
|
$this->user = new User($userData);
|
||||||
$this->session->set(self::SESSION_KEY, $userData);
|
$this->session->set(self::SESSION_KEY, $userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->user;
|
return $this->user;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user ID
|
* Get user ID
|
||||||
*/
|
*/
|
||||||
public function id(): int|string|null
|
public function id(): int|string|null
|
||||||
{
|
{
|
||||||
return $this->user()?->getId();
|
return $this->user()?->getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set user data after external verification
|
* Set user data after external verification
|
||||||
*/
|
*/
|
||||||
public function setUserData(array $userData): void
|
public function setUserData(array $userData): void
|
||||||
{
|
{
|
||||||
$this->user = new User($userData);
|
$this->user = new User($userData);
|
||||||
$this->session->set(self::SESSION_KEY, $userData);
|
$this->session->set(self::SESSION_KEY, $userData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hash a password
|
* Hash a password
|
||||||
*/
|
*/
|
||||||
public function hashPassword(string $password): string
|
public function hashPassword(string $password): string
|
||||||
{
|
{
|
||||||
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
|
return password_hash($password, PASSWORD_BCRYPT, ['cost' => 10]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify a password against hash
|
* Verify a password against hash
|
||||||
*/
|
*/
|
||||||
public function verifyPassword(string $password, string $hash): bool
|
public function verifyPassword(string $password, string $hash): bool
|
||||||
{
|
{
|
||||||
return password_verify($password, $hash);
|
return password_verify($password, $hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize user from session
|
* Initialize user from session
|
||||||
*/
|
*/
|
||||||
private function initializeFromSession(): void
|
private function initializeFromSession(): void
|
||||||
{
|
{
|
||||||
$this->session->start();
|
$this->session->start();
|
||||||
$this->user();
|
$this->user();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create remember token for user
|
* Create remember token for user
|
||||||
*/
|
*/
|
||||||
private function createRememberToken(array $userData): void
|
private function createRememberToken(array $userData): void
|
||||||
{
|
{
|
||||||
$token = $this->generateRememberToken();
|
$token = $this->generateRememberToken();
|
||||||
$hashedToken = hash('sha256', $token);
|
$hashedToken = hash('sha256', $token);
|
||||||
|
|
||||||
$userData['remember_token'] = $hashedToken;
|
$userData['remember_token'] = $hashedToken;
|
||||||
$this->session->set('remember_user_data', $userData);
|
$this->session->set('remember_user_data', $userData);
|
||||||
|
|
||||||
$this->setRememberCookie($userData['id'] . '|' . $token);
|
$this->setRememberCookie($userData['id'] . '|' . $token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a random remember token
|
* Generate a random remember token
|
||||||
*/
|
*/
|
||||||
private function generateRememberToken(): string
|
private function generateRememberToken(): string
|
||||||
{
|
{
|
||||||
return bin2hex(random_bytes(32));
|
return bin2hex(random_bytes(32));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set remember cookie
|
* Set remember cookie
|
||||||
*/
|
*/
|
||||||
private function setRememberCookie(string $value): void
|
private function setRememberCookie(string $value): void
|
||||||
{
|
{
|
||||||
setcookie(
|
setcookie(
|
||||||
$this->config['cookie_name'],
|
$this->config['cookie_name'],
|
||||||
$value,
|
$value,
|
||||||
[
|
[
|
||||||
'expires' => time() + $this->config['cookie_lifetime'],
|
'expires' => time() + $this->config['cookie_lifetime'],
|
||||||
'path' => $this->config['cookie_path'],
|
'path' => $this->config['cookie_path'],
|
||||||
'domain' => $this->config['cookie_domain'],
|
'domain' => $this->config['cookie_domain'],
|
||||||
'secure' => $this->config['cookie_secure'],
|
'secure' => $this->config['cookie_secure'],
|
||||||
'httponly' => $this->config['cookie_httponly'],
|
'httponly' => $this->config['cookie_httponly'],
|
||||||
'samesite' => $this->config['cookie_samesite']
|
'samesite' => $this->config['cookie_samesite']
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear remember cookie
|
* Clear remember cookie
|
||||||
*/
|
*/
|
||||||
private function clearRememberCookie(): void
|
private function clearRememberCookie(): void
|
||||||
{
|
{
|
||||||
setcookie(
|
setcookie(
|
||||||
$this->config['cookie_name'],
|
$this->config['cookie_name'],
|
||||||
'',
|
'',
|
||||||
[
|
[
|
||||||
'expires' => time() - 3600,
|
'expires' => time() - 3600,
|
||||||
'path' => $this->config['cookie_path'],
|
'path' => $this->config['cookie_path'],
|
||||||
'domain' => $this->config['cookie_domain'],
|
'domain' => $this->config['cookie_domain'],
|
||||||
'secure' => $this->config['cookie_secure'],
|
'secure' => $this->config['cookie_secure'],
|
||||||
'httponly' => $this->config['cookie_httponly'],
|
'httponly' => $this->config['cookie_httponly'],
|
||||||
'samesite' => $this->config['cookie_samesite']
|
'samesite' => $this->config['cookie_samesite']
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user data from remember cookie
|
* Get user data from remember cookie
|
||||||
*/
|
*/
|
||||||
private function getUserDataFromRememberCookie(): ?array
|
private function getUserDataFromRememberCookie(): ?array
|
||||||
{
|
{
|
||||||
$cookie = $_COOKIE[$this->config['cookie_name']] ?? null;
|
$cookie = $_COOKIE[$this->config['cookie_name']] ?? null;
|
||||||
|
|
||||||
if (!$cookie || !str_contains($cookie, '|')) {
|
if (!$cookie || !str_contains($cookie, '|')) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
[$id, $token] = explode('|', $cookie, 2);
|
[$id, $token] = explode('|', $cookie, 2);
|
||||||
$hashedToken = hash('sha256', $token);
|
$hashedToken = hash('sha256', $token);
|
||||||
|
|
||||||
$userData = $this->session->get('remember_user_data');
|
$userData = $this->session->get('remember_user_data');
|
||||||
|
|
||||||
if (!$userData || $userData['id'] != $id || ($userData['remember_token'] ?? null) !== $hashedToken) {
|
if (!$userData || $userData['id'] != $id || ($userData['remember_token'] ?? null) !== $hashedToken) {
|
||||||
$this->clearRememberCookie();
|
$this->clearRememberCookie();
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $userData;
|
return $userData;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear user data and session
|
* Clear user data and session
|
||||||
*/
|
*/
|
||||||
public function clear(): void
|
public function clear(): void
|
||||||
{
|
{
|
||||||
$this->user = null;
|
$this->user = null;
|
||||||
$this->session->remove(self::SESSION_KEY);
|
$this->session->remove(self::SESSION_KEY);
|
||||||
$this->session->remove('remember_user_data');
|
$this->session->remove('remember_user_data');
|
||||||
$this->clearRememberCookie();
|
$this->clearRememberCookie();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,198 +7,198 @@ require_once __DIR__ . '/Auth.php';
|
|||||||
*/
|
*/
|
||||||
class AuthMiddleware
|
class AuthMiddleware
|
||||||
{
|
{
|
||||||
private Auth $auth;
|
private Auth $auth;
|
||||||
|
|
||||||
public function __construct(Auth $auth)
|
public function __construct(Auth $auth)
|
||||||
{
|
{
|
||||||
$this->auth = $auth;
|
$this->auth = $auth;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Require authentication
|
* Require authentication
|
||||||
*/
|
*/
|
||||||
public function requireAuth(): callable
|
public function requireAuth(): callable
|
||||||
{
|
{
|
||||||
return function(Context $context, callable $next) {
|
return function(Context $context, callable $next) {
|
||||||
if ($this->auth->guest()) {
|
if ($this->auth->guest()) {
|
||||||
// Check if request expects JSON
|
// Check if request expects JSON
|
||||||
if ($context->request->expectsJson()) {
|
if ($context->request->expectsJson()) {
|
||||||
$context->json(['error' => 'Unauthenticated'], 401);
|
$context->json(['error' => 'Unauthenticated'], 401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to login page
|
// Redirect to login page
|
||||||
$context->redirect('/login');
|
$context->redirect('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user to context
|
// Add user to context
|
||||||
$context->set('user', $this->auth->user());
|
$context->set('user', $this->auth->user());
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Require guest (not authenticated)
|
* Require guest (not authenticated)
|
||||||
*/
|
*/
|
||||||
public function requireGuest(): callable
|
public function requireGuest(): callable
|
||||||
{
|
{
|
||||||
return function(Context $context, callable $next) {
|
return function(Context $context, callable $next) {
|
||||||
if ($this->auth->check()) {
|
if ($this->auth->check()) {
|
||||||
// Check if request expects JSON
|
// Check if request expects JSON
|
||||||
if ($context->request->expectsJson()) {
|
if ($context->request->expectsJson()) {
|
||||||
$context->json(['error' => 'Already authenticated'], 403);
|
$context->json(['error' => 'Already authenticated'], 403);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to home or dashboard
|
// Redirect to home or dashboard
|
||||||
$context->redirect('/');
|
$context->redirect('/');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Optional authentication (sets user if authenticated)
|
* Optional authentication (sets user if authenticated)
|
||||||
*/
|
*/
|
||||||
public function optional(): callable
|
public function optional(): callable
|
||||||
{
|
{
|
||||||
return function(Context $context, callable $next) {
|
return function(Context $context, callable $next) {
|
||||||
if ($this->auth->check()) $context->set('user', $this->auth->user());
|
if ($this->auth->check()) $context->set('user', $this->auth->user());
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if user has specific role
|
* Check if user has specific role
|
||||||
*/
|
*/
|
||||||
public function requireRole(string|array $roles): callable
|
public function requireRole(string|array $roles): callable
|
||||||
{
|
{
|
||||||
$roles = is_array($roles) ? $roles : [$roles];
|
$roles = is_array($roles) ? $roles : [$roles];
|
||||||
|
|
||||||
return function(Context $context, callable $next) use ($roles) {
|
return function(Context $context, callable $next) use ($roles) {
|
||||||
if ($this->auth->guest()) {
|
if ($this->auth->guest()) {
|
||||||
if ($context->request->expectsJson()) {
|
if ($context->request->expectsJson()) {
|
||||||
$context->json(['error' => 'Unauthenticated'], 401);
|
$context->json(['error' => 'Unauthenticated'], 401);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$context->redirect('/login');
|
$context->redirect('/login');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$user = $this->auth->user();
|
$user = $this->auth->user();
|
||||||
$userRole = $user->role;
|
$userRole = $user->role;
|
||||||
|
|
||||||
if (!in_array($userRole, $roles)) {
|
if (!in_array($userRole, $roles)) {
|
||||||
if ($context->request->expectsJson()) {
|
if ($context->request->expectsJson()) {
|
||||||
$context->json(['error' => 'Insufficient permissions'], 403);
|
$context->json(['error' => 'Insufficient permissions'], 403);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$context->error(403, 'Forbidden');
|
$context->error(403, 'Forbidden');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$context->set('user', $user);
|
$context->set('user', $user);
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rate limiting per user
|
* Rate limiting per user
|
||||||
*/
|
*/
|
||||||
public function rateLimit(int $maxAttempts = 60, int $decayMinutes = 1): callable
|
public function rateLimit(int $maxAttempts = 60, int $decayMinutes = 1): callable
|
||||||
{
|
{
|
||||||
return function(Context $context, callable $next) use ($maxAttempts, $decayMinutes) {
|
return function(Context $context, callable $next) use ($maxAttempts, $decayMinutes) {
|
||||||
if ($this->auth->guest()) {
|
if ($this->auth->guest()) {
|
||||||
$identifier = $context->request->ip();
|
$identifier = $context->request->ip();
|
||||||
} else {
|
} else {
|
||||||
$identifier = 'user:' . $this->auth->id();
|
$identifier = 'user:' . $this->auth->id();
|
||||||
}
|
}
|
||||||
|
|
||||||
$key = 'rate_limit:' . $identifier . ':' . $context->request->path;
|
$key = 'rate_limit:' . $identifier . ':' . $context->request->path;
|
||||||
$attempts = $context->session->get($key, 0);
|
$attempts = $context->session->get($key, 0);
|
||||||
$resetTime = $context->session->get($key . ':reset', 0);
|
$resetTime = $context->session->get($key . ':reset', 0);
|
||||||
|
|
||||||
// Reset counter if decay time has passed
|
// Reset counter if decay time has passed
|
||||||
if (time() > $resetTime) {
|
if (time() > $resetTime) {
|
||||||
$attempts = 0;
|
$attempts = 0;
|
||||||
$context->session->set($key . ':reset', time() + ($decayMinutes * 60));
|
$context->session->set($key . ':reset', time() + ($decayMinutes * 60));
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($attempts >= $maxAttempts) {
|
if ($attempts >= $maxAttempts) {
|
||||||
$retryAfter = $resetTime - time();
|
$retryAfter = $resetTime - time();
|
||||||
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
|
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
|
||||||
$context->response->header('X-RateLimit-Remaining', '0');
|
$context->response->header('X-RateLimit-Remaining', '0');
|
||||||
$context->response->header('Retry-After', (string)$retryAfter);
|
$context->response->header('Retry-After', (string)$retryAfter);
|
||||||
|
|
||||||
if ($context->request->expectsJson()) {
|
if ($context->request->expectsJson()) {
|
||||||
$context->json([
|
$context->json([
|
||||||
'error' => 'Too many requests',
|
'error' => 'Too many requests',
|
||||||
'retry_after' => $retryAfter
|
'retry_after' => $retryAfter
|
||||||
], 429);
|
], 429);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$context->error(429, 'Too Many Requests');
|
$context->error(429, 'Too Many Requests');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Increment attempts
|
// Increment attempts
|
||||||
$context->session->set($key, $attempts + 1);
|
$context->session->set($key, $attempts + 1);
|
||||||
|
|
||||||
// Add rate limit headers
|
// Add rate limit headers
|
||||||
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
|
$context->response->header('X-RateLimit-Limit', (string)$maxAttempts);
|
||||||
$context->response->header('X-RateLimit-Remaining', (string)($maxAttempts - $attempts - 1));
|
$context->response->header('X-RateLimit-Remaining', (string)($maxAttempts - $attempts - 1));
|
||||||
|
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSRF protection
|
* CSRF protection
|
||||||
*/
|
*/
|
||||||
public function verifyCsrf(): callable
|
public function verifyCsrf(): callable
|
||||||
{
|
{
|
||||||
return function(Context $context, callable $next) {
|
return function(Context $context, callable $next) {
|
||||||
// Skip CSRF for safe methods
|
// Skip CSRF for safe methods
|
||||||
if (in_array($context->request->method, ['GET', 'HEAD', 'OPTIONS'])) {
|
if (in_array($context->request->method, ['GET', 'HEAD', 'OPTIONS'])) {
|
||||||
$next();
|
$next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$token = $context->request->input('_token')
|
$token = $context->request->input('_token')
|
||||||
?? $context->request->header('X-CSRF-TOKEN')
|
?? $context->request->header('X-CSRF-TOKEN')
|
||||||
?? $context->request->header('X-XSRF-TOKEN');
|
?? $context->request->header('X-XSRF-TOKEN');
|
||||||
|
|
||||||
if (!$context->session->validateCsrf($token)) {
|
if (!$context->session->validateCsrf($token)) {
|
||||||
if ($context->request->expectsJson()) {
|
if ($context->request->expectsJson()) {
|
||||||
$context->json(['error' => 'CSRF token mismatch'], 419);
|
$context->json(['error' => 'CSRF token mismatch'], 419);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
$context->error(419, 'CSRF token mismatch');
|
$context->error(419, 'CSRF token mismatch');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remember user from cookie
|
* Remember user from cookie
|
||||||
*/
|
*/
|
||||||
public function remember(): callable
|
public function remember(): callable
|
||||||
{
|
{
|
||||||
return function(Context $context, callable $next) {
|
return function(Context $context, callable $next) {
|
||||||
// Auth class already handles remember cookies in constructor
|
// Auth class already handles remember cookies in constructor
|
||||||
// This middleware can be used to refresh the remember token if needed
|
// This middleware can be used to refresh the remember token if needed
|
||||||
|
|
||||||
if ($this->auth->check()) {
|
if ($this->auth->check()) {
|
||||||
$context->set('user', $this->auth->user());
|
$context->set('user', $this->auth->user());
|
||||||
}
|
}
|
||||||
|
|
||||||
$next();
|
$next();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
118
auth/User.php
118
auth/User.php
@ -5,68 +5,68 @@
|
|||||||
*/
|
*/
|
||||||
class User
|
class User
|
||||||
{
|
{
|
||||||
public int|string $id;
|
public int|string $id;
|
||||||
public string $username;
|
public string $username;
|
||||||
public string $email;
|
public string $email;
|
||||||
public string $password;
|
public string $password;
|
||||||
public ?string $rememberToken;
|
public ?string $rememberToken;
|
||||||
public string $role;
|
public string $role;
|
||||||
public ?string $lastLogin;
|
public ?string $lastLogin;
|
||||||
|
|
||||||
public function __construct(array $data = [])
|
public function __construct(array $data = [])
|
||||||
{
|
{
|
||||||
$this->id = $data['id'] ?? 0;
|
$this->id = $data['id'] ?? 0;
|
||||||
$this->username = $data['username'] ?? '';
|
$this->username = $data['username'] ?? '';
|
||||||
$this->email = $data['email'] ?? '';
|
$this->email = $data['email'] ?? '';
|
||||||
$this->password = $data['password'] ?? '';
|
$this->password = $data['password'] ?? '';
|
||||||
$this->rememberToken = $data['remember_token'] ?? $data['rememberToken'] ?? null;
|
$this->rememberToken = $data['remember_token'] ?? $data['rememberToken'] ?? null;
|
||||||
$this->role = $data['role'] ?? 'user';
|
$this->role = $data['role'] ?? 'user';
|
||||||
$this->lastLogin = $data['last_login'] ?? $data['lastLogin'] ?? null;
|
$this->lastLogin = $data['last_login'] ?? $data['lastLogin'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user identifier (email or username)
|
* Get user identifier (email or username)
|
||||||
*/
|
*/
|
||||||
public function getIdentifier(): string
|
public function getIdentifier(): string
|
||||||
{
|
{
|
||||||
return $this->email ?: $this->username;
|
return $this->email ?: $this->username;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get user ID
|
* Get user ID
|
||||||
*/
|
*/
|
||||||
public function getId(): int|string
|
public function getId(): int|string
|
||||||
{
|
{
|
||||||
return $this->id;
|
return $this->id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert to array
|
* Convert to array
|
||||||
*/
|
*/
|
||||||
public function toArray(): array
|
public function toArray(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'username' => $this->username,
|
'username' => $this->username,
|
||||||
'email' => $this->email,
|
'email' => $this->email,
|
||||||
'password' => $this->password,
|
'password' => $this->password,
|
||||||
'remember_token' => $this->rememberToken,
|
'remember_token' => $this->rememberToken,
|
||||||
'role' => $this->role,
|
'role' => $this->role,
|
||||||
'last_login' => $this->lastLogin,
|
'last_login' => $this->lastLogin,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert to safe array (without sensitive data)
|
* Convert to safe array (without sensitive data)
|
||||||
*/
|
*/
|
||||||
public function toSafeArray(): array
|
public function toSafeArray(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => $this->id,
|
'id' => $this->id,
|
||||||
'username' => $this->username,
|
'username' => $this->username,
|
||||||
'email' => $this->email,
|
'email' => $this->email,
|
||||||
'role' => $this->role,
|
'role' => $this->role,
|
||||||
'last_login' => $this->lastLogin,
|
'last_login' => $this->lastLogin,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user