diff --git a/app/Controllers/TestController.php b/app/Controllers/TestController.php new file mode 100644 index 0000000..8a944a4 --- /dev/null +++ b/app/Controllers/TestController.php @@ -0,0 +1,22 @@ +response->view('subnet'); + } +} \ No newline at end of file diff --git a/config/routes.php b/config/routes.php index 33b550b..659acde 100644 --- a/config/routes.php +++ b/config/routes.php @@ -2,7 +2,10 @@ use App\Controllers\Api\SubnetController; use App\Controllers\HomeController; -use Core\Router\Router; +use App\Controllers\TestController; +use Core\Routing\Router; Router::get('/', HomeController::class, 'index'); -Router::post('/api/subnet', SubnetController::class, 'data'); \ No newline at end of file +Router::post('/api/subnet', SubnetController::class, 'data'); +Router::get('/test/{id}', TestController::class, 'test'); +Router::get('/test/{id}/update', TestController::class, 'test'); \ No newline at end of file diff --git a/public/index.php b/public/index.php index a3c2da1..2bc027d 100644 --- a/public/index.php +++ b/public/index.php @@ -1,7 +1,7 @@ handleException($exception); + + exit(0); } } \ No newline at end of file diff --git a/src/Exceptions/Exceptions/HttpException.php b/src/Exceptions/Exceptions/HttpException.php new file mode 100644 index 0000000..833aeeb --- /dev/null +++ b/src/Exceptions/Exceptions/HttpException.php @@ -0,0 +1,5 @@ +data = $data; + + // Capture all exceptions + Exceptions::catch($this); + } + /** * Get request method * @@ -43,7 +60,7 @@ class Request */ public final function has(string $param): bool { - return isset($_POST[$param]); + return isset($this->data[$param]); } /** @@ -56,11 +73,11 @@ class Request public final function get(string|null $param = null, mixed $default = null): mixed { if($param == null) { - return $_POST; + return $this->data; } if($this->has($param)) { - return $_POST[$param]; + return $this->data[$param]; } return $default; diff --git a/src/Router/Router.php b/src/Router/Router.php deleted file mode 100644 index a8b57a2..0000000 --- a/src/Router/Router.php +++ /dev/null @@ -1,109 +0,0 @@ - $controller, - 'action' => $action, - ]; - } - - /** - * Dispatch router and run application - * - * @return void - */ - public static function dispatch(): void - { - // Init request - $request = new Request(); - $url = $request->url(); - $method = $request->method(); - - // Capture all exceptions - Exceptions::catch($request); - - try { - if (array_key_exists($url, self::$routes[$method])) { - $controllerName = self::$routes[$method][$url]['controller']; - $actionName = self::$routes[$method][$url]['action']; - - // Check for controller existence - if (!class_exists($controllerName)) { - throw new Exception(sprintf("Controller '%s' missing", $controllerName)); - } - - // Create controller object - $controller = new $controllerName($request); - - // Check for method on controller - if (!method_exists($controller, $actionName)) { - throw new Exception(sprintf("Method '%s' not found on '%s'", $actionName, $controllerName)); - } - - // Build response object of action - $response = $controller->$actionName(); - - // Render action - if ($response instanceof Render) { - $response->render(); - } - } else { - throw new Exception(sprintf("No route found for: %s", $url)); - } - } catch (Exception $e) { - Exceptions::catchOne($e); - } - } -} \ No newline at end of file diff --git a/src/Routing/RouteDispatcher.php b/src/Routing/RouteDispatcher.php new file mode 100644 index 0000000..3ca7782 --- /dev/null +++ b/src/Routing/RouteDispatcher.php @@ -0,0 +1,162 @@ +request = $request; + $this->routeCollection = $routeCollection; + + try { + $route = $this->findMatchingRoute(); + $controller = $this->instantiateController($route['controller']); + $action = $this->validateActionExists($controller, $route['action']); + $params = $this->resolveParameters($controller, $action, $route['params'] ?? []); + + $this->executeAction($controller, $action, $params); + } catch (NotFoundHttpException $e) { + $this->handleException($e, 404, 'page not found'); + } catch (Exception $e) { + $this->handleException($e, 500, 'something went wrong'); + } + } + + public static function dispatch(Request $request, array $routeCollection): void + { + new self($request, $routeCollection); + } + + /** + * Locate a matching route for the incoming request. + * + * @return array + * @throws \Core\Exceptions\Exceptions\NotFoundHttpException + */ + private function findMatchingRoute(): array + { + $url = $this->request->url(); + $method = $this->request->method(); + + foreach ($this->routeCollection[$method] ?? [] as $routeRegex => $route) { + if (preg_match($routeRegex, $url, $matches)) { + $params = array_filter( + $matches, + fn($key) => !is_numeric($key), + ARRAY_FILTER_USE_KEY + ); + return array_merge($route, ['params' => $params]); + } + } + + throw new NotFoundHttpException(sprintf("No route found for: %s", $url)); + } + + /** + * Create an instance of the requested controller. + * + * @param string $controllerName + * @return object + * @throws \Core\Exceptions\Exceptions\NotFoundHttpException + */ + private function instantiateController(string $controllerName): object + { + if (!class_exists($controllerName)) { + throw new NotFoundHttpException(sprintf("Controller '%s' missing", $controllerName)); + } + + return new $controllerName($this->request); + } + + /** + * Validate that the action exists in the controller. + * + * @param object $controller + * @param string $actionName + * @return string + * @throws \Core\Exceptions\Exceptions\NotFoundHttpException + */ + private function validateActionExists(object $controller, string $actionName): string + { + if (!method_exists($controller, $actionName)) { + throw new NotFoundHttpException(sprintf("Method '%s' not found on '%s'", $actionName, get_class($controller))); + } + + return $actionName; + } + + /** + * Validate and resolve parameters for the controller action. + * + * @param object $controller + * @param string $action + * @param array $params + * @return array + * @throws \Exception + */ + private function resolveParameters(object $controller, string $action, array $params): array + { + return RouteValidator::resolve($controller, $action, $params); + } + + /** + * Execute the resolved action on the controller with validated parameters. + * + * @param object $controller + * @param string $action + * @param array $params + * @return void + */ + private function executeAction(object $controller, string $action, array $params): void + { + $response = $controller->$action(...$params); + + if ($response instanceof Render) { + $response->render(); + } + } + + /** + * Handle exceptions gracefully. + * + * @param Exception $e + * @param int $statusCode + * @param string $message + * @return void + */ + private function handleException(Exception $e, int $statusCode, string $message): void + { + if (Env::get('debug')) { + Exceptions::catchOne($e); + } + + http_response_code($statusCode); + echo $message; + } +} \ No newline at end of file diff --git a/src/Routing/RouteValidator.php b/src/Routing/RouteValidator.php new file mode 100644 index 0000000..2852407 --- /dev/null +++ b/src/Routing/RouteValidator.php @@ -0,0 +1,89 @@ +getParameters(); + $validatedParams = []; + + foreach ($methodParameters as $methodParameter) { + $paramName = $methodParameter->getName(); + $paramType = $methodParameter->getType(); + $isOptional = $methodParameter->isOptional(); + $defaultValue = $isOptional ? $methodParameter->getDefaultValue() : null; + + if (!array_key_exists($paramName, $params)) { + if ($isOptional) { + $validatedParams[$paramName] = $defaultValue; + continue; + } + throw new NotFoundHttpException(sprintf("Missing parameter '%s' for action '%s'", $paramName, $action)); + } + + $value = $params[$paramName]; + $validatedParams[$paramName] = self::validateType($paramName, $value, $paramType, $action); + } + + return $validatedParams; + } + + /** + * Validate and cast a parameter based on its type. + * + * @param string $paramName + * @param mixed $value + * @param ReflectionNamedType|null $paramType + * @param string $action + * @return mixed + * @throws \Core\Exceptions\Exceptions\NotFoundHttpException + */ + private static function validateType(string $paramName, mixed $value, ?ReflectionNamedType $paramType, string $action): mixed + { + if (!$paramType || $paramType->allowsNull() && $value === null) { + return $value; // No type or allows null, return as-is. + } + + $typeName = $paramType->getName(); + + switch ($typeName) { + case 'int': + if (!ctype_digit((string)$value)) { + throw new NotFoundHttpException(sprintf("Invalid type for parameter '%s'. Expected integer, got '%s'", $paramName, gettype($value))); + } + return (int)$value; + + case 'float': + if (!is_numeric($value)) { + throw new NotFoundHttpException(sprintf("Invalid type for parameter '%s'. Expected float, got '%s'", $paramName, gettype($value))); + } + return (float)$value; + + case 'string': + if (!is_string($value)) { + throw new NotFoundHttpException(sprintf("Invalid type for parameter '%s'. Expected string, got '%s'", $paramName, gettype($value))); + } + return $value; + + default: + throw new NotFoundHttpException(sprintf("Unsupported type '%s' for parameter '%s'", $typeName, $paramName)); + } + } +} \ No newline at end of file diff --git a/src/Routing/Router.php b/src/Routing/Router.php new file mode 100644 index 0000000..e88c095 --- /dev/null +++ b/src/Routing/Router.php @@ -0,0 +1,77 @@ +[a-zA-Z0-9_-]+)', $route); + $routeRegex = '#^' . $routeRegex . '$#'; + + self::$routes[$method][$routeRegex] = [ + 'controller' => $controller, + 'action' => $action, + 'original' => $route, + ]; + } + + /** + * Dispatch router and run application + * + * @return void + */ + public static function dispatch(): void + { + // Create request + $request = new Request($_POST + $_FILES); + + // Dispatch router + RouteDispatcher::dispatch($request, self::$routes); + } +} \ No newline at end of file