Aprende Symfony2 (Parte 4): Controladores

3 de octubre de 2014

Este es el cuarto artículo de la serie para aprender sobre el framework Symfony2. En los anteriores artículos comenzamos a crear una aplicación vacía de un solo bundle con los siguientes archivos:

.
├── app
│   ├── AppKernel.php
│   ├── cache
│   │   └── .gitkeep
│   ├── config
│   │   └── config.yml
│   └── logs
│       └── .gitkeep
├── composer.json
├── composer.lock
├── src
│    └── AppBundle
│         └── AppBundle.php
├── .gitignore
└── web
    └── app.php

Ejecutar el comando composer install debería crear el directorio vendor/, que hemos ignorado en Git. Si lo necesitas, echa un vistazo al repositorio de código público en el que estamos desarrollando la aplicación. En este artículo, aprenderemos más sobre el enrutamiento y los controladores.

Conociendo el enrutamiento y los controladores

Para familiarizarnos con el enrutamiento y los controladores, crearemos una ruta que no devuelve nada. Lo primero que hay que hacer es configurar el router:

# app/config/app.yml
framework:
    secret: "El valor de esta opción debe ser una cadena aleatoria."
    router:
        resource: %kernel.root_dir%/config/routing.yml

Ahora podemos escribir nuestras rutas en un archivo aparte:

# app/config/routing.yml
cotizacion:
    path: /api/{empresa}
    methods:
        - GET
    defaults:
        _controller: AppBundle:Api:cotizacion

Como ves, una ruta tiene:

  • un nombre (cotizacion).
  • un patrón (/api/{empresa}) en el que las partes encerradas con { y } indican que se trata de una parte variable.
  • uno o más verbos HTTP (GET).
  • un controlador AppBundle\Controller\ApiController::cotizacionAction()

NOTA El parámetro _controller es un atajo compuesto de tres partes, que son el nombre del bundle, después el nombre del controlador sin sufijo, y finalmente el nombre del método sin sufijo.

Ahora necesitamos crear el siguiente directorio:

$ mkdir src/AppBundle/Controller

Y la siguiente clase o controlador:

<?php
// src/AppBundle/Controller/ApiController.php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ApiController extends Controller
{
    public function cotizacionAction(Request $request, $empresa)
    {
        return new Response('', Response::HTTP_NO_CONTENT);
    }
}

Para probarlo, te recomiendo usar un cliente HTTP, como por ejemplo HTTPie, que puedes instalar de la siguiente manera en sistemas Linux:

$ sudo apt-get install python-pip
$ sudo pip install --upgrade httpie

Ya podemos probar nuestro webservice (cambia app.local por el nombre del host local que hayas creado para tu aplicación):

$ http GET app.local/api/cotizacion/ACME

La primera línea de la respuesta debería ser HTTP/1.1 204 No Content.

Enviando datos a nuestra aplicación

Nuestro scrum master y nuestro cliente han escrito un esquema con el funcionamiento deseado para la aplicación:

Como usuario, quiero un nuevo webservice que al pasarle el código de una empresa me devuelva toda su información y no simplemente su última cotización

Esto significa que vamos a necesitar la siguiente ruta:

# app/config/routing.yml
informacion:
    path: /api/informacion
    methods:
        - POST
    defaults:
        _controller: AppBundle:Api:informacion

Nuestro controlador recogerá el valor pasado mediane POST (llamado empresa), comprobará si es un código válido de empresa (por jemplo ACME), y en caso afirmativo, devolverá toda su información. En caso contrario, la respuesta contendrá simplemente un mensaje de error:

<?php
// src/AppBundle/Controller/ApiController.php

namespace AppBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;

class ApiController extends Controller
{
    public function informacionAction(Request $request)
    {
        $contenidoDeLaPeticion = $request->getContent();
        $contenidoEnviado = json_decode($contenidoDeLaPeticion, true);

        if (!isset($contenidoEnviado['empresa'])
            || 'ACME' !== $contenidoEnviado['empresa']) {
            $respuesta['mensaje'] = 'ERROR - Empresa desconocida';
            $codigoEstado = Response::HTTP_UNPROCESSABLE_ENTITY;
        } else {
            $respuesta['mensaje'] = 'OK';
            $respuesta['empresa'] = array(...);
            $codigoEstado = Response::HTTP_OK;
        }

        return new JsonResponse($respuesta, $codigoEstado);
    }
}

La clase JsonResponse convierte el array a JSON y establece las cabeceras HTTP correctas. Si ahora intentamos enviar algo incorrecto, como esto:

$ http POST app.local/api/informacion empresa=NO_EXISTE

Deberíamos obtener una respuesta parecida a:

HTTP/1.1 422 Unprocessable Entity
Cache-Control: no-cache
Content-Type: application/json
Date: Thu, 10 Jul 2014 15:23:00 GMT
Server: Apache
Transfer-Encoding: chunked

{
    "mensaje": "ERROR - Empresa desconocida"
}

Y cuando enviemos la ofrenda correcta:

$ http POST app.local/api/informacion empresa=ACME

Deberíamos obtener algo similar a:

HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Date: Thu, 10 Jul 2014 21:42:00 GMT
Server: Apache
Transfer-Encoding: chunked

{
    "mensaje": "OK",
    "empresa": { ... }
}

La API de la clase Request

Esta es parte de la API de la clase Request:

<?php

namespace Symfony\Component\HttpFoundation;

class Request
{
    public $request; // parámetros enviados mediante POST ($_POST)
    public $query;   // parámetros enviados en la "query string" ($_GET)
    public $files;   // archivos subidos ($_FILES)
    public $cookies; // cookies $_COOKIE
    public $headers; // cabeceras de la petición obtenidas de $_SERVER

    public static function createFromGlobals():
    public static function create(
        $uri,
        $method = 'GET',
        $parameters = array(),
        $cookies = array(),
        $files = array(),
        $server = array(),
        $content = null
    );

    public function getContent($asResource = false);
}

Usamos createFromGlobals en nuestro controlador frontal (web/app.php), y hace exactamente lo que dice: inicializa la petición con la información obtenida mediante las variables superglobales de PHP ($_POST, $_GET, etc).

El método create es realmente útil en tests, dado que no necesitaremos sobreescribir los valores de las variables superglobales de PHP.

Todos los atributos que aparecen listados son instancias de la clase Symfony\Component\HttpFoundation\ParameterBag, que es como un array orientado a objetos, con los métodos set, has y get (entre otros).

Cuando envías un formulario, tu navegador establece automáticamente el parámetro Content-Type de la cabecera de la petición HTTP a application/x-www-form-urlencoded, y los valores del formulario son enviados en el contenido de la peticion de esta forma:

empresa=ACME

PHP entiende esta petición, y guarda los valores en la variable superglobal $_POST. Por eso puedes acceder a ese valor de la siguiente manera:

$request->request->get('empresa');

Sin embargo, cuando enviamos algo en JSON con el Content-Type igual a application/json, PHP no rellena $_POST. Así que tendrás que recuperar los datos originales enviados mediante getContent y convertirlos después usando json_decode, como hemos hecho en nuestro controlador.

La API de la clase Response

Esta es parte de la API de la clase Response:

<?php

namespace Symfony\Component\HttpFoundation;

class Response
{
    const HTTP_OK = 200;
    const HTTP_CREATED = 201;
    const HTTP_NO_CONTENT = 204;
    const HTTP_UNAUTHORIZED = 401;
    const HTTP_FORBIDDEN = 403;
    const HTTP_NOT_FOUND = 404;
    const HTTP_UNPROCESSABLE_ENTITY = 422; // RFC4918

    public $headers; // @var Symfony\Component\HttpFoundation\ResponseHeaderBag

    public function __construct($content = '', $status = 200, $headers = array())

    public function getContent();
    public function getStatusCode();

    public function isSuccessful();
}

Hay muchas constantes de códigos de estado HTTP, así que he seleccionado solamente los que más uso.

Puedes establecer y recuperar las cabeceras de Response mediante una propiedad pública, que también es de tipo ParameterBag.

El constructor te permite establecer el contenido, el código de estado y las cabeceras. Los otros tres métodos se usan sobre todo en tests. Hay muchos métodos is para comprobar el tipo de petición, pero normalmente lo único que te interesará será saber que la respuesta es correcta.

Existen además otros tipos de respuesta:

  • JsonResponse: establece el Content-Type y convierte el contenido a JSON.
  • BinaryFileResponse: establece las cabeceras y adjunta un archivo a la respuesta.
  • RedirectResponse: establece el destino para una redirección.
  • StreamedResponse: útil para el streaming de archivos muy grandes.

Conclusión

Symfony2 es un framework HTTP, cuyas principales API son los controladores: reciben como parámetro una petición y devuelven una respuesta. Todo lo que tenemos que hacer es crear un controlador, escribir una configuración mínima para enlazarlo a una URL ¡y ya está!

No olvides hacer un commit al repositorio:

$ git add -A
$ git commit -m 'Creada la ruta Ni y el controlador'

El próximo artículo tratará sobre tests: ¡permanece atento!

Sobre el autor

Este artículo fue publicado originalmente por Loïc Chardonnet y ha sido traducido con permiso por Manuel Gómez.

Artículos de la serie Aprende Symfony