Enrutamiento de URLs con .htaccess y PHP

La mayoría de los frameworks en PHP y otros lenguajes ofrecen una propiedad o método de enrutamiento de URL, en pocas palabras, un método para direcciones amigables “User Flriendly URLs”. Estos convierten las direcciones de localizador de recursos uniforme (URL) en algo más parecido al lenguaje humano basado en jerarquías.

Los enrutamientos de URL no solo ayudan a mejorar el SEO, también ayudan a desarrollar una aplicación o sitio web más organizada, que en combinación con otras técnicas de desarrollo como MVC o MVW/MVVM, pueden facilitar el trabajo en equipo, aún si el equipo está compuesto por personas en diferentes partes del mundo y hacer mucho más sencillo el mantenimiento y alcance del proyecto.

Existen varios métodos de enrutamiento. Todo depemderá de en que plataforma o lenguaje estas desarrollando. Si desarrollas en .NET, este enlace puede ayudarte a configurar enrutamientos en IIS. En este breve artículo les voy a mostrar como hacer enrutamientos con PHP con mod_rewrite de Apache Webserver y el uso de .htaccess. Utilizaremos dos escenarios utilizando las reglas de mod_rewrite, uno, sólo con .htaccess y otro con PHP y .htaccess. Para ambos escenarios vamos a tomar como ejemplo un diario digital porque utiliza muchos URL dinámicos.

Supongamos que tenemos un sitio: http://diarioenlaweb.com, el cual tiene módulo como noticias, clasificados, galería, etc. y tiene secciones como actualidad, opinión, autos, empleos, albums, etc. y que por supuesto tiene identificadores únicos para cada artículo, auto, anuncio, etc.

Si queremos visitar una noticia de actualidad la URL sería:

http://diarioenlaweb.com/?modulo=noticias&seccion=actualidad&id=4002

Para visitar un artículo clasificado sería algo similar a:

http://diarioenlaweb.com/?modulo=clasfificados&categoria=autos&id=8456

Para visitar una foto o ver una galería:

http://diarioenlaweb.com/?modulo=galeria&album=conferencia&id=52036

Podemos notar que la estructura es muy similar, sin embargo, los nombres de los parámetros y sus valores son distintos. Esto no solo es malo para SEO, es también difícil de recordar y parece más un código de programación que una dirección. Léanlo en voz alta literalmente y me darán la razón.

1. El método estático solo con .htaccess

Podemos hacer un enrutamiento de URL con un archivo .htaccess en la raíz de documentos de nuestro servidor. Para esto es necesario identificar el significado de los parámetros de la URI y construir una estructura de jerarquía basado en estos parámetros. Nuestro archivo .htaccess tendría las siguientes líneas:

RewriteEngine On

RewriteRule ^(.*)/(.*)/(.*)/([^/]*).html$ index.php?modulo=$1&seccion=$2&id=$3 [QSA]
RewriteRule ^(.*)/(.*)/(.*)/$ index.php?modulo=$1&seccion=$2&id=$3 [QSA]
RewriteRule ^(.*)/(.*)/$ index.php?modulo=$1&seccion=$2 [QSA]
RewriteRule ^(.*)/$ index.php?modulo=$1 [QSA]

RewriteRule ^(.*)/(.*)/(.*)/([^/]*).html$ index.php?modulo=$1&categoria=$2&id=$3 [QSA]
RewriteRule ^(.*)/(.*)/(.*)/$ index.php?modulo=$1&categoria=$2&id=$3 [QSA]
RewriteRule ^(.*)/(.*)/$ index.php?modulo=$1&categoria=$2 [QSA]

RewriteRule ^(.*)/(.*)/(.*)/$ index.php?modulo=$1&album=$2&id=$3 [QSA]
RewriteRule ^(.*)/(.*)/$ index.php?modulo=$1&album=$2 [QSA]

Hemos creado una serie de directivas de forma escalonada, esto, debido a que las directivas se ejecutan desde arriba hacia abajo para poder convertir cada parámetro de nuestra URL en un nodo de jerarquía. Des esta forma en nuestro código PHP podemos utilizar cada parámetro como una serie de variables que podemos invocar según el nombre de la declaración y así obtener su valor, es decir, podemos utilizar $_REQUEST o $_GET para utilizar cada variable:

/* Para obtener el módulo solicitado */
$modulo = filter_var($_REQUEST['modulo'], FILTER_SANITIZE_STRING);

/* Para obtener la sección solicitada */
$seccion = filter_var($_REQUEST['seccion'], FILTER_SANITIZE_STRING);

Ahora podemos utilizar los valores para incluir documentos, hacer consultas en base de datos, procesar información, etc., siguiendo el patrón de declaración anterior para cada uno de los parámetros definidos en .htaccess.

PROS

  • Si tienes una aplicación o web que trabajas solo
  • Si es un proyecto pequeño y no esperas muchas visitas o es algo interno

