Forms no Symfony2 : Validando apenas os campos submetidos

No último post, sobre CRUDs RESTful com Symfony2, uma das características do código de exemplo era usar o método PUT na atualização do objeto. Seguindo isso, estive fazendo algumas exeperiências com o mesmo form do Symfony na criação e atualização. No caso do update, gostaria de enviar apenas os campos a serem alterados (em um objeto que tenha ‘id‘, ‘title‘, ‘description‘ e ‘date‘ como atributos, por exemplo), assim:

$ curl -X PUT -d "title=Testando a atualizacao" http://localhost:8000/api/resources/123

Então descobri que o método que substitui o bind a partir da versão 2.3 do Symfony, tem um segundo parâmetro para indicar se os campos não enviados devem ou não ser limpos, se não forem, terão o valor do objeto original mantido:

$clearMissing = 'PUT' !== $request->getMethod();
$form->submit($request, $clearMissing);

Simples assim!

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