Escrevendo um CRUD RESTful com Symfony2

Antes de começar, a intenção desse post não é incentivar ninguém a reinventar a roda, mas mostrar como as características de roteamento do Symfony podem ser usadas para manipular métodos HTTP. Eu, particularmente, não cheguei a experimentar, mas existe um bundle bastante mencionado que promete facilitar a vida de quem quer desenvolver webservices REST em cima do Symfony2: o FOSRestBundle. Esclarecido isso, vamos lá!

Para esse CRUD, vou usar a seguinte convenção:

  • GET /api/resources/ — buscar todos os objetos do nosso recurso;
  • POST /api/resources/ — criar um novo objeto;
  • GET /api/resources/123/ — buscar um objeto específico, de acordo com o código informado;
  • PUT /api/resources/123/ — atualizar um objeto;
  • DELETE /api/resources/123/ — excluir um objeto existente.

Definido isso, podemos escrever controller abaixo. Ah! Estou omitindo vários detalhes de lógica relacionados à busca e persistência das informações na camada de modelo (M do MVC) para facilitar o entendimento.

<?php

namespace Acme\DemoBundle\Controller;

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

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;

/**
 *
 * @Route("/api/resources")
 */
class ApiController extends Controller
{

    /**
     *
     * @param \Symfony\Component\HttpFoundation\Request $request
     * @return array|\Symfony\Component\HttpFoundation\Response
     *
     * @Route("/", name="resources_api_index")
     * @Method("GET")
     */
    public function indexAction(Request $request)
    {
        // ...

        return $this->createJsonResponse(array(
            'resources' => $resources,
        ));
    }

    /**
     *
     * @param \Symfony\Component\HttpFoundation\Request $request
     * @return array|\Symfony\Component\HttpFoundation\Response
     *
     * @Route("/", name="resources_api_create")
     * @Method("POST")
     */
    public function createAction(Request $request)
    {

        // ...

        if (!$success) { // Algum erro de validação, de comunicação com a camada de persistência etc.
            return $this->createJsonResponse(array(
                'error' => 'Não foi possível criar o objeto. Detalhes: ...',
            ), 400);
        }

        return $this->createJsonResponse(array(
            'resource' => $resource,
        ), 201);
    }

    /**
     *
     * @param int $id
     * @return array|\Symfony\Component\HttpFoundation\Response
     *
     * @Route("/{id}", name="resources_api_retrieve")
     * @Method("GET")
     */
    public function retrieveAction($id = 0)
    {
        $id = (int) $id;

        // ...

        if (null === $resource) {
            return $this->createJsonResponse(array(
                'error' => 'Recurso não encontrado. Detalhes: ...',
            ), 404);
        }

        return $this->createJsonResponse(array(
            'resource' => $resource,
        ));
    }

    /**
     *
     * @param int $id
     * @return array|\Symfony\Component\HttpFoundation\Response
     *
     * @Route("/{id}", name="resource_api_update")
     * @Method("PUT")
     */
    public function updateAction($id = 0)
    {
        $id = (int) $id;

        // ...

        if (null === $resource) {
            return $this->createJsonResponse(array(
                'error' => 'Recurso não encontrado. Detalhes: ...',
            ), 404);
        }

        // ...

        if (!$success) { // Algum erro de validação, de comunicação com a camada de persistência etc.
            return $this->createJsonResponse(array(
                'error' => 'Não foi possível criar o objeto. Detalhes: ...',
            ), 400);
        }

        return $this->createJsonResponse(array(
            'resource' => $resource,
        ));
    }

    /**
     *
     * @param int $id
     * @return array|\Symfony\Component\HttpFoundation\Response
     *
     * @Route("/{id}", defaults={"id"=null}, name="resources_api_delete")
     * @Method("DELETE")
     */
    public function deleteAction($id = 0)
    {
        $id = (int) $id;

        // ...

        if (null === $resource) {
            return $this->createJsonResponse(array(
                'error' => 'Recurso não encontrado. Detalhes: ...',
            ), 404);
        }

        // ...

        return $this->createJsonResponse(array(
            'success' => 1,
        ));
    }

    /**
     *
     * @param array $data
     * @param int $code
     * @param array $headers
     * @return \Symfony\Component\HttpFoundation\Response
     */
    protected function createJsonResponse(array $data, $code = 200, array $headers = array())
    {
        $response = new Response(json_encode($data), $code, $headers);
        $response->headers->set('Content-type', 'application/json');

        return $response;
    }

}

Os pontos que merecem antenção especial no nosso controller de exemplo são os seguintes:

@Route e @Method

As rotas se repetem entre as diferentes ações (/api/resources/ e /api/resources/{id}), o que faz com que o Symfony direcione para a ação correta é o método HTTP (GET, POST, PUT ou DELETE).

Códigos HTTP

No retorno, além do código padrão para sucesso — 200, amplamente usado — retornamos:

  • 201, para quando um novo objeto foi criado;
  • 400, para os casos em que há algum erro “capturável” no processamento do request; e
  • 404, para quando um objeto não foi encontrado.

Referências

Advertisements

Script de deploy para projetos que usam git

Há alguns meses atrás desenvolvi um script de deploy para um projeto em que estou trabalhando. Com o tempo vi a possibilidade de melhorar alguns pontos, especialmente com o uso do componente Console do Symfony2,  e agora disponibilizo uma versão pública que pode ser usada com projetos armazenados em um repositório git.

https://github.com/straube/deploy

Uma das limitações dessa versão é que ela trabalha apenas com SSH, mas em breve devo adicionar suporte a FTP.

Auto formatting CSS code with JavaScript

The guys from Webstandards Blog have written a guide to front-end developers on writing beautiful and efficient code. Based on their idea, I write the following code with Krause. It orders and formats CSS code with the rules designed by them.

Some text editor allows you to write plugins using Javascript. This code can be useful in that context.

Notice that only properties defined in props array are ordered. Other properties are placed at the end of the rule. You can add more properties to props as you want.

Continue reading “Auto formatting CSS code with JavaScript”