Usándola

Este capítulo describe cómo utilizar Silex.

Instalando

Si quieres empezar rápidamente, descarga Silex como un archivo y descomprímelo, deberías tener la siguiente estructura de directorios:

├── composer.json
├── composer.lock
├── vendor
│   └── ...
└── web
    └── index.php

Si quieres más flexibilidad, en su lugar usa Composer. Crea un archivo composer.json:

{
    "require": {
        "silex/silex": "1.0.*@dev"
    }
}

Y ejecuta Composer para instalar Silex y todas sus dependencias:

$ curl -s http://getcomposer.org/installer | php
$ php composer.phar install

Truco

De manera predeterminada, Silex confía en los componentes estables de Symfony. Si en su lugar, quieres utilizar su versión maestra, añade "minimum-stability": "dev" en tu archivio composer.json.

Actualizando

Actualizar Silex a la versión más reciente es tan fácil como ejecutar la orden update:

$ php composer.phar update

Arranque

Para arrancar Silex, todo lo que necesitas hacer es requerir el archivo vendor/autoload.php y crear una instancia de Silex\Application. Después de definir tu controlador, llama al método run en tu aplicación:

// web/index.php

require_once __DIR__.'/../vendor/autoload.php';

$app = new Silex\Application();

// definiciones

$app->run();

Luego, debes configurar tu servidor web (consulta el capítulo dedicado para más información).

Truco

Cuando desarrollas un sitio web, posiblemente quieras activar el modo de depuración para que se te facilite la corrección de errores:

$app['debug'] = true;

Truco

Si tu aplicación se encuentra detrás de un delegado inverso y deseas que Silex compruebe las cabeceras X-Forwarded-For*, tendrás que ejecutar tu aplicación de esta manera:

use Symfony\Component\HttpFoundation\Request;

Request::trustProxyData();
$app->run();

Enrutado

En Silex defines simultáneamente una ruta y el controlador que se invocará cuando dicha ruta concuerde

Un patrón de ruta se compone de:

  • Pattern: El patrón de ruta define una ruta que apunta a un recurso. El patrón puede incluir partes variables y tú podrás establecer los requisitos con expresiones regulares.
  • Method: Uno de los siguientes métodos HTTP: GET, POST, PUT o DELETE. Este describe la interacción con el recurso. Normalmente sólo se utilizan GET y POST, pero, también es posible utilizar los otros.

El controlador se define usando un cierre de esta manera:

function () {
    // hace algo
}

Los cierres son funciones anónimas que pueden importar el estado desde fuera de su definición. Esto es diferente de las variables globales, porque el estado exterior no tiene que ser global. Por ejemplo, podrías definir un cierre en una función e importar variables locales desde esa función.

Nota

Los cierres que no importan el ámbito se conocen como lambdas. Debido a que en PHP todas las funciones anónimas son instancias de la clase Closure, no vamos a hacer una distinción aquí.

El valor de retorno del cierre se convierte en el contenido de la página.

Ejemplo de ruta GET

He aquí un ejemplo de una definición de ruta GET:

$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;
});

Al visitar /blog devolverá una lista con los títulos de los artículos en el blog. La declaración use significa algo diferente en este contexto. Esta instruye al cierre para que importe la variable $blogPosts desde el ámbito externo. Esto te permite utilizarla dentro del cierre.

Enrutado dinámico

Ahora, puedes crear otro controlador para ver artículos individuales del blog:

$app->get('/blog/show/{id}', function (Silex\Application $app, $id) use ($blogPosts) {
    if (!isset($blogPosts[$id])) {
        $app->abort(404, "Post $id does not exist.");
    }

    $post = $blogPosts[$id];

    return  "<h1>{$post['title']}</h1>".
            "<p>{$post['body']}</p>";
});

Esta definición de ruta tiene una parte variable {id} que se pasa al cierre.

Cuando el artículo no existe, usamos abort() para detener la petición inicial. En realidad, se produce una excepción, la cual veremos cómo manejar más adelante.

Ejemplo de ruta POST

Las rutas POST denotan la creación de un recurso. Un ejemplo de esto es un formulario de comentarios. Vamos a utilizar la función mail para enviar un correo electrónico:

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] Feedback', $message);

    return new Response('Thank you for your feedback!', 201);
});

Es bastante sencillo.

Nota

Hay un SwiftmailerServiceProvider incluido que puedes utilizar en lugar de mail().

La petición actual es inyectada al cierre automáticamente por Silex gracias a la insinuación de tipo. Es una instancia de Request, por tanto puedes recuperar las variables usando el método get de la petición.

En lugar de devolver una cadena regresamos una instancia de Response. Esto nos permite fijar un código de estado HTTP, en este caso configurado a 201 Creado.

Nota

Internamente, Silex siempre utiliza una Respuesta, la convierte a cadenas para respuestas con código de estado 200 OK.

Otros métodos

Puedes crear controladores para la mayoría de los métodos HTTP. Sólo tienes que llamar a uno de estos métodos en tu aplicación: get, post, put o delete. También puedes invocar a match, el cual coincidirá con todos los métodos:

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

Entonces puedes restringir los métodos permitidos a través del método method:

$app->match('/blog', function () {
    ...
})
->method('PATCH');

Puedes sincronizar varios métodos con un controlador utilizando la sintaxis de expresiones regulares:

$app->match('/blog', function () {
    ...
})
->method('PUT|POST');

Nota

El orden en que definas las rutas es importante. La primera ruta que coincida se utilizará, por lo tanto coloca tus rutas más genéricas en la parte inferior.

Variables de ruta

Como mostramos antes, puedes definir partes variables en una ruta, como esta:

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

También es posible tener más de una parte variable, basta con que encierres los argumentos coincidentes con los nombres de las partes variables:

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

Si bien no se sugiere, también lo puedes hacer (ten en cuenta la conmutación de los argumentos):

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

También puedes consultar la Petición actual y el objeto Aplicación:

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

Nota

Ten en cuenta que para los objetos Aplicación y Petición, Silex hace la inyección basándose en el indicador de tipo y no en el nombre de la variable:

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

Convertidores de variables de ruta

Antes de inyectar las variables de ruta en el controlador, puedes aplicar algunos convertidores:

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

Esto es útil cuando quieres convertir las variables de ruta a objetos, ya que permite reutilizar el código de conversión entre diferentes controladores:

$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 retrollamada al convertidor también recibe la Petición 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);

Requisitos

En algunos casos es posible que sólo desees detectar ciertas expresiones. Puedes definir los requisitos usando expresiones regulares llamando a assert en el objeto Controller, que es devuelto por los métodos de enrutado.

Lo siguiente se asegurará de que el argumento id es numérico, ya que \d+ coincide con cualquier cantidad de dígitos:

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

También puedes encadenar estas llamadas:

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

valores predeterminados

Puedes definir un valor predeterminado para cualquier variable de ruta llamando a value en el objeto Controlador:

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

Esto te permitirá coincidir /, en cuyo caso la variable nombrePagina tendrá el valor de index.

Rutas con nombre

Algunos proveedores (como UrlGeneratorProvider) pueden usar rutas con nombre. De manera predeterminada Silex generará un nombre de ruta para ti, el cual, no puedes utilizar realmente. Puedes dar un nombre a una ruta llamando a bind en el objeto Controlador devuelto por los métodos de enrutado:

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

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

Nota

Sólo tiene sentido nombrar rutas si utilizas proveedores que usan la RouteCollection.

Controladores en clases

Si no quieres utilizar funciones anónimas, también puedes definir tus controladores como métodos. Al utilizar la sintaxis ControllerClass::methodName, le puedes decir a Silex que cree por ti el objeto controlador de manera diferida:

$app->get('/', 'Igorw\Foo::bar');

use Silex\Application;
use Symfony\Component\HttpFoundation\Request;

namespace Igorw
{
    class Foo
    {
        public function bar(Request $request, Application $app)
        {
            ...
        }
    }
}

Esto cargará la clase Igorw\Foo bajo demanda, crea una instancia y llama al método bar para conseguir la respuesta. Puedes utilizar la insinuación de tipo Request y Silex\Application para inyectar la $request y $app.

Para una separación incluso más fuerte entre Silex y tus controladores, puedes definir tus controladores como servicios.

Configuración global

Si una opción del controlador se debe aplicar a todos los controladores (un convertidor, un servicio de lógica intermedia, un requisito o un valor predeterminado), los puedes configurar en $application['controllers'], que tiene todos los controladores de la aplicación:

$app['controllers']
    ->value('id', '1')
    ->assert('id', '\d+')
    ->requireHttps()
    ->method('get')
    ->convert('id', function () { /* ... */ })
    ->before(function () { /* ... */ })
;

Estos ajustes se aplican a los controladores que ya están registrados y se convierten en los valores predefinidos para los nuevos controladores.

Nota

La configuración global no aplica a proveedores de controlador, podrías montar tantos como tengas en tu propia configuración global (ve el párrafo sobre la modularidad más adelante).

Controladores de error

Si alguna parte de tu código produce una excepción de la que desees mostrar algún tipo de página de error al usuario. Esto es lo que hacen los controladores de error. También los puedes utilizar para hacer cosas adicionales, tal como registrar eventos cronológicamente.

Para registrar un controlador de error, pasa un cierre al método error el cual toma un argumento Exception y devuelve una respuesta:

use Symfony\Component\HttpFoundation\Response;

$app->error(function (\Exception $e, $code) {
    return new Response('We are sorry, but something went terribly wrong.');
});

También puedes comprobar si hay errores específicos usando el argumento $code, y manejándolo de manera diferente:

use Symfony\Component\HttpFoundation\Response;

$app->error(function (\Exception $e, $code) {
    switch ($code) {
        case 404:
            $message = 'The requested page could not be found.';
            break;
        default:
            $message = 'We are sorry, but something went terribly wrong.';
    }

    return new Response($message);
});

Nota

Debido a que Silex garantiza que el código de estado de la respuesta se ajusta al más adecuado en función de la excepción, configurar el estado en la respuesta no va a funcionar. Si quieres reescribir el código de estado (no sin una muy buena razón), establece la cabecera X-Status-Code:

return new Response('Error', 404 /* ignored */, array('X-Status-Code' => 200));

Puedes restringir un controlador de errores para que sólo maneje algunas clases de excepciones estableciendo un tipo de pista más específico para el argumento del Cierre:

$app->error(function (\LogicException $e, $code) {
    // este controlador sólo ve excepciones \LogicException
    // y \LogicException extendidas
 });

Si deseas configurar el registro cronológico de eventos, para ello, puedes utilizar un controlador de errores independiente. Sólo asegúrate de registrarlo antes que los controladores que responden al error, porque una vez que se devuelve una respuesta, se omiten los demás controladores.

Nota

Silex viene con un proveedor para Monolog el cual maneja el registro de errores. Échale un vistazo al capítulo Proveedores para más detalles.

Truco

Silex viene con un controlador de errores predeterminado que muestra un mensaje de error detallado con el seguimiento de la pila cuando debug es true, y de otra manera un mensaje de error simple. Los manipuladores de error registrados a través del método error() siempre tienen prioridad, pero puedes mantener agradables los mensajes de error de depuración cuando se enciende con algo como esto:

use Symfony\Component\HttpFoundation\Response;

$app->error(function (\Exception $e, $code) use ($app) {
    if ($app['debug']) {
        return;
    }

    // lógica para manejar el error y devolver una respuesta
});

A los manipuladores de error también se les llama cuando utilizas abort para anular prematuramente una petición:

$app->get('/blog/show/{id}', function (Silex\Application $app, $id) use ($blogPosts) {
    if (!isset($blogPosts[$id])) {
        $app->abort(404, "Post $id does not exist.");
    }

    return new Response(...);
});

Redirigiendo

Puedes redirigir a otra página devolviendo una respuesta de redirección, la cual puedes crear mediante una llamada al método redirect:

$app->get('/', function () use ($app) {
    return $app->redirect('/hello');
});

Esto redirigirá de / a /hello.

Reenviando

Cuando quieres delegar la reproducción a otro controlador, sin una ida y vuelta al navegador (tal como una redirección), utiliza una subpetición:

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;

$app->get('/', function () use ($app) {
    // redirige a /hello
    $subRequest = Request::create('/hello', 'GET');

    return $app->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
});

Truco

Si estás usando UrlGeneratorProvider, también puedes generar la URI:

$request = Request::create($app['url_generator']->generate('hello'), 'GET');

No obstante, hay algunas cosas más que necesitas tener en cuenta. En muchos casos querrás reenviar algunas partes de la petición maestra actual a la subpetición. Estas incluyen: Galletas, información del servidor, sesión. Lee más en cómo hacer subpeticiones.

JSON

Si quieres devolver datos JSON, puedes usar el método ayudante json. Simplemente le tienes que proporcionar tus datos, código de estado y cabeceras, y este creará una respuesta JSON para ti:

$app->get('/users/{id}', function ($id) use ($app) {
    $user = getUser($id);

        if (!$user) {
        $error = array('message' => 'The user was not found.');
        return $app->json($error, 404);
    }

    return $app->json($user);
});

Transmitiendo secuencias

Es posible crear una respuesta para la transmisión de secuencias, la cual es importante en los casos cuando no puedes mantener en memoria los datos enviados:

$app->get('/images/{file}', function ($file) use ($app) {
    if (!file_exists(__DIR__.'/images/'.$file)) {
        return $app->abort(404, 'The image was not found.');
    }

    $stream = function () use ($file) {
        readfile($file);
    };

    return $app->stream($stream, 200, array('Content-Type' => 'image/png'));
});

Si necesitas enviar segmentos, asegúrate de llamar a ob_flush y flush después de cada parte:

$stream = function () {
    $fh = fopen('http://www.ejemplo.com/', 'rb');
    while (!feof($fh)) {
      echo fread($fh, 1024);
      ob_flush();
      flush();
    }
    fclose($fh);
};

Peculiaridades

Silex viene con características de PHP que definen accesos directos a métodos.

Prudencia

Es necesario que utilices PHP 5.4 o más reciente para beneficiarte de esta característica.

Casi todos los proveedores de servicio integrados tienen alguna característica PHP correspondiente. Para usarlos, define tu propia clase Application e incluye las características que desees:

use Silex\Application;

class MyApplication extends Application
{
    use Application\TwigTrait;
    use Application\SecurityTrait;
    use Application\FormTrait;
    use Application\UrlGeneratorTrait;
    use Application\SwiftmailerTrait;
    use Application\MonologTrait;
    use Application\TranslationTrait;
}

También puedes definir tu propia clase Route y usar algunas características:

use Silex\Route;

class MyRoute extends Route
{
    use Route\SecurityTrait;
}

Para usar tu ruta recién definida, sustituye la configuración de $app['route_class']:

$app['route_class'] = 'MyRoute';

Lee cada capítulo de proveedor para obtener más información acerca de los métodos añadidos.

Seguridad

Asegúrate de proteger tu aplicación contra ataques.

Escapando

Cuando reproduces cualquier aportación de los usuarios (ya sea en las variables GET/POST o en variables obtenidas desde la petición), tendrás que asegurarte de escaparlas correctamente, para evitar ataques que exploten vulnerabilidades del sistema.

  • Escapando HTML: PHP proporciona la función htmlspecialchars para esto. Silex ofrece un atajo, el método escape:

    $app->get('/name', function (Silex\Application $app) {
        $name = $app['request']->get('name');
        return "You provided the name {$app->escape($name)}.";
    });
    

    Si utilizas el motor de plantillas Twig debes usar su escape o, incluso, mecanismos de autoescape.

  • Escapando JSON: Si quieres proporcionar datos en formato JSON debes utilizar la función json de Silex:

    $app->get('/name.json', function (Silex\Application $app) {
        $name = $app['request']->get('name');
        return $app->json(array('name' => $name));
    });
    
Bifúrcame en GitHub