CONS

  • Alto consumo de procesamiento, a.k.a carga del servidor
  • Por lo general, htaccess es responsabilidad de la seguridad de server y no del programador
  • Si el proyecto es grande, este archivo pude crecer exponencialmente
  • Si es un equipo de desarrollo, todos tendrán que conocer la jerarquía y limitarse a esta
  • Poca seguridad y alta probabilidad de mal funcionamiento
  • Las directivas estáticas no pueden ser modificadas sin que afecte las demás

2. Método dinámico con .htaccess y PHP

Este método requiere un poco más de esfuerzo que el anterior, ya que necesitaremos una directiva en el archivo .htaccess para procesar la consulta y una función en PHP para convertir los parámetros en un arreglo que podamos utilizar posteriormente.

Vamos a iniciar por nuestras directivas mod_rewrite, por lo que tendremos en el archivo .htaccess las siguientes líneas:

Options -MultiViews
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

Como vemos, ahora solo tenemos una regla que indica que la condición aplica únicamente cuando el archivo solicitado no exista en el servidor y la regla aplica para cualquier parámetro o parámetros sean pasados como una sola entidad de query string. Es decir, todo información será procesada como query string o parámetros sin importar el formato que tenga le query string y asume que la estructura jerárquica de directorios común “/modulo/seccion/” sea convertida a query string o cadena de parámetros siempre que no exista el archivo.

Ya tenemos la directiva, ahora vamos a tomar los parámetros en formato jerárquico de directorio “/1/2/etc” y vamos a procesarlos con PHP para extraer los valores.

Para hacerlo más interesante, vamos a construir una clase (POO) para procesar los parámetros. Crearemos un archivo al que llamaremos Route.php que será nuestra clase y dentro tendremos el siguiente código:

<?php
class Route {

private $basepath;
private $uri;
private $base_url;
private $routes;
private $route;

private function getCurrentUri()
{
$this->basepath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/';
$this->uri = substr($_SERVER['REQUEST_URI'], strlen($this->basepath));
if (strstr($this->uri, '?')) $this->uri = substr($this->uri, 0, strpos($this->uri, '?'));
$this->uri = '/' . trim($this->uri, '/');
return $this->uri;
}

public function getRoutes()
{
$this->base_url = $this->getCurrentUri();
$this->routes = explode('/', $this->base_url);
return $this->routes;
}

}
?>

Primero tenemos la declaración de la clase en PHP:

class Route

Dentro de la clase, declaramos primero las variables que utilizaremos a largo de la clase y sus métodos. Estas variables van declarándose una a una según las necesitemos; pero al momento de escribirlas las escribimos al inicio de nuestro archivo para saber lo que tenemos.

private $basepath;
private $uri;
private $base_url;
private $routes;
private $route;

Creamos un método para capturar la URL actual del navegador: getCurrentUri(). En este método capturamos la URL del navegador que es la ruta relativa completa desde la raíz de documentos:

$this->basepath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/';

A partir de la ruta actual del archivo que estamos invocando, es decir supongamos que tenemos un archivo index.php en la raíz de documentos de nuestro server. Todo a partir del directorio actual del archivo index.php será considerado como parámetro o query string, como si fuera /index.php?modulo=x&seccion=y entonces separamos lo que es ruta real de lo que son parámetros:

$this->uri = substr($_SERVER['REQUEST_URI'], strlen($this->basepath));

Si existe el caracter “?” significa que es un query string, entonces removemos este y todo lo que esté a la derecha, ya que lo que esperamos es una ruta de tipo /modulo/seccion/, de esta forma limpiamos, por le momento, lo que no sea “User Friendly”.

if (strstr($this->uri, '?')) $this->uri = substr($this->uri, 0, strpos($this->uri, '?'));

Ahora que tenemos todos los elemtos como un string único, limpiamos cualquier espacio en blanco antes y después:

$this->uri = '/' . trim($this->uri, '/');

Y retornamos el query string:

return $this->uri;

Luego creamos otro método, esta vez para convertir nuestra cadena de caracteres o query string en una matriz o array unidimensional que podamos utilizar de forma dinámica: getRoutes() que será en cierta medida el método que invocaremos para utilizar los parámetros.

Como tenemos nuestra ruta vamos a incluirla en el nuevo método para procesar la URL.

$this->base_url = $this->getCurrentUri();

Luego, vamos a separar todos los parámetros, que como sabemos, vienen separados por “/”, que serán nuestras rutas o los nodos de nuestra ruta:

$this->routes = explode('/', $this->base_url);

Una vez separados, sólo necesitamos retornarlos para utilizarlo a los largo de nuestra aplicación o sitio web:

return $this->routes;

Para utilizar nuestra clase, crearemos un archivo index.php en la raíz de documentos de nuestro server o en la carpeta de nuestro proyecto, si aún no lo han creado. Y como cualquier clase en php la invocamos, declaramos un nuevo objeto y hacemos un llamado al método getRoutes() para tener nuestros parámetros como un array:

<?php

require_once dirname(__FILE__) . "/Route.php";
$routes = new Route(true);
$route = $routes->getRoutes();

print_r($route);

?>

De esta forma tenemos un enrutador de URL dinámico que puede ser reutilizado una y otra vez y los programadores puede hacer módulos separados sin depender uno de otro ni de reglas predeterminadas en .htaccess. el resultado será un arreglo o array con el elemento o posición cero vacío o “”, esto debido a que la posición 0 es la posción de la ruta actual del archivo “/” la cual no es considerada como parámetro:

Array (0=>'', 1=>noticias, 2=>actualidad, 3=>4002)

Ahora, por que desechar los parámetros tipo query string general “/?x=n&y=n&z=n”?, como estamos enrutando URL de forma amigable “/x/y/z/ los parámetros de tipo query string no son necesarios. ¿Pero que tal si quiero utilizarlos en alguna parte de mi proyecto? No hay problema.

Siempre que la URL sea en forma estructurada “/x/y/z”, nuestro enrutamiento procesará cada nodo como un parámetro. Si ademas de nuestro formato amigable también tenemos query string, entonces lo procesaremos como una entidad independiente.

Necesitaremos declarar dos nuevas variables, una para almacenar cada elemento del query string y su valor como una arreglo asociativo y otra para indicarle a la clase o enrutador “Route” que deseamos o no utilizar query string.

private $params;
private $get_params

Luego, creamos un método mágico __construct() para indicar si queremos o no utilizar query string en nuestro proyecto:

function __construct($get_params = false) {
$this->get_params = $get_params;
}

Creamos un nuevo método para procesar el query string y convertirlo en arreglo o array, a partir del caracter “?” y si existe el mismo en la URL del navegador:

private function getParams()
{
if (strstr($this->uri, '?'))
{
$params = explode("?", $this->uri);
$params = $params[1];

parse_str($params, $this->params);
$this->routes[0] = $this->params;
array_pop($this->routes);
}

}

Para hacer uso del mismo tendremos que modificar los métodos anteriores getRoutes y getCurrentUri para invocar el nuevo método de ser necesario. A continuación el nuevo código de nuestra clase Route con le nuevo método y los cambios necesarios:

<?php
class Route {

private $basepath;
private $uri;
private $base_url;
private $routes;
private $route;
private $params;
private $get_params;

function __construct($get_params = false) {
$this->get_params = $get_params;
}

public function getRoutes()
{
$this->base_url = $this->getCurrentUri();
$this->routes = explode('/', $this->base_url);

$this->getParams(); //invocamos el neuvo método
return $this->routes;
}

private function getCurrentUri()
{
$this->basepath = implode('/', array_slice(explode('/', $_SERVER['SCRIPT_NAME']), 0, -1)) . '/';
$this->uri = substr($_SERVER['REQUEST_URI'], strlen($this->basepath));

if($this->get_params)
{
$this->getParams();
}else{
if (strstr($this->uri, '?')) $this->uri = substr($this->uri, 0, strpos($this->uri, '?'));
}

$this->uri = '/' . trim($this->uri, '/');
return $this->uri;
}

private function getParams()
{
if (strstr($this->uri, '?'))
{
$params = explode("?", $this->uri);
$params = $params[1];

parse_str($params, $this->params);
$this->routes[0] = $this->params;
array_pop($this->routes);
}

}

}
?>

Ahora no solo podemos utilizar el enrutador como una clase re-utilizable en nuestro proyecto y otros proyectos, sino que también podemos utilizar query string tradicional para capturar valores o parámetros que no tiene que ver que con el enrutamiento, por ejemplo, atributos de artículos para una tienda como lo hace Amazon.com o para opciones de contenido o configuraciones en cualquier tipo de aplicación que necesite pasar valores para ser procesados en una nueva vista, formulario o pantalla, etc.

PROS

  • Requiere menos procesamiento de Apache
  • No requiere declaración independiente en .htaccess
  • Puedes validar las rutas contra una Base de Datos o con un archivo de configuración para rutas válidas
  • Mayor control y seguridad
  • Recomendable para un equipo de varios programadores
  • Recomendable para aplicaciones grandes con formas diferentes de pasar parámetros
  • Puedes utilizar todas las rutas que te puedas imaginar
  • Facilitan el proceso de desarrollo de nuevos módulos o capacidades de tu aplicación o sitio web
  • Puedes reutilizarlo en muchas aplicaciones y proyectos. Recicla 😉
  • etc., etc., etc.

CONS

  • Requiere conocimiento intermedio de PHP
  • Necesita algún tipo de validación interna de las rutas válidas
  • Requiere mayor tiempo de desarrollo (Ya no tanto ;p )

Para más detalles sobre mod_rewrite, visita la documentación de Apache Web Server.

Espero les sea útil. CODE IS POETRY!