<?php
namespace App\Core;
/**
* Flexible Router Class
* Supports various deployment configurations (subdomain, subfolder, domain)
* Handles middleware pipeline and parameter extraction
*/
class Router
{
private array $routes = [];
private string $basePath = '';
private array $globalMiddleware = [];
private array $routeGroups = [];
private ?string $currentGroupPrefix = null;
private array $currentGroupMiddleware = [];
/**
* @param string $basePath
*/
public function __construct(string $basePath = '')
{
$this->basePath = rtrim($basePath, '/');
$this->detectBasePath();
}
/**
* Auto-detect base path based on deployment configuration
*/
private function detectBasePath(): void
{
$config = $this->getConfig();
switch ($config['deployment_type']) {
case 'subdomain':
// For subdomain deployment (api.domain.com)
$this->basePath = '';
break;
case 'subfolder':
// For subfolder deployment (domain.com/store/api)
$this->basePath = rtrim($config['api_base_path'], '/');
break;
case 'domain':
// For dedicated domain (api-domain.com)
$this->basePath = '';
break;
default:
// Default to subfolder if not specified
$this->basePath = rtrim($config['api_base_path'], '/');
}
}
/**
* Add global middleware that runs on all routes
*/
public function addGlobalMiddleware(string $middleware): void
{
$this->globalMiddleware[] = $middleware;
}
/**
* Create a route group with shared attributes
*/
public function group(array $attributes, callable $callback): void
{
$previousPrefix = $this->currentGroupPrefix;
$previousMiddleware = $this->currentGroupMiddleware;
// Set group attributes
$this->currentGroupPrefix = ($this->currentGroupPrefix ?? '').($attributes['prefix'] ?? '');
$this->currentGroupMiddleware = array_merge(
$this->currentGroupMiddleware,
$attributes['middleware'] ?? []
);
// Execute the group callback
$callback($this);
// Restore previous group state
$this->currentGroupPrefix = $previousPrefix;
$this->currentGroupMiddleware = $previousMiddleware;
}
/**
* Add a route with specified method
*/
public function addRoute(string $method, string $path, $handler, array $middleware = []): void
{
// Apply group prefix
$fullPath = ($this->currentGroupPrefix ?? '').$path;
// Combine group middleware with route middleware
$allMiddleware = array_merge(
$this->globalMiddleware,
$this->currentGroupMiddleware,
$middleware
);
$pattern = $this->convertToRegex($fullPath);
$this->routes[] = [
'method' => strtoupper($method),
'pattern' => $pattern,
'handler' => $handler,
'middleware' => $allMiddleware,
'path' => $fullPath,
'original_path' => $path
];
}
/**
* HTTP Method shortcuts
*/
public function get(string $path, $handler, array $middleware = []): void
{
$this->addRoute('GET', $path, $handler, $middleware);
}
/**
* @param string $path
* @param $handler
* @param array $middleware
*/
public function post(string $path, $handler, array $middleware = []): void
{
$this->addRoute('POST', $path, $handler, $middleware);
}
/**
* @param string $path
* @param $handler
* @param array $middleware
*/
public function put(string $path, $handler, array $middleware = []): void
{
$this->addRoute('PUT', $path, $handler, $middleware);
}
/**
* @param string $path
* @param $handler
* @param array $middleware
*/
public function patch(string $path, $handler, array $middleware = []): void
{
$this->addRoute('PATCH', $path, $handler, $middleware);
}
/**
* @param string $path
* @param $handler
* @param array $middleware
*/
public function delete(string $path, $handler, array $middleware = []): void
{
$this->addRoute('DELETE', $path, $handler, $middleware);
}
/**
* @param string $path
* @param $handler
* @param array $middleware
*/
public function options(string $path, $handler, array $middleware = []): void
{
$this->addRoute('OPTIONS', $path, $handler, $middleware);
}
/**
* Handle preflight OPTIONS requests for CORS
*/
public function handleCors(): void
{
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
$config = $this->getConfig();
if ($config['cors_enabled']) {
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
$allowedOrigins = $config['cors_origins'];
if (in_array('*', $allowedOrigins) || in_array($origin, $allowedOrigins)) {
header('Access-Control-Allow-Origin: '.($origin ?: '*'));
header('Access-Control-Allow-Methods: '.implode(', ', $config['cors_methods']));
header('Access-Control-Allow-Headers: '.implode(', ', $config['cors_headers']));
if ($config['cors_credentials']) {
header('Access-Control-Allow-Credentials: true');
}
header('Access-Control-Max-Age: 86400'); // 24 hours
}
}
http_response_code(200);
exit;
}
}
/**
* Get request method with _method override support
*/
private function getRequestMethod(): string
{
$method = $_SERVER['REQUEST_METHOD'];
// Support method override for POST requests only
if ($method === 'POST' && isset($_POST['_method'])) {
$overrideMethod = strtoupper($_POST['_method']);
// Only allow safe method overrides to simulate REST methods
if (in_array($overrideMethod, ['PUT', 'PATCH', 'DELETE'])) {
return $overrideMethod;
}
}
return $method;
}
/**
* Main dispatch method
*/
public function dispatch(): void
{
try {
// Handle CORS preflight requests
$this->handleCors();
$method = $this->getRequestMethod();
$uri = $this->getCurrentUri();
// Find matching route
$matchedRoute = $this->findRoute($method, $uri);
if (!$matchedRoute) {
$this->handleNotFound();
return;
}
// Execute middleware pipeline
$this->executeMiddleware($matchedRoute['middleware']);
// Execute route handler
$this->executeHandler($matchedRoute);
} catch (\Exception $e) {
$this->handleException($e);
}
}
/**
* Get current URI with base path removed
*/
private function getCurrentUri(): string
{
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
// Remove base path if present
if ($this->basePath && strpos($uri, $this->basePath) === 0) {
$uri = substr($uri, strlen($this->basePath));
}
// Ensure URI starts with /
return '/'.ltrim($uri, '/');
}
/**
* Find matching route
*/
private function findRoute(string $method, string $uri): ?array
{
foreach ($this->routes as $route) {
if ($route['method'] === $method && preg_match($route['pattern'], $uri, $matches)) {
// Remove full match from parameters
array_shift($matches);
$route['parameters'] = $matches;
return $route;
}
}
return null;
}
/**
* Execute middleware pipeline
*/
private function executeMiddleware(array $middleware): void
{
foreach ($middleware as $middlewareClass) {
if (class_exists($middlewareClass)) {
$middlewareInstance = new $middlewareClass();
if (method_exists($middlewareInstance, 'handle')) {
$result = $middlewareInstance->handle();
// If middleware returns false, stop execution
if ($result === false) {
return;
}
}
}
}
}
/**
* Execute route handler
*/
private function executeHandler(array $route): void
{
$handler = $route['handler'];
$parameters = $route['parameters'] ?? [];
if (is_callable($handler)) {
// Direct callable
$result = call_user_func_array($handler, $parameters);
} elseif (is_array($handler) && count($handler) === 2) {
// Controller@method format
[$controllerClass, $method] = $handler;
if (class_exists($controllerClass)) {
$controller = new $controllerClass();
if (method_exists($controller, $method)) {
$result = call_user_func_array([$controller, $method], $parameters);
} else {
throw new \Exception("Method {$method} not found in {$controllerClass}");
}
} else {
throw new \Exception("Controller {$controllerClass} not found");
}
} else {
throw new \Exception("Invalid route handler");
}
// Handle response
$this->handleResponse($result);
}
/**
* Handle response output
*/
private function handleResponse($result): void
{
if ($result === null) {
return;
}
if (is_array($result) || is_object($result)) {
header('Content-Type: application/json');
echo json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
} else {
echo $result;
}
}
/**
* Convert route path to regex pattern
*/
private function convertToRegex(string $path): string
{
// Escape forward slashes
$pattern = str_replace('/', '\/', $path);
// Convert {param} to named capture groups
$pattern = preg_replace('/\{([^}]+)\}/', '([^\/]+)', $pattern);
// Convert {param?} to optional parameters
$pattern = preg_replace('/\{([^}]+)\?\}/', '([^\/]*)', $pattern);
return '/^'.$pattern.'$/';
}
/**
* Handle 404 Not Found
*/
private function handleNotFound(): void
{
http_response_code(404);
header('Content-Type: application/json');
echo json_encode([
'error' => 'Not Found',
'message' => 'The requested resource was not found',
'code' => 404
]);
}
/**
* Handle exceptions
*/
private function handleException(\Exception $e): void
{
$config = $this->getConfig();
http_response_code(500);
header('Content-Type: application/json');
if ($config['debug']) {
echo json_encode([
'error' => 'Internal Server Error',
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTrace()
], JSON_PRETTY_PRINT);
} else {
echo json_encode([
'error' => 'Internal Server Error',
'message' => 'An unexpected error occurred',
'code' => 500
]);
}
// Log the error
error_log("Router Exception: ".$e->getMessage()." in ".$e->getFile().":".$e->getLine());
}
/**
* Get application configuration
*/
private function getConfig(): array
{
/**
* @var mixed
*/
static $config = null;
if ($config === null) {
// Try store-specific config first, fallback to default
$storeConfigFile = __DIR__.'/../../config/app-store.php';
$defaultConfigFile = __DIR__.'/../../config/app.php';
if (file_exists($storeConfigFile)) {
$config = require $storeConfigFile;
} elseif (file_exists($defaultConfigFile)) {
$config = require $defaultConfigFile;
} else {
$config = [];
}
}
return $config;
}
/**
* Get all registered routes (for debugging)
*/
public function getRoutes(): array
{
return $this->routes;
}
/**
* Generate URL for named route
*/
public function url(string $name, array $parameters = []): string
{
// This would be implemented if we add named routes
// For now, return a basic URL construction
$config = $this->getConfig();
$baseUrl = $config['app_url'].$this->basePath;
return $baseUrl.$name;
}
}