Propel ModelCriteria : leftJoin + useQuery

When using leftJoin and useQuery from a ModelCriteria (Query class), you can set the join type, this way:

// ...
->leftJoinFoo()
->useFooQuery(null, \Criteria::LEFT_JOIN)
// ...
->endUse()
// ...

 

Propel + Symfony2 : Debugando queries em comandos

Quando no ambiente de desenvolvimento, em um projeto baseado no Symfony2, usar o webprofiler na interface web (a partir da barra que fica no rodapé das páginas) é uma mão na roda em várias situações. Mas no console geralmente não temos essa facilidade tão a mão, porém não é impossível acessá-la. Especificamente para as queries executadas através do Propel, é possível usar o seguinte trecho para fins de debug:

$profiler = $this->getContainer()->get('profiler');
$db = $profiler->get('propel');
$db->collect(new \Symfony\Component\HttpFoundation\Request(), new \Symfony\Component\HttpFoundation\Response()); // Stubs, não são usados pelo profiler
var_dump($db->getQueries());

Você pode dar uma olhada na classe Symfony\Bridge\Propel1\DataCollector\PropelDataCollector e conferir os métodos disponíveis.

Outros profilers podem ser acessados através do container, mas como a requisição (request) e a resposta (response) não estão disponíveis no console, pode ser que nem todos funcionem como esperado.

Testando comandos do Symfony que usam serviços da aplicação

Se você tem um comando no Symfony2 que usa serviços da aplicação, como um ORM por exemplo, e seguir o modelo que a documentação do framework fornece para escrever testes unitários, poderá ver erros com os a seguir:

Fatal error: Call to undefined method Symfony\Component\Console\Application::getKernel() in [...]/vendor/symfony/symfony/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php on line [...]

ao acessar o kernel da aplicação, a partir do comando, ou:

Fatal error: Call to a member function getParameter() on a non-object in [...]

ao tentar acessar algum método do container.

O primeiro caso, acontece quando você usa a classe Symfony\Component\Console\Application ao invés de Symfony\Bundle\FrameworkBundle\Console\Application. Verifique os uses da sua classe de testes e ajuste se for necessário.

A segunda exceção é lançada quando você está usando a classe Application correta, mas usando um kernel inválido na sua instanciação. Talvez exista outro modo de fazer isso, mas para mim funcionou extender a classe Symfony\Bundle\FrameworkBundle\Test\WebTestCase no lugar de \PHPUnit_Framework_TestCase e construir a aplicação da seguinte forma:

$application = new Application(self::createClient()->getKernel());

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

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.

Correção de bug no AvalancheImagineBundle

O AvalancheImagineBundle é um bundle super útil para o Symfony2. Ele adiciona funções de geração de thumbnails com caching ao framework. Porém, dependendo da estrutura de diretório e do número de requisições da aplicação, dois problemas podem acontecer:

  1. loop infinito de redirects; e
  2. bug na criação recursiva de diretórios devido a concorrência.

Para resolver essas situações em um projeto de Classificados da Gazeta do Povo, precisamos alterar o core do bundle e, ontem, nosso pull request foi aceito: https://github.com/avalanche123/AvalancheImagineBundle/pull/160.

Se você usa o AvalancheImagineBundle, atualize para última versão e evite esses erros.