Ver índice de contenidos del libro

2.4. Enrutamiento

Al definir una ruta en Silex también se define el código del controlador que se ejecuta cuando el usuario accede a esa ruta.

Un ruta está formada por dos partes:

  • Patrón: define la ruta que apunta a un determinado recurso. Puede incluir partes variables e incluso se pueden restringir los posibles valores de esas variables.
  • Método: describe la interacción que tiene lugar con el recurso solicitado. Silex soporta los siguientes métodos HTTP: GET, POST, PUT, DELETE, PATCH y OPTIONS.

El código del controlador se define en un closure como el siguiente:

function () {
    // código de la función
}

Los closures son funciones anónimas que pueden utilizar variables que se encuentran fuera de su código. La diferencia con las variables globales es que los closures no requieren que esas variables accedidas sean globales.

El valor que devuelve la ejecución del closure se utiliza como contenido de la página que se devuelve al usuario.

2.4.1. Ejemplo de cómo definir una ruta GET

A continuación se muestra la definición de una ruta GET de ejemplo:

$blogPosts = array(
    1 => array(
        'date'   => '2011-03-29',
        'author' => 'igorw',
        'title'  => 'Using Silex',
        'body'   => '...',
    ),
);
 
$app->get('/blog', function () use ($blogPosts) {
    $output = '';
    foreach ($blogPosts as $post) {
        $output .= $post['title'];
        $output .= '<br />';
    }
 
    return $output;
});

Si accedes a la URL /blog verás un listado con los títulos de los artículos de un blog. La instrucción use en este caso no tiene nada que ver con los namespaces de PHP. Se trata de la forma en la que se indica al closure que debe importar la variable exterior $blogPosts. Una vez importada, ya puedes utilizarla dentro de la función anónima como cualquier otra variable normal.

2.4.2. Rutas dinámicas

El siguiente ejemplo muestra el código de otro controlador que permite mostrar el contenido de un artículo individual del blog:

$app->get('/blog/show/{id}', function (Silex\Application $app, $id) use ($blogPosts) {
    if (!isset($blogPosts[$id])) {
        $app->abort(404, "El artículo $id no existe.");
    }
 
    $post = $blogPosts[$id];
 
    return  "<h1>{$post['title']}</h1>".
            "<p>{$post['body']}</p>";
});

La definición de esta ruta incluye una parte variable llamada {id}, cuyo valor se pasa como argumento al closure.

Una de las partes más importantes del código anterior es function (Silex\Application $app, ...). Esta instrucción hace que Silex pase automáticamente a la función un objeto de tipo Application que contiene toda la información de la aplicación actual.

Internamente Silex utiliza lo que se conoce como type hinting para detectar que estás solicitando el objeto de la aplicación. ¿Cómo funciona? La clave está en añadir el tipo de argumento (Silex\Application en este caso).

Si el post solicitado no existe, se ejecuta el método abort() para detener la ejecución de la aplicación lo antes posible. En realidad, este método lanza una excepción, tal y como se explicará más adelante.

2.4.3. Ejemplo de cómo definir una ruta POST

Las rutas de tipo POST se utilizan para crear nuevos recursos en el servidor. El típico ejemplo de estas rutas es un formulario de contacto, cuyos contenidos se envían por email haciendo uso de la función mail() de PHP:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
 
$app->post('/feedback', function (Request $request) {
    $message = $request->get('message');
    mail('feedback@yoursite.com', '[YourSite] Comentarios', $message);
 
    return new Response('Gracias por tus comentarios', 201);
});

Su funcionamiento es realmente tan sencillo como parece.

Nota Si en vez de la función mail() quieres utilizar un componente de envío de emails con más funcionalidades, puedes hacer uso del proveedor SwiftmailerServiceProvider.

Observa como este código también hace uso del "type hinting" para pedir a Silex que inyecte automáticamente el objeto $request que contiene toda la información de la petición del usuario.

A partir del objeto pasado de tipo Request, puedes acceder directamente a cualquier variable de la petición con el método get().

Observa también cómo el código anterior no devuelve una cadena de texto con el código HTML de la página sino un objeto de tipo Response. Aunque es un poco más larga de escribir, esta forma permite añadir cabeceras HTTP (como la que pone el código de estado de la respuesta a 201).

Nota Silex siempre utiliza internamente un objeto de tipo Response, por lo que convierte tus cadenas de texto en objetos Response con el código de estado 200.

2.4.4. Ejemplo de cómo definir rutas con otros métodos

Cada método HTTP puede definir sus propios controladores. Para ello solo debes utilizar los métodos: get(), post(), put(), delete(), patch() y options().

$app->put('/blog/{id}', function ($id) {
    // ...
});
 
$app->delete('/blog/{id}', function ($id) {
    // ...
});
 
$app->patch('/blog/{id}', function ($id) {
    // ...
});

Truco En la mayoría de navegadores, los formularios solo pueden utilizar los métodos GET y POST. Para evitar esta limitación, se utiliza el truco de aplicar el método POST al formulario y añadir un campo de formulario llamado _method que contiene el nombre del verdadero método HTTP que se quiere utilizar.

<form action="/my/target/route/" method="post">
    <!-- ... -->
    <input type="hidden" id="_method" name="_method" value="PUT" />
</form>

Para que el truco del campo _method functione, debes modificar ligeramente la aplicación de la siguiente manera:

use Symfony\Component\HttpFoundation\Request;
 
Request::enableHttpMethodParameterOverride();
$app->run();

Además de estos métodos, también existe otro método llamado match() que funciona para todos los métodos HTTP a la vez:

// este controlador response a cualquier método HTTP
$app->match('/blog', function () {
    // ...
});

Aunque utilices el método match(), es posible restringir los métodos HTTP para los que funciona:

$app->match('/blog', function () {
    // ...
})
->method('PATCH');
 
// utiliza expresiones regulares para admitir más de un método HTTP
$app->match('/blog', function () {
    // ...
})
->method('PUT|POST');

Nota El orden en el que se definen las rutas dentro del archivo de la aplicación es muy importante. Silex siempre utiliza la primera ruta cuyo patrón coincida con la URL solicitada por el usuario, así que debes colocar las rutas genéricas lo más tarde posible.

2.4.5. Variables en las rutas

Tal y como se mostró anteriormente, las rutas pueden contener partes variables en su patrón, que después se convierten en argumentos de la función anónima del controlador:

$app->get('/blog/{id}', function ($id) {
    // ...
});

Las rutas pueden contener más de una variable, pero siempre debes asegurarte de que los argumentos de la función coinciden con los de la ruta (tanto en número como en nombre):

$app->get('/blog/{postId}/{commentId}', function ($postId, $commentId) {
    // ...
});

No es muy recomendable hacerlo, pero puedes cambiar el orden de los argumentos si quieres (a Silex solo le importa su nombre):

$app->get('/blog/{postId}/{commentId}', function ($commentId, $postId) {
    // ...
});

Además de las variables de la ruta, es posible acceder a los objetos Request y Application que representan, respectivamente, la petición del usuario y la aplicación:

$app->get('/blog/{id}', function (Application $app, Request $request, $id) {
    // ...
});

Nota Recuerda que como Silex utiliza el type hinting, para las variables especiales Request y Application solo importa que incluyas por delante el nombre de la clase. En otras palabras, el nombre de la variable no importa en absoluto, por lo que puedes cambiarlo si quieres:

$app->get('/blog/{id}', function (Application $foo, Request $bar, $id) {
    // ...
});

2.4.6. Modificando las variables de la ruta

Silex permite modificar el valor de las variables antes de pasarlas al controlador:

$app->get('/user/{id}', function ($id) {
    // ...
})->convert('id', function ($id) { return (int) $id; });

Esta técnica es muy útil para reutilizar en varios controladores el mismo código que convierte por ejemplo las variables de la ruta en objetos:

$userProvider = function ($id) {
    return new User($id);
};
 
$app->get('/user/{user}', function (User $user) {
    // ...
})->convert('user', $userProvider);
 
$app->get('/user/{user}/edit', function (User $user) {
    // ...
})->convert('user', $userProvider);

La función que se ejecuta para modificar las variables también recibe el objeto Request como segundo argumento:

$callback = function ($post, Request $request) {
    return new Post($request->attributes->get('slug'));
};
 
$app->get('/blog/{id}/{slug}', function (Post $post) {
    // ...
})->convert('post', $callback);

El código que modifica las variables también puede ser un servicio. El siguiente ejemplo muestra cómo usar el ObjectManager de Doctrine para convertir el valor $id en un usuario:

use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
 
class UserConverter
{
    private $om;
 
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }
 
    public function convert($id)
    {
        if (null === $user = $this->om->find('User', (int) $id)) {
            throw new NotFoundHttpException(sprintf('El usuario %d no existe.', $id));
        }
 
        return $user;
    }
}

Ahora ya sólo tienes que registrar ese servicio en la aplicación y usarlo como función para transformar variables:

$app['converter.user'] = $app->share(function () {
    return new UserConverter();
});
 
$app->get('/user/{user}', function (User $user) {
    // ...
})->convert('user', 'converter.user:convert');

2.4.7. Requisitos

Resulta muy común tener que restringir las rutas a unos determinados valores de sus variables. Estos requisitos se definen mediante expresiones regulares pasadas al método assert() del controlador.

El siguiente código muesta cómo asegurar que el valor de la variable id es un número (la expresión regular \d+ se cumple para cualquier número entero positivo):

$app->get('/blog/{id}', function ($id) {
    // ...
})
->assert('id', '\d+');

Para establecer varios requisitos, encadena llamadas a este método:

$app->get('/blog/{postId}/{commentId}', function ($postId, $commentId) {
    // ...
})
->assert('postId', '\d+')
->assert('commentId', '\d+');

2.4.8. Valores por defecto

Los valores por defecto de las variables se pueden definir con el método value():

$app->get('/{pageName}', function ($pageName) {
    // ...
})
->value('pageName', 'index');

El código anterior hace que la ruta también se ejecute cuando el usuario solicite la URL /, en cuyo caso la variable pageName tomará el valor index.

2.4.9. Rutas con nombre

Como se verá más adelante, algunos proveedores (como por ejemplo UrlGeneratorProvider) utilizan nombres para referirse a las rutas. Por defecto Silex asigna un nombre generado automáticamente a cada ruta, pero lo mejor es que emplees el método bind() para asignar tus propios nombres a las rutas:

$app->get('/', function () {
    // ...
})
->bind('homepage');
 
$app->get('/blog/{id}', function ($id) {
    // ...
})
->bind('blog_post');

2.4.10. Controladores como métodos de clases

Si no te gusta definir los controladores como funciones anónimas, también puedes definirlos como métodos dentro de una clase. Para ello utiliza la sintaxis ClaseDelControlador::nombreDelMetodo. Silex solo instanciará esa clase si se ejecuta el controlador, por lo que el rendimiento de la aplicación no se ve afectado:

$app->get('/', 'Acme\\Foo::bar');
 
use Silex\Application;
use Symfony\Component\HttpFoundation\Request;
 
namespace Acme
{
    class Foo
    {
        public function bar(Request $request, Application $app)
        {
            ...
        }
    }
}

Cuando un usuario solicita la portada del sitio, el código anterior hace que Silex instancie la clase Acme\Foo y ejecute el método bar() para obtener la respuesta enviada al usuario. Si necesitas acceso a la petición o a la aplicación, utiliza la técnica habitual de añadir las clases Request y Silex\Application como argumentos.

Los controladores también se pueden definir como servicios, lo que permite desacoplarlos todavía más respecto a Silex.