Una de las partes más importantes de una web, sobre todo de cara al SEO, son las url utilizadas. A nadie le gusta que en su blog las urls aparezcan con feos parámetros obligatorios en la query, como wordpress por defecto:

http://emiliocobos.net/?p=787

Hoy os presento un par de clases en php que os facilitarán la tarea de crear webs con urls bonitas:

Demo Descarga Ver en GitHub

Para lograr urls bonitas hay dos alternativas:

1- .htaccess

Usar mod_rewrite es a priori la más “simple” de conseguir urls bonitas, ya que viene integrada en nuestro servidor, pero tiene muchos inconvenientes:

  • Es poco mantenible: El día que quieras cambiar la estructura de una url (pasar un nombre de usuario en vez de una id) tendrás que cambiar tanto tu código PHP como tu .htaccess
  • Al final trabajas con la query string: Puede haber conflictos entre parámetros pasados por el usuario y los tuyos, o que cualquier otro parámetro sea ignorado. Supongamos que /user/1 redirige mediante apache a /user.php?uid=1. Si quisieras usar un parámetro get para otra cosa, seguramente no funcione ir a /user/4?edit, sino que habrá que crear otra regla específica.

2- Redirigir todas las solicitudes a index.php y desde ahí leer

Ésta es la opción más sana (sobre todo porque mantienes toda tu lógica en el mismo sitio), se consigue con un .htaccess tal que así:

<IfModule mod_rewrite.c>
	RewriteEngine on

	RewriteCond %{REQUEST_FILENAME} !-f
	RewriteCond %{REQUEST_FILENAME} !-d

	RewriteRule ^(.*)$ index.php/$1 [L]
</IfModule>

Se pueden leer las urls a mano (mediante $_SERVER['REQUEST_URI']), pero yo he hecho un par de clases para facilitar la tarea, permitiéndote usar expresiones regulares, y dejándote elegir métodos específicos (GET, POST…).

Un ejemplo sencillo:

Nuestro index.php con nuestra nueva configuración para hacer el rewrite anterior quedaría así:

<?php
include 'src/Router/Route.php';
include 'src/Router/Router.php';

// Si está en el directorio raíz dejar así, si no especificar como primer parámetro '/la-subcarpeta'
$router = new Router\Router();

$router->add('/', function() {
    // Mostrar el home
});

$router->add('/user/([0-9]+)', function($user_id) {
    // Ya tenemos la id del usuario en la variable $user_id, sin tener que acceder a $_GET ni nada por el estilo
    // En este caso habría que comprobar si el usuario existe, y si no mandar un 404
});

$router->add('/*.', function() {
    // 404
});
?>

Más ejemplos:

Podéis ver más ejemplos en github, donde se encuentra el código, o la demo en vivo:

Demo y descarga

Demo Descarga Ver en GitHub

29 pensamientos en “Router en PHP: Urls dinámicas sin saber .htaccess!

  1. Imagen de oscaroscar el dijo:

    Hola excelente enrutador,
    puedes explicar como funciona con GET, POST, … y el resto q no se para que sirven. Solo conozco GET y POST
    la verdad es q me ha ahorrado mucha faena esta clase.

    un saludo.

    • Imagen de Emilio Cobos Álvarez el dijo:

      El resto de métodos en realidad no funcionan como deberían en la mayoría de servidores (normalmente no están soportados siquiera), pero bueno, ahí están ;)

      El método add toma tres parámetros, uno de ellos opcional, que es una array con los métodos para los que se ejecutarán, que por defecto son todos. Si quisieras que algo se ejecutara sólo para GET, tendrías que hacer algo así:

      $router->add('/ruta', function () {}, ['GET']);

      Pero como soy vago, pues creé un método para reducir la sintaxis:

      $router->get('/ruta', function () {});

      Lo mismo con los métodos post, head, …

  2. Imagen de oscaroscar el dijo:

    Tb he visto otros enrutadores y la sintaxis es así: $router::add en lugar de la flecha. Esto como lo puedo cambiar?
    Me parece mas agradable a la hora de leer.

    Gracias

    • Imagen de Emilio Cobos Álvarez el dijo:

      Eso sólo depende de si hacen los métodos estáticos o dinámicos, yo he optado por instanciar el enrutador para que en el constructor vaya algo de configuración, y poder almacenar las rutas, pero cambiando las variables por variables estáticas, y haciendo lo mismo con los métodos tendrás un bonito Router::add(); ;)

  3. Imagen de oscaroscar el dijo:

    Muchas gracias por tiempo Emilio.
    Si es un formulario post (con 2 inputs login y password) y action:index.php?accion=login
    como seria con el enrutador?

  4. Imagen de oscaroscar el dijo:

    Hola Emilio, otra preguntita:
    como puedo utilizar una variable dentro de la función, sin tener q utilizar $GLOBALS?
    ejemplo:

    $variable=’asdfasdf’;

    $router->add(‘/’, function() {
    echo $variable;
    });

    se podría pasar como parámetro? como?

    saludos.

  5. Imagen de oscaroscar el dijo:

    como se podría hacer un: Redirect::route(‘inicio’); como en laravel?
    Para así no tener que repetir código en caso de que sea el mismo.

    saluditos.

  6. Imagen de Emilio Cobos Álvarez el dijo:

    Simplemente envía el contenido con json y las cabeceras adecuadas:

    $router->add('/ajax', function () {
        header('Content-Type: application/json');
        $data = get_data(); // obtener unos datos costosísimos desde la bd (por ejemplo)
        echo json_encode($data);
    });
    
  7. Imagen de oscaroscar el dijo:

    Hola,

    ¿como podemos hacer que la expresion regular sea opcional y si se pone que siga la expresion regular, es decir que entre aquí :
    $router->add(‘/user/([0-9]+)’, function($user_id) {
    //entra
    }
    con estas 2 direcciones (sin tener q añadir 2 rutas claro):
    /user/
    /user/6

    un cordial saludo.

    • Imagen de Emilio Cobos Álvarez el dijo:

      En teoría se puede (ahora no puedo testarlo pero…).

      Sería usar la interrogación en el regexp para indicar que es opcional, sobre un gupo que no capture (con el ?:), por ejemplo:

      $router->add('/user(?:/([0-9]+)?)', function($user_id = null) {});

      El argumento por defecto sería necesario para que PHP no rompa al entrar a /user.

  8. Imagen de oscaroscar el dijo:

    Hola Emilio,

    me interesa mucho poder ejecutar una ruta sin cargar la pagina, es decir:

    ahora como esta la clase para ejecutar el mismo código (sin tener que cargar una pagina) en 2 rutas distintas hacemos:
    function algo(){
    //codigo aquí
    }
    $router->add(‘/index.php’, algo() );
    $router->add(‘/’, algo() );

    no se puede de alguna manera hacer esto:
    $router->add(‘/’, function(){
    //codigo aquí
    });
    $router->add(‘/index.php’, function(){
    ejecutar(‘/’); //ejecutamos el código de la ruta /
    });

    he probado con expresiones regulares pero queda un código muy sucio.

    • Imagen de Emilio Cobos Álvarez el dijo:
      function algo() {
          // Lo que veas
      }
      
      $router->add('/', function() {
          return algo();
      });
      
      $router->add('/index.php', function() {
          return algo();
      });
      
      // O: Más legible pero a la antigua (debería de funcionar si no me equivoco...):
      $router->add('/', 'algo');
      $router->add('/index.php', 'algo');
      
      
  9. Imagen de oscaroscar el dijo:

    porque lo llamas a la antigua?

    de la forma antigua como puedo pasar un parámetro si la función es:
    function algo($string) {
    echo $string;
    }

    Gracias Emilio

    • Imagen de Emilio Cobos Álvarez el dijo:

      Lo llamo a la antigua porque hasta hace relativamente poco no se aceptaban closures en PHP, se tenía que pasar el nombre de la función como string para que fuera llamado.

      Los parámetros los debería de recibir sin más, porque si no me equivoco se usa call_user_func_array.

  10. Imagen de oscaroscar el dijo:

    Entiendo Emilio.

    Mira estoy probando unas rutas pero tengo este problema:
    //esta ruta SI funciona –> /user
    //esta ruta SI funciona –> /admin
    $router->add(‘/(user|admin)’, function($user) {
    echo “USUARIO = $user”;
    });

    //esta ruta NO funciona –> /user/8
    $router->add(‘/(user|admin)(?:/([0-9]+)?)’, function($user,$user_id = null) {
    if( $user_id === null ) {
    echo “No hay id de usuario”;
    } else {
    echo “UID = $user_id”;
    }
    });

    las he probado por separado.
    cuando pongo opciones la ruta no la coge.

    saludos.

    • Imagen de Emilio Cobos Álvarez el dijo:

      Haciendo este test:

      $route = new Route('/(user|admin)(?:/([0-9]+))?', function($type, $id = null) {
      	var_dump(func_get_args());
      });
      
      $route->matches('/user/8');
      $route->exec();
      
      $route->matches('/user');
      $route->exec();

      Ambas rutas funcionan… Sólo he cambiado una cosa, que es la posición de la última interrogación (porque técnicamente es más correcto, lo que quieres que sea opcional es la parte del /8).

      Sin embargo, de la otra manera también son ejecutadas ambas… Puedes describirme con más detalles tu entorno? (al depender tanto PHP de la configuración global del servidor, SO, cambios entre versiones… Podría dar algún problema, aunque no debería).

  11. Imagen de oscaroscar el dijo:

    Gracias Emilio,

    he estado haciendo pruebas en local y todo perfecto, pero cuando lo subo a produccion en un dominio, tengo problemas.

    Si el codigo esta en index.php y este esta en el raiz del sitio web. que ponemos?
    $router = new Router\Router(”); //no funciona se queda la pagina en blanco
    $router = new Router\Router(‘/’); //así tampoco funciona se queda la pagina en blanco
    $router = new Router\Router(‘/carpeta’); //si pongo una carpeta funciona perfectamente el index.php dentro

    en el raiz no me funciona. porque?

    un cordial saludo.

  12. Imagen de oscaroscar el dijo:

    he intentado contactar contigo, a traves de la sección contacto, pero hay algo mal en el formulario.
    sale el error en rojo: Hubo un error al enviar el e-mail.

    saludos.

  13. Imagen de oscaroscar el dijo:

    en logs me aparecen varias lineas:
    [04-May-2015 20:41:36 UTC] PHP Fatal error: Class ‘Router\Router’ not found in /home/usuario/public_html/miweb.com/index.php on line 5

    2-include ‘src/Router/Route.php’;
    3-include ‘src/Router/Router.php’;
    4-echo ‘hola’;
    5-$router = new Router\Router(”); //tb con barra da error, pero con carpeta no da error.

  14. Imagen de Freddy Campos el dijo:

    Bien me encuentro en un dilema… tengo un customs MVC made in CASA…

    hasta los momentos a trabajado bastante bien hasta que decidí utilizar pagos recurrentes desde paypal
    puedo hacer route muy bien desde direcciones locales, pero paypal envia parametros por get por ejemplo
    el token id pero no me encuentro de buenas para recibir el ?token=ELTOKEN. termina en error

    y luego de eso.. la tabla de rutas internas pierde su forma.

    osea en vez de ir al index / este me lleva al index/index

    ejemplo

    paguina.com/controller/index/index

    y no entiendo por que todo esto luego de intentar recibir el parametro get token de paypal, cierro session abro de nuevo y las url estan como sinada todo se me enrrolla al tratar de recibir ese GET[‘TOKEN’]

    Alguna sugerencia???

    • Imagen de Freddy Campos el dijo:

      buenas aun gracias a todos ya lo logre resolver… cambiando la funcion router->add por router->get

      con esto santo remedio gracias al Desarrollador..

      como ayuda o como aclaratoria seria bueno otro post detallando todas las funciones y sus posibles funciones o eventos.

      sin mas que decir muchas gracias

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Puedes usar las siguientes etiquetas y atributos HTML:<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre data-language=""> <ul> <ol> <li>
Para poner código usa <pre data-language="[lenguaje]"><code>[código]</code></pre>, y no olvides escapar el HTMl.