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, + ]; + } }