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; });
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+');
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.