Aplicação REST simples com Silex, parte II

Continuando

Na primeira parte aprendemos como iniciar nossa aplicação REST com Silex e implementamos as rotas /livros e /livros/{id}.

Dando sequencia à nossa aplicação, vamos implementar as Rotas para Incluir, Alterar e Deletar livros, usando os verbos HTTP POST, PUT e DELETE.

Rotas

POST /livros

Através desta rota será possível a criação de um novo recurso (no caso um livro). O client deve enviar, através de uma requisição POST, a representação que deseja inserir no formato JSON, informando o header Content-type como application/json.

Após inserir os dados, nossa aplicação REST deve retornar o statuscode 201 (Created), incluíndo o header Location que deve conter um link apontando para o novo recurso recém-criado.

PUT /livros/{id}

Para editar um recurso vamos utilizar o método HTTP PUT. Lembrando que ao desenvolver APIs RESTful não devemos assumir PUT somente para editar recursos. É correto dizer também que PUT pode ser usado para criar novos recursos além do POST. Veremos em um outro momento a principal diferença entre POST e PUT. Neste momento vamos usar o PUT apenas para realizar a edição.

Ao editar recursos com PUT, devemos retornar o statuscode 200. Devemos retornar também o estado da representação do recurso após a edição no formato JSON.

Os dados que deseja editar do recurso devem ser enviados no corpo da requisição representados no formato JSON. Lembre-se de informar o header Content-type como application/json.

DELETE /livros/{id}

Para remover um recurso (livro) basta fazermos uma requisição DELETE informando o ID do livro que desejamos excluir. Após realizarmos a operação de exclusão, devemos retornar o statuscode 204 (No Content) sem nenhuma informação no corpo da resposta. Caso o id informado para exclusão não existir devemos retornar o statuscode HTTP 404.

Implementação

Rotas:

  • POST /livros
  • Inserindo novo livro:

    // POST - incluir
    $app->post('/livros', function(Request $request) use ($app, $dbh) {
        $dados = json_decode($request->getContent(), true);
    
        $sth = $dbh->prepare('INSERT INTO livros (titulo, autor, isbn) 
                VALUES(:titulo, :autor, :isbn)');
    
        $sth->execute($dados);
        $id = $dbh->lastInsertId();
    
        // response, 201 created
        $response = new Response('Ok', 201);
        $response->headers->set('Location', "/livros/$id");
        return $response;
    });
    
  • PUT /livros/{id}
  • Atualizando um Livro:

    // PUT - editar (toda estrutura)
    $app->put('/livros/{id}', function(Request $request, $id) use ($app, $dbh) {
        $dados = json_decode($request->getContent(), true);
        $dados['id'] = $id;
    
        $sth = $dbh->prepare('UPDATE livros 
                SET titulo=:titulo, autor=:autor, isbn=:isbn
                WHERE id=:id');
    
        $sth->execute($dados);
        return $app->json($dados, 200);
    })->assert('id', '\d+');
    
  • DELETE /livros/{id}
  • Excluindo um Livro:

    // DELETE - excluir 
    $app->delete('/livros/{id}', function($id) use ($app, $dbh) {
        $sth = $dbh->prepare('DELETE FROM livros WHERE id = ?');
        $sth->execute([ $id ]);
    
        if($sth->rowCount() < 1) {
            return new Response("Livro com id {$id} não encontrado para exclusão!", 404);
        }
        
        // registro foi excluido, retornar 204 - no content
        return new Response(null, 204);
    })->assert('id', '\d+');
    

Juntando tudo

Segue o arquivo index.php final, incluíndo o aplicativo completo:

<?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;

$app = new Silex\Application();

/* Connect to mysql atabase */
$dsn = 'mysql:dbname=teste;host=127.0.0.1;charset=utf8';
try {
    $dbh = new PDO($dsn, 'livros', 'livros2016');
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

// GET /livros
$app->get('/livros', function () use ($app, $dbh) {
    // consulta todos livros
    $sth = $dbh->prepare('SELECT id, titulo, autor, isbn FROM livros');
    $sth->execute();
    $livros = $sth->fetchAll(PDO::FETCH_ASSOC);

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

// GET /livros/{id}
$app->get('/livros/{id}', function ($id) use ($app, $dbh) {
    $sth = $dbh->prepare('SELECT id, titulo, autor, isbn FROM livros WHERE id=?');
    $sth->execute([ $id ]);

    $livro = $sth->fetchAll(PDO::FETCH_ASSOC);
    if(empty($livro)) {
        // nao encontrado, 404
        return new Response("Livro com id {$id} não encontrado para consulta!", 404);
    }

    return $app->json($livro);
})->assert('id', '\d+');

// POST - incluir
$app->post('/livros', function(Request $request) use ($app, $dbh) {
    $dados = json_decode($request->getContent(), true);

    $sth = $dbh->prepare('INSERT INTO livros (titulo, autor, isbn) 
            VALUES(:titulo, :autor, :isbn)');
    
    $sth->execute($dados);
    $id = $dbh->lastInsertId();

    // response, 201 created
    $response = new Response('Ok', 201);
    $response->headers->set('Location', "/livros/$id");
    return $response;
});

// PUT - editar (toda estrutura)
$app->put('/livros/{id}', function(Request $request, $id) use ($app, $dbh) {
    $dados = json_decode($request->getContent(), true);
    $dados['id'] = $id;

    $sth = $dbh->prepare('UPDATE livros 
            SET titulo=:titulo, autor=:autor, isbn=:isbn
            WHERE id=:id');
    
    $sth->execute($dados);
    return $app->json($dados, 200);
})->assert('id', '\d+');

// DELETE - excluir 
$app->delete('/livros/{id}', function($id) use ($app, $dbh) {
    $sth = $dbh->prepare('DELETE FROM livros WHERE id = ?');
    $sth->execute([ $id ]);

    if($sth->rowCount() < 1) {
        return new Response("Livro com id {$id} não encontrado para exclusão!", 404);
    }
    
    // registro foi excluido, retornar 204 - no content
    return new Response(null, 204);
})->assert('id', '\d+');

$app->run();

Testando as Rotas

Vamos concluir os testes das rotas implementadas interagindo com os recursos através de POST, PUT e DELETE, através do comando curl:

PS: O parâmetro -i do curl faz exibir os headers de resposta da requisição HTTP.

POST /livros
$ curl -X POST -H "Content-Type: application/json" \
-d '{"titulo":"My New Book","autor":"Douglas","isbn":"111-11-1111-111-1"}' \
-i http://localhost/livros
HTTP/1.1 201 Created
Host: localhost
Connection: close
Cache-Control: no-cache
Location: /livros/4
Content-Type: text/html; charset=UTF-8
PUT /livros/{id}
$ curl -X PUT -H "Content-Type: application/json" \
-d '{"titulo":"PHP","autor":"Douglas","isbn":"111-11-1111-111-1"}' \
-i http://localhost/livros/4
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Length: 70
Content-Type: application/json

{"titulo":"PHP","autor":"Douglas","isbn":"111-11-1111-111-1","id":"5"}
DELETE /livros/{id}
$ curl -X DELETE -i http://localhost/livros/4
HTTP/1.1 204 No Content
Cache-Control: no-cache
Content-Length: 0
Content-Type: text/html

Tentando excluir um livro inexistente:

$ curl -X DELETE -i http://localhost/livros/10
HTTP/1.1 404 Not Found
Cache-Control: no-cache
Content-Length: 47
Content-Type: text/html; charset=UTF-8

Livro com id 10 não encontrado para exclusão!

Indo Além

Neste tutorial de duas partes aprendemos como criar uma aplicação simples REST com Silex. Muitos conceitos REST foram aplicados. Porém é lógico que você não deve parar por aqui, existem muitos outros conceitos e melhorias para explorarmos. Pretendo escrever novos artigos sobre o assunto.

Ainda sobre esse nosso aplicativo simples gostaria de sugerir as seguintes recomendações para que você possa evoluir ele para algo ainda mais profissional:

  • Validação de Input. É importante deixar nosso aplicativo mais seguro, melhorando a validação do input de dados principalmente nos métodos POST e PUT.
  • Ao invés de PDO para interagir com o banco de dados, sugiro utilizar Doctrine DBAL, uma biblioteca do doctrine, que acrescenta uma camada de abstração além do PDO, facilitando ainda mais sua vida. O silex possui integração dom o DBAL através do DoctrineServiceProvider.
  • Veja que nossa implementação esta praticamente usando apenas 1 arquivo. Caso for evoluir para uma aplicação maior, é imprescindível organizar melhor as coisas. O silex permite você organizar suas rotas de uma forma bem mais interessante. Este outro artigo do sitepoint também é bem interessante e ajuda a esclarecer o assunto.

Deixe um comentário

Follow

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

Join other followers: