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 elContent-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.