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