Files
whatsapp/core/Router.php
T
2026-04-16 22:23:33 -05:00

146 lines
4.6 KiB
PHP

<?php
/**
* core/Router.php
* Enrutador central del ERP.
*
* Mapea la URL entrante a modules/{m}/views/{v}.php.
* Soporta rutas limpias (/turnero/dashboard) y parámetros explícitos (?m=&v=).
*
* RUTAS PÚBLICAS (sin autenticación):
* turnero/display → pantalla TV para sala de espera
* turnero/kiosko → pantalla táctil para tomar turno
*/
class Router
{
/** Módulo por defecto cuando no se especifica ?m= */
private const DEFAULT_MODULE = 'dashboard';
/** Vista por defecto cuando no se especifica ?v= */
private const DEFAULT_VIEW = 'index';
/** Rutas que NO requieren sesión iniciada. Formato: 'modulo/vista' */
private const PUBLIC_ROUTES = [
'turnero/display',
'turnero/kiosko',
];
/** Patrón permitido para módulo y vista: solo letras, números y guión bajo */
private const SLUG_PATTERN = '/^[a-zA-Z0-9_]{1,64}$/';
private string $module;
private string $view;
// ─── Constructor ────────────────────────────────────────────────────────
public function __construct()
{
[$this->module, $this->view] = $this->resolveRoute();
}
// ─── API pública ────────────────────────────────────────────────────────
public function getModule(): string { return $this->module; }
public function getView(): string { return $this->view; }
/**
* Indica si la ruta actual es pública (no requiere login).
*/
public function isPublic(): bool
{
return in_array($this->module . '/' . $this->view, self::PUBLIC_ROUTES, true);
}
/**
* Resuelve la ruta y devuelve el archivo de vista correspondiente.
* Lanza una excepción si el archivo no existe.
*
* @return string Ruta absoluta al archivo de vista.
* @throws RuntimeException Si el módulo/vista no se encuentran.
*/
public function resolveFile(): string
{
$file = APP_ROOT . "/modules/{$this->module}/views/{$this->view}.php";
if (!file_exists($file)) {
throw new RuntimeException(
"Vista no encontrada: {$this->module}/{$this->view}",
404
);
}
return $file;
}
// ─── Internos ───────────────────────────────────────────────────────────
/**
* Determina módulo y vista a partir de la URL.
* Prioridad: PATH_INFO limpio → parámetros GET ?m= ?v=
*
* @return array{string, string}
*/
private function resolveRoute(): array
{
// 1. Intentar ruta limpia desde PATH_INFO o REQUEST_URI
// Formato: /modulo/vista → e.g. /turnero/dashboard
$path = $this->cleanPath();
if ($path !== null) {
[$m, $v] = $path;
if ($this->isValidSlug($m) && $this->isValidSlug($v)) {
return [$m, $v];
}
}
// 2. Parámetros GET ?m=module&v=view
$m = $_GET['m'] ?? self::DEFAULT_MODULE;
$v = $_GET['v'] ?? self::DEFAULT_VIEW;
// Sanitizar: solo caracteres seguros
$m = preg_replace('/[^a-zA-Z0-9_]/', '', $m);
$v = preg_replace('/[^a-zA-Z0-9_]/', '', $v);
if (!$this->isValidSlug($m)) $m = self::DEFAULT_MODULE;
if (!$this->isValidSlug($v)) $v = self::DEFAULT_VIEW;
return [$m, $v];
}
/**
* Extrae módulo y vista de la REQUEST_URI limpia.
* Retorna null si la URI no corresponde al formato /m/v.
*
* @return array{string,string}|null
*/
private function cleanPath(): ?array
{
$uri = $_SERVER['REQUEST_URI'] ?? '';
// Eliminar query string
$pos = strpos($uri, '?');
if ($pos !== false) {
$uri = substr($uri, 0, $pos);
}
// Eliminar el subdirectorio base si el proyecto no está en la raíz
$scriptDir = rtrim(dirname($_SERVER['SCRIPT_NAME'] ?? ''), '/');
if ($scriptDir !== '' && str_starts_with($uri, $scriptDir)) {
$uri = substr($uri, strlen($scriptDir));
}
// Normalizar y dividir
$parts = array_values(array_filter(explode('/', trim($uri, '/'))));
if (count($parts) >= 2) {
return [$parts[0], $parts[1]];
}
return null;
}
private function isValidSlug(string $slug): bool
{
return (bool) preg_match(self::SLUG_PATTERN, $slug);
}
}