Gestión básica de rutas con PHP y Apache

Camilo Herrera
winkhostingla
Published in
9 min readFeb 5, 2024
Photo by José Martín Ramírez Carrasco on Unsplash

Hola!, espero que estés iniciando el año con muchos planes y tranquilidad, recuerda que cada día que existimos es una bendición y debemos aprovecharlo de la mejor forma posible.

Hoy vamos a estudiar un concepto básico para la gestión de un sitio o software web, el manejo de rutas. En este artículo nos enfocaremos en la gestión de las rutas usando PHP, Apache y mod_rewrite.

Exploremos como lo hacemos en nuestra compañía, generalmente nuestros requerimientos son bastante básicos y no necesitamos frameworks o librerías con funcionalidades avanzadas para este tipo de tareas.

R.I.P my meme friend, you will always be remembered.

Vamos a iniciar como siempre con nuestros requisitos y luego iniciaremos la implementación y explicación de conceptos aplicados para nuestra solución.

Requisitos

Como en la mayoría de nuestras publicaciones, serán necesarios los siguientes elementos:

  1. Un entorno local/remoto de desarrollo que incluya PHP 8.2, Apache 2.4 y el módulo mod_rewrite activo Apache
  2. Café, mucho café
  3. Voluntad de acero, coraje y ganas de aprender.

La solución

Todo inicia con algo pequeño, un paso corto tras otro, en nuestro caso, vamos a iniciar creando un directorio en nuestro directorio raíz del servidor web, vamos a darle un nombre de super héroe de película “routeman”, si manejar rutas en aplicaciones web fuera un super poder mutante, este sería parte del equipo X-men.

Una vez creado, dentro de él crea los siguientes archivos y directorios:

  • .htaccess: Este archivo es el encargado de la configuración de redirección y sobreescritura de rutas en el servidor web usando mod_rewrite, también agregaremos en él algunas opciones para controlar el acceso a archivos php en el directorio.
  • index.php: Este archivo recibirá todas las peticiones realizadas al directorio y será el encargado de la definición de las rutas y sus características.
  • RouteMan.php: El corazón de nuestra solución, esta clase será la encargada de toda la lógica de gestión de rutas y comportamiento del sitio.
  • /routes: Este será nuestro directorio que contendrá los archivos php asociados a cada ruta de tipo “file”. Más adelante veremos los dos tipos de rutas que podemos crear (“file” y “function”), para el tipo “file”, nuestro manejador de rutas invocará el archivo correspondiente según sean configuradas en el index.php.
  • /routes/default.php: Mostrará contenido por defecto al ingresar al URL del home, este incluye una lista de las rutas disponibles y una ruta con errores para mostrar el comportamiento cuando esto ocurra.
  • /routes/about.php: Mostrará contenido en la ruta /about.
  • /routes/testjson.php Retornará contenido en la ruta /json con un ContentType personalizado “application/json”.

La estructura general de nuestra solución con respecto a directorios y archivos será la siguiente:

Estructura de archivos y directorios

Ignora el directorio .vscode es parte del proyecto cuando creas perfiles de depuración en VS Code.

Ahora vamos a la configuración de Apache HTTP Server en nuestro archivo .htaccess

.htaccess

# Restringimos el acceso directo a archivos .php desde orígenes diferentes a
# localhost.
<Files "*.php">
Require ip 127.0.0.1
</Files>

# Ahora autorizamos el único punto de acceso para peticiones que provienen
# de IPs externas, nuestro archivo index.php
<Files "index.php">
Require all granted
</Files>

# Activamos el motor de reescritura de rutas.
RewriteEngine On

# Quita el comentario si quieres forzar el acceso a través de HTTPS
#RewriteCond %{HTTPS} off
#RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [R,L]

# Activamos el header de autorización si usas php en modo cgi.
RewriteCond %{HTTP:Authorization} ^(.*)
RewriteRule .* - [e=HTTP_AUTHORIZATION:%1]

# Le indicamos a mod_rewrite la ruta relativa dentro de la cual están nuestros
# archivos, si vas a usar un directorio al final de la URL ej.
# http://midominio.com/routeman
# debes escribir el RewriteBase
RewriteBase /routeman/

# y ahora nuestro control de redirección, estas líneas le indican al servidor
# que cualquier nombre de archivo o directorio en una URL debe redireccionarlo
# al archivo index.php.
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.+)$ index.php [QSA,L]

Con esta configuración logramos que todo el tráfico sea dirigido a index.php y desde allí podemos hacer la gestión de las rutas usando la clase RouteMan que implementaremos a continuación.

RouteMan.php

<?php

/**
* Clase RouteMan que proporciona funcionalidad de enrutamiento.
*/
class RouteMan
{
/** @var string La ruta al directorio de archivos para rutas tipo "file". */
private string $routePath;

/** @var string El tipo de contenido predeterminado. */
private string $defaultContentType;

/** @var array El arreglo de rutas configuradas. */
private array $arrRoutes;

/** @var string El protocolo usado para acceder la ruta HTTP/HTTPS. */
private string $requestURL;

/** @var string La ruta actual gestionada. */
private string $currentRoute;

/** @var string La base de reescritura para las rutas. */
private string $rewriteBase;

/**
* Constructor de RouteMan.
*/
public function __construct()
{
// Inicializar propiedades
$this->requestURL = (isset($_SERVER["HTTPS"]) ? "https://" : "http://")
. $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];
$this->routePath = 'routes/';
$this->defaultContentType = 'text/html';
$this->currentRoute = '';
$this->rewriteBase = '/routeman';

//mensaje por defecto cuando no se encuentra una ruta.
$this->addRoute('error', [
'function' => function ($route) {
echo "Route " . $route . " not found!";
}
]);
}

/**
* Agrega una ruta a la instancia de RouteMan.
*
* @param string $url La URL de la ruta, ejemplo "about".
* @param array $routeAttr El atributo de archivo de la ruta. Este parámetro
* tiene la siguiente estructura:
*
* array("file" => "nombre del archivo php a usar",
* "content" => "text/html")
*
* array("function" => function(){
* echo "hello";
* },
* "content" => "text/html")
*
* @return void
*/
public function addRoute(string $url, array $routeAttr): void
{
$arrURLs = explode(",", $url);

foreach ($arrURLs as $urlItem) {
$this->arrRoutes[$urlItem] = $routeAttr;
}
}

/**
* Gestiona las acciones y acceso a las rutas configuradas a partir del URL
*
* @return void
*/
public function manageRoutes(): void
{

$this->requestURL = (isset($_SERVER["HTTPS"]) ? "https://" : "http://") . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"];

$arrURL = parse_url($this->requestURL);
$arrURLParams = array();

if (isset($arrURL['query'])) {

parse_str($arrURL['query'], $arrURLParams);
}

// Convertimos los parámetros enviados a través de javascript usando
// .fetch() a un arreglo "global" como $_POST o $_GET
$_RMFETCH = json_decode(file_get_contents("php://input"), true);

if (is_null($_RMFETCH)) {

$_RMFETCH = array();
}

$arrURL["path"] = str_replace($this->rewriteBase, "", $arrURL["path"]);
$this->currentRoute = trim($arrURL['path'], "/");

if (array_key_exists($this->currentRoute, $this->arrRoutes)) {

if (array_key_exists('content', $this->arrRoutes[$this->currentRoute])) {

$this->setHeaderType($this->arrRoutes[$this->currentRoute]['content']);
} else {

$this->setHeaderType();
}

if (isset($this->arrRoutes[$this->currentRoute]['file'])) {

$filePath = $this->routePath . $this->arrRoutes[$this->currentRoute]['file'];

if (file_exists($filePath)) {

require $filePath;
exit;
}
}

if (isset($this->arrRoutes[$this->currentRoute]['function'])) {

$this->arrRoutes[$this->currentRoute]['function']();
exit;
}
} else {
$this->arrRoutes["error"]['function']($this->currentRoute);
exit;
}
}

/**
* Fija el Content-Type a usar para cada ruta invocada.
*
* @param string|null $type The content type
* @return void
*/
public function setHeaderType($type = null): void
{
if (is_null($type)) {

$type = $this->defaultContentType;
}

header("Content-Type: " . $type . "; charset=UTF-8");
}
}

Analicemos el código fuente. En el constructor vamos a definir algunos valores por defecto necesarios para la operación de RoutMan, el primero de ellos será requestURL en él se guardará el tipo de protocolo usado para acceder a nuestras rutas (HTTP/HTTPS), este es necesario para construir la ruta final completa.

A continuación fijaremos routePath, que contiene el directorio donde se buscarán los archivos asociados a rutas de tipo “file”, las rutas de este tipo buscarán el archivo en el directorio, lo inyectarán en el script y finalizarán la ejecución.

También definimos el Content-Type por defecto a usar en nuestras rutas en la propiedad defaultContentType, en este caso “text/html”.

Contamos con un atributo que nos permite guardar dentro de la instancia, la ruta actual procesada, lo llamaremos currentRoute y por defecto estará vacío al inicio de la ejecución.

Y para terminar tendremos el atributo asociado a la ruta base para la sobreescritura llamado rewriteBase, este debe corresponder con el fijado en el archivo .htaccess, (será el mismo), para nuestro caso particular “/routeman”.

Ahora vamos a las funciones incluídas en la clase.

El método addRoute() permite agregar, al objeto instanciado, las rutas definidas que manejará, este cuenta con dos parámetros de tipo string $url y $routeAttr que permiten agregar rutas así:

<?php
require_once("RouteMan.php");

$objRouteMan = new RouteMan();

//rutas de tipo file (archivo)
$objRouteMan->addRoute('about', ['file' => 'about.php']);

//ruta con Content-Type personalizado
$objRouteMan->addRoute('json', [
'file' => 'testjson.php',
'content' => 'application/json'
]);

//ruta de tipo function
//(funcion anónima o puedes pasar una referencia a una función existente)
$objRouteMan->addRoute('testfunct', [
'function' => function () {
echo "<!DOCTYPE html>
<html>

<head>
<title>About</title>
</head>

<body>

<h1>This is a function generated content</h1>
<p>Great!.</p>

</body>

</html>";
}
]);

Como puedes ver, dispones de dos tipos de ruta “file” y “function”, el primero invoca un archivo .php en el directorio /routes y ejecuta el código en él.

El tipo “function” permite gestionar rutas usando funciones anónimas o una referencia a una función existente en tu código.

También puedes establecer el Content-Type por ruta si necesitas que el servidor responda de forma particular a una petición.

Por último tenemos la función encargada de fijar el Content-Type de la respuesta del servidor al hacer una petición a una ruta setHeaderType(), esta recibe un parámetro opcional $type que indica el Content-Type a usar y modifica el header de respuesta correspondiente del servidor, si quieres fijar un Content-Type personalizado por ruta solo es necesario agregarlo en la configuración de la ruta al agregarla en la entrada “content” del arreglo $routeAttr.

A continuación revisemos el contenido de los archivos de rutas, en su mayoría será un mensaje dentro de una estructura básica de HTML.

default.php

Este archivo será mostrado por defecto al ingresar a http://localhost/routeman en él se mostrará la lista de rutas disponibles y un ejemplo de una ruta desconocida para probar el mensaje de error si se presenta un evento de este tipo.

<!DOCTYPE html>
<html>

<head>
<title>Default!</title>
</head>

<body>

<h1>Route list:</h1>
<ul>
<li><a href="about">About</a></li>
<li><a href="json">Json</a></li>
<li><a href="anotherroute">Route</a></li>
</ul>
</body>

</html>

about.php

Este archivo será incluído y mostrado por el sitio una vez un visitante ingrese a la URL http://localhost/about

<!DOCTYPE html>
<html>

<head>
<title>About</title>
</head>

<body>

<h1>This is about</h1>
<p>This is a paragraph.</p>

</body>

</html>

testjson.php

Este archivo de ruta mostrará un mensaje codificado en formato JSON, para presentar el contenido se cambia el Content-Type por “application/json”

<?php
echo json_encode(array("this is" => "json content"));

Ahora teniendo nuestros archivos de ruta, nuestra clase RouteMan y .htaccess vamos al archivo index.php encargado de hacer la magia.

index.php

<?php

//incluimos nuestra clase
require_once("RouteMan.php");

//creamos una instancia de RouteMan
$objRouteMan = new RouteMan();

//configuramos nuestras rutas de archivo
$objRouteMan->addRoute('', ['file' => 'default.php']);
$objRouteMan->addRoute('about', ['file' => 'about.php']);

//configuramos una ruta de muestra cambiando el Content-Type a JSON
$objRouteMan->addRoute('json', [
'file' => 'testjson.php',
'content' => 'application/json'
]);

//configuramos una ruta usando funciones anónimas (si eso es lo tuyo)
$objRouteMan->addRoute('testfunct', [
'function' => function () {
echo "<!DOCTYPE html>
<html>

<head>
<title>About</title>
</head>

<body>

<h1>This is a function generated content</h1>
<p>Great!.</p>

</body>

</html>";
}
]);

//y finalmente pedimos a nuestro RoutMan que gestione nuestras rutas.
$objRouteMan->manageRoutes();

Nuestro archivo index.php incluye el archivo de la clase RouteMan, crea una instancia y configura las rutas a gestionar, una vez configuradas se hace el llamado a la función manageRoutes() para ejecutar las instrucciones a partir de las peticiones de los visitantes.

Y tenemos listo nuestro manejador de rutas, elegante verdad?

Ahora vamos a probarlo, abre tu navegador e ingresa a http://localhost/routeman (o la URL que tengas configurada en tu entorno de desarrollo, tienes un entorno de desarrollo verdad?). Y puedes empezar a ingresar a las rutas configuradas en tu index.php, por ejemplo:

http://localhost/routeman
http://localhost/routeman/about
http://localhost/routeman/json
http://localhost/routeman/testfunct

Debes ver resultados como este:

Ruta por defecto
Ruta /about
Ruta /json
Mensaje de ruta no encontrada

Como puedes ver, el concepto e implementación son bastante sencillos, se requieren ciertos conocimientos en configuración del servidor web, pero en general gestionar las rutas de esta forma permite un mayor control del tráfico y respuesta que quieras dar a las peticiones de los visitantes.

Recuerda que en winkhosting.co somos mucho más que hosting!, trabaja en tus propósitos para este año, diles a tus seres queridos que los amas y nos veremos en nuestra próxima publicación.

--

--