RESTful & PHP Formatos de Representação

Introdução

Mais uma vez aprendendo conceitos RESTful com foco na linguagem PHP com apoio do micro-framework Silex, vamos mostrar neste artigo, como você pode disponibilizar várias representações de dados na mesma API. No caso, retornando JSON e XML, permitindo que o cliente que esta consumindo a API informe qual é o formato que deseja receber, ou qual formato ele sabe lidar, utilizando o header Accept na mensagem de requisição HTTP. Nossa implementação no lado do servidor deve saber analisar o header Accept e a partir dele retornar os dados na representação desejada.

Formatos de Representações

Quando falamos em APIs RESTful, não existe uma regra determinística que diz que os dados devem ser representados no formato X ou Y. Estamos abertos para representar em qualquer formato, levando em consideração que os mais comuns são JSON e XML. Nada impede que você trabalhe com outros formatos, como html, xhtml e até mesmo texto puro. Segue um pequeno descritivo dos formatos mais comuns:

JSON

JSON (Javascript Object Notation) é caracterizado pela facilidade e simplicidade em representação de dados. Se você esta familiarizado com webservices RESTful, já deve ter percebido que JSON é bastante popular. O formato tem baixa complexidade. Para humanos, é fácil para ler e escrever. Para máquinas, é fácil para tratar e gerar e pode ser facilmente lido e escrito em qualquer linguagem de programação atual.

Muito provavelmente você já trabalha e esta familiarizado com este formato em sua linguagem de programação de trabalho.

Media Type

application/json

XML

XML é outro formato muito comum utilizados em webservices. Com XML você consegue representar muito mais informações do que JSON, devido possuir mais recursos. (tags, atributos, namespaces, etc.). Porém acaba tendo um custo maior pois uma representação em XML trafegará mais dados do que a mesma informação trafegada em JSON. Outro ponto é que a complexidade de se trabalhar com XML é maior do que JSON na maioria das linguagens.

Apesar disso, dependendo do cliente que esta consumindo sua API, será necessário permitir XML. Muitas aplicações corporativas baseadas em Java, .NET, utilizam XML como padrão de representação de dados. Nesses casos é importante permitir tanto JSON como XML.

Media Type

application/xml

Exemplo de Implementação

Vamos criar uma simples aplicação PHP com Silex para ilustrar que podemos devolver formatos de representação diferentes de acordo com o header Accept, informado pelo cliente durante a requisição HTTP.

O controle será realizado usando o header Accept. O cliente deve informar:

Accept: application/json

ou

Accept: application/xml

Para ajudar na implementação vamos utilizar o apoio do Silex. A melhor forma de instalar o silex é através do composer. Crie o arquivo composer.json com o seguinte conteúdo:

{
    "require": {
        "silex/silex": "~1.3"
    }
}

Execute o install do composer:

composer install

Vamos utilizar a ajuda também do SerializerServiceProvider, um Service Provider para o Silex para serializar objetos e arrays para JSON ou XML. A instalação deve ser feita através do composer também:

composer require symfony/serializer

Caso queira mais dicas sobre criação de APIs REST com Silex, sugiro ler minha outra série de artigos sobre o assunto: Aplicação REST simples com Silex, parte I e Aplicação REST simples com Silex, parte II.

No Silex, vamos implementar um middleware before que será executado antes do processamento de qualquer requisição enviado para qualquer rota configurada no Silex. Nele vamos verificar o header de requisição Accept, identificando se o cliente deseja o formato JSON ou XML. Após identificar o mime type, vamos associá-lo à uma variável $app[‘config.format’] que poderá ser consultada dentro de qualquer rota criada no silex.

// middleware application
$app->before(function (Request $request, Application $app) {
    $acceptRequestHeader = $request->headers->get('Accept');
    switch($acceptRequestHeader) {
        case 'application/xml':
            $format = 'xml';
            break;

        case 'application/json':
            $format = 'json';
            break;

        default:
            $format = 'json';
    }
    $app['config.format'] = $format;
});

Por padrão, caso não seja possível determinar o formato informado no header Accept, vamos utilizar o formato JSON.

Vamos criar a rota GET /livros/{id} que deverá retornar os dados de determinado livro baseado no valor do campo {id} passado como parâmetro. No caso temos apenas um livro na coleção, com o id => 1, com o propósito de ilustração:

$app = new Silex\Application();
$app->register(new SerializerServiceProvider());

// coleacao de livros
$livros = [ 1 => [
    'titulo' => 'PHP Moderno',
    'autor' => 'Josh Lockhart',
    'isbn' => '978-85-7522-428-1',
]];

// GET 
$app->get("/livros/{id}", function ($id, Request $request) use ($app, $livros) {
    if(!isset($livros[$id])) {
        $app->abort(404, "Livro nao encontrado para o id: $id");
    }
    $livro = $livros[$id];

    return new Response($app['serializer']->serialize($livro, $app['config.format']), 200, array(
        "Content-Type" => $app['request']->getMimeType($app['config.format'])
    ));
})->assert("id", "\d+");

Usamos a variável $app[‘config.format’] como parâmetro para o serialize() para formatar os dados no formato desejado. Também é necessário informarmos o header Content-type na requisição de resposta para que o cliente possa saber qual formato de dados esta recebendo.

Colocando tudo junto:

<?php
// timezone
date_default_timezone_set('America/Sao_Paulo');

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

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Silex\Application;
use Silex\Provider\SerializerServiceProvider;

$app = new Silex\Application();
$app->register(new SerializerServiceProvider());

// coleacao de livros
$livros = [ 1 => [
    'titulo' => 'PHP Moderno',
    'autor' => 'Josh Lockhart',
    'isbn' => '978-85-7522-428-1',
]];

// middleware application
$app->before(function (Request $request, Application $app) {
    $acceptRequestHeader = $request->headers->get('Accept');
    switch($acceptRequestHeader) {
        case 'application/xml':
            $format = 'xml';
            break;

        case 'application/json':
            $format = 'json';
            break;

        default:
            $format = 'json';
    }
    $app['config.format'] = $format;
});

// GET 
$app->get("/livros/{id}", function ($id, Request $request) use ($app, $livros) {
    if(!isset($livros[$id])) {
        $app->abort(404, "Livro nao encontrado para o id: $id");
    }
    $livro = $livros[$id];

    return new Response($app['serializer']->serialize($livro, $app['config.format']), 200, array(
        "Content-Type" => $app['request']->getMimeType($app['config.format'])
    ));
})->assert("id", "\d+");

$app->run();

Testando

Vamos usar o curl para testar nossa implementação. Veja que fazer a mesma requisição, alternando apenas o header de requisição Acccept. Estamos usando também o parâmetro -i que diz ao curl imprimir na tela os headers da requisição de resposta.

XML

$ curl -i -H "Accept: application/xml" http://localhost/livros/1
HTTP/1.1 200 OK
Host: localhost
Connection: close
Content-Type: text/xml; charset=UTF-8
Cache-Control: no-cache

<?xml version="1.0"?>
   <response>
     <titulo>PHP Moderno</titulo>
     <autor>Josh Lockhart</autor>
     <isbn>978-85-7522-428-1</isbn>
   </response>

JSON

$ curl -i -H "Accept: application/json" http://localhost/livros/1
HTTP/1.1 200 OK
Host: localhost
Connection: close
Content-Type: application/json
Cache-Control: no-cache

{  
   "titulo":"PHP Moderno",
   "autor":"Josh Lockhart",
   "isbn":"978-85-7522-428-1"
}

Complementando

Veja que o nosso parse do conteúdo do header de requisição Accept esta bem simples. Porém, dependendo da realidade da sua aplicação o header Accept pode ser bem mais complexo, podendo ser originado de uma aplicação cliente onde você não tem muito controle. Por exemplo, veja o header Accept gerado pelo Chrome ao navegar na internet:

Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Criar um parse próprio para tratar do header Accept acaba sendo um pouco complexo. Sugiro utilizar uma biblioteca pronta e muito bem feita como o Mimeparse (https://github.com/conneg/mimeparse-php)

Até o próximo artigo!

Comments

  1. Reply

Deixe um comentário

Follow

Get every new post on this blog delivered to your Inbox.

Join other followers: