Validação: Form Theme e Event Listeners

André Chaves
Code Maestro
Published in
12 min readJan 12, 2020

Uma das grandes vantagens de se utilizar frameworks MVC, além de toda a camada de rotas e injeção de dependências, é a camada de processamento de requisições com dados de formulários.

Já discutimos um pouco esse tipo de processamento com os Form Types do Symfony e como podemos evitar o uso da variável request diretamente. Se você não leu esse post ainda, vale bastante a pena dar uma lida antes de continuar.

Mas, podemos melhorar (e muito) nossa abordagem.

Nesse post vamos implementar um formulário de cadastro em um grupo de investimentos aprofundando a implementação com tudo que os Forms do Symfony podem oferecer =)

Implementando o modelo

é necessário algum esforço.

Para criar um formulário de cadastro, precisamos de dados básicos do investidor como nome, e-mail, CPF e telefone pra entrar em contato.

Portanto, vamos juntar esses dados em uma classe simples chamada Investidor:

Entidade Investidor

Além da classe, precisamos de um Controller para renderizar a tela do formulário. Usando o FormBuilder do Symfony podemos passar todos os dados chamando o método add:

CadastroController action para renderizar o formulário

Implementando o Front-end

Tendo o formulário criado e o controller, basta criar a view cadastro/index.html.twig:

index.html.twig do cadastro

Aqui estamos inicializando o formulário com form_start(), o form_widget() se responsabiliza por renderizar todos os campos que cadastramos no controller e o form_end() se encarrega de fechar a tag do formulário.

Portanto, se acessarmos a rota /cadastro/ devemos ver o formulário renderizado:

formulário de cadastro renderizado

Bootstrap Form Theme

Nesse projeto, estou usando o bootstrap 4 pra dar uma carinha mais feliz.

Mas, não somos nós quem renderizamos o formulário. Afinal, estamos usando o form_widget pra isso.

Essa é a desvantagem de encapsular tanto a renderização. Com HTML puro, ao invés do Twig, não teríamos esse problema.

Porém, seria legal dar uma carinha de bootstrap pro nosso formulário também, mesmo com toda a renderização nas mãos do Twig.

Pra isso, o Symfony suporta por padrão a aplicação de temas nos formulários! Também conhecido como Form Theme.

A gente só precisa apontar na configuração do Twig qual tema a gente quer:

twig.yaml

E, só de aplicar esse theme, já temos outra cara no formulário:

formulário com Theme do bootstrap 4 aplicado
magica!

É possível, inclusive criar nossos próprios temas, mas esse assunto fica pra outro post =)

Implementando o back-end do formulário

Agora que temos nosso formulário renderizado com o tema bonitão, podemos criar uma ação no controller para enviar os dados desse formulário.

Essa ação precisará construir o formulário, processar a requisição com o método handleRequest e caso o formulário esteja válido mandamos o investidor pro banco de dados com o entity manager do Doctrine utilizando o método persist

Action para enviar o formulário com os dados do investidor

Repetindo código?

Aqui cometemos um grande pecado como desenvolvedores.

Se olharmos nosso controller como um todo da pra perceber que estamos repetindo todo o código de inicialização do formulário com os campos nome, cpf, telefone e email.

O que acontece se precisarmos adicionar outro campo no investidor no futuro? A gente teria que mexer nos dois lugares.

Isolando o código

Uma possível solução seria isolar esse código todo em um único método dentro do próprio controller e reutilizar esse método dentro de cada action

método getForm isolado

Assim, se surgir um campo novo no investidor, mexemos apenas no método getForm que a gente acabou de criar =)

Indo além

Isolar métodos no controller resolve um problema mas cria outro.

Se um dia a gente precisar utilizar esse formulário em outro controller precisaríamos copiar e colar o método getForm.

O que nos deixaria no mesmo cenário de duplicar o código nas ações.

Pra resolver esse problema, podemos criar uma classe que seja responsável por construir o formulário de um Investidor com todos os campos necessários.

Abstract Type

Pra isso, a galera do Symfony deixou pronto pra gente uma classe que recebe o form builder que pegamos no controller!

Assim, a gente pode isolar tudo nessa classe e reaproveitar a construção do formulário em qualquer lugar.

Essa classe se chama AbstractType e podemos herdar dela pra receber o form builder com o método buildForm

criando a classe Investidor

Nele, já adicionamos todos os campos referentes ao nosso Investidor.

Indo além

Aqui é importante ressaltar que a classe AbstractType não serve apenas para injetar o form builder. Ela representa um formulário!

Ou seja, daqui pra frente, o InvestidorType é a representação de um formulário de Investidor.

Da mesma forma que temos o TextType do próprio framework para representar um campo de texto, ou o ChoiceType para representar um select, temos o InvestidorType para representar um Investidor.

Essa semântica é extremamente poderosa porque podemos criar nossos próprios tipos e fazer com que um utilize o outro se necessário, da mesma forma que fazemos com os tipos do framework!

Estrutura de pastas

Pra garantir uma boa organização no nosso código, é legal deixar todas as classes de formulário na mesma pasta. Pela documentação do framework, é indicado que essa pasta seja a pasta Form

estrutura de pastas com Form

Beleza, e como usa essa classe?

Agora que a gente já isolou tudo, podemos remover o código do controller e chamar nossa classe InvestidorType.

Pra isso, a galera do Symfony deixou pronto um método em todos os controllers chamado createForm que recebe a classe do nosso formulário e devolve ele pronto

código do controller consumindo o InvestidorType

Com isso, já temos uma forma de reaproveitar esse formulário em qualquer Controller!

Indo além

Nosso Controller já está muito mais enxuto. Mas, algumas linhas de código ainda fazem um volume desnecessário.

Quando criamos um investidor, temos que definir todos os campos dele a partir dos dados que vieram do formulário. Ou seja, temos que dar new Investidor() e chamar todos os métodos set dele.

Mas, e se essa classe tivesse 98 campos? Ficaria gigante!

Configurando o tipo do formulário

Por isso, nosso formulário pode receber configurações em um método chamado configureOptions.

Nesse método podemos passar um array associativo com a chave data_class indicando qual classe o Symfony tem que dar new e chamar os setters pra gente!

método configureOptions apontando a classe do Investidor

Agora, não precisamos mais dar new no Controller, basta chamar o getData do nosso formulário e pronto!

removendo a instância de Investidor

Se atualizarmos nosso formulário, tudo deve funcionar da mesma forma que funcionava anteriormente. Porém, agora temos um código muito mais fácil de manter e alterar.

Quando alteramos o código sem alterar nenhum comportamento, estamos fazendo uma refatoração. Portanto, acabamos de refatorar nosso código =)

Validando o formulário

Agora que nosso código está bem organizado no back-end, podemos focar um pouco mais no comportamento dos usuários.

Será que estamos garantindo que nosso formulário será preenchido corretamente?

Por exemplo, o que acontece se no campo do CPF não passarmos um CPF?

formulário preenchido com CPF inválido

Para saber o que acontece, podemos adicionar um dump no nosso código logo antes de verificar se ele está válido

dump de validação do formulário

E, ao clicar em enviar, sabaremos se ele enviaría para o banco ou não

resultado do dump

Ou seja, nosso usuário estaria prestes a enviar o CPF “xablau” para o banco de dados!

o caos está instalado

Uma possível abordagem para resolver esse problema sería verificar se o CPF é válido junto com a verificação do formulário.

Beleza, e como a gente valida um CPF?

Para garantir que nosso CPF é válido, vamos usar uma função criada no gist pelo Rafael Neri que já encapsula o algoritmo de validação de CPF


private function validaCPF($cpf) {

// Extrai somente os números
$cpf = preg_replace( '/[^0-9]/is', '', $cpf );

// Verifica se foi informado todos os digitos corretamente
if (strlen($cpf) != 11) {
return false;
}

// Verifica se foi informada uma sequência de digitos repetidos. Ex: 111.111.111-11
if (preg_match('/(\d)\1{10}/', $cpf)) {
return false;
}

// Faz o calculo para validar o CPF
for ($t = 9; $t < 11; $t++) {
for ($d = 0, $c = 0; $c < $t; $c++) {
$d += $cpf{$c} * (($t + 1) - $c);
}
$d = ((10 * $d) % 11) % 10;
if ($cpf{$c} != $d) {
return false;
}
}
return true;

}

Então, no nosso controller bastaria chamar essa função junto com a verificação do formulário:

Validação do CPF

Com isso, já impedimos que o usuário envie qualquer CPF inválido.

Mas, é realmente responsabilidade do Controller válidar o CPF?

A gente acabou de criar uma representação para o formulário de um Investidor, seria muito melhor realizar esse procedimento lá.

Afinal, a validação faz parte do formulário. Se o CPF não está válido o formulário não está válido também!

Portanto, vamos mover esse código para dentro do nosso InvestidorType

método de validação de CPF no InvestidorType

E queremos utilizar ele assim que o formulário for enviado.

Portanto, precisamos de alguma forma interceptar o evento de envio do formulario no nosso back-end.

Algo que deve acontecer no momento em que chamamos o método handleRequest no controller. Já que é ele quem processa toda a requisição que vem do front-end.

Event listeners

Para isso, a galera do Symfony criou formas de escutar os eventos dos nossos formulários.

Os principáis eventos, em ordem de execução são:

  • PRE_SET_DATA
  • POST_SET_DATA
  • PRE_SUBMIT
  • SUBMIT
  • POST_SUBMIT

SET_DATA

fluxograma com os eventos pré set data e post set data em sequência
fluxograma set

Primeiro, o Symfony executa o evento PRE_SET_DATA antes e POST_SET_DATA depois de definir os dados do nosso Investidor.

Esses eventos são executados independente do formulário ter sido enviado ou não o que pode ser útil em momentos que a gente precise aplicar rotinas de transformação de valores para o formulário.

SUBMIT

Fluxograma com os eventos pré submit, submit e post submit em sequncia
fluxograma submit

Após isso, temos os eventos de envio do formulário, começando com o PRE_SUBMIT que nos retorna os dados puros da requisição, da mesma forma que o controller devolve pelo Request.

Em seguida os dados da requisição são normalizados do array puro que vem da requisição para a instância que a gente informou a classe lá atrás no configureOptions.

Aqui também são chamados os setters da classe por reflection. Por isso não precisamos mais ficar chamando os setters no controller.

imagem de um lutador de MMA indignado
eu sei… parece magia

Após a normalização é executado o SUBMIT com a instância de Investidor já criada e populada com os setters

Finalmente, chamamos o POST_SUBMIT caso a gente queira executar algo após todo o processamento.

Escolhendo o evento certo

No nosso caso, queremos interceptar o evento de envio de preferência com o Investidor já criado. Portanto, vamos ficar com o evento SUBMIT.

Mas, em outras situações você pode precisar de outros eventos e é sempre bom saber escolher qual o melhor para o seu caso pensando no clico de vida dos eventos =)

Beleza e como eu uso esses eventos?

Para utilizar os eventos, precisamos dizer para o nosso FormBuilder que queremos escutar algum deles.

Para isso, podemos utilizar o método addEventListener, que recebe o nome do evento e o que queremos executar neste evento

cadastrando um event listener no form builder
cadastrando um event listener no form builder

No nosso caso, criamos o método onSubmit escutando o evento FormEvents::SUBMIT.

Ou seja, quando nosso formulário for enviado, chamaremos o método onSubmit do próprio InvestidorType passando o evento.

Indo além

Aqui, o ideal seria criar uma outra classe apenas para escutar esses eventos do nosso formulário. Porém, para manter as coisas mais simples eu mantive dentro do próprio InvestidorType.

Utilizando o FormEvent

Agora que temos uma forma de escutar os eventos do nosso formulário, podemos explorar o que é possível fazer com esse evento.

Pra isso, podemos dar uma olhada nos métodos que estão disponiveis na classe FormEvent

métodos disponíveis na classe FormEvent

Aqui, já temos claro que podemos pegar os dados que foram enviados com o método getData. Além disso, podemos pegar o próprio formulário com o método getForm.

Também, podemos definir os dados do formulário também se necessário com o método setData. Nesse post não vamos precisar dele, mas é possível que você precise em outras ocasiões mais complexas.

Os outros dois métodos estão obsoletos (depracted) pelo framework então é ideal que a gente não use eles. Afinal, em próximas versões eles podem não existir, o que faria nossa aplicação quebrar.

Validando dados no listener

Para validar nosso CPF, precisamos pegar os dados que foram enviados na requisição. Ou seja, precisamos do getData.

Como sabemos que nesse ponto do cliclo de vida dos eventos os dados já foram processados, o retorno do getData será uma instância de Investidor.

Portanto, podemos consultar o CPF desse investidor e passar o valor para nosso método validaCPF

validando o CPF do investidor

Agora, caso o cpf não seja válido, precisamos informar isso ao nosso formulário.

Pra isso, precisamos buscar a instância do formulário do nosso evento com o método getForm

buscando a instância do form com o método getForm

Tendo o formulário em mãos, precisamos adicionar um erro no campo CPF.

Adicionando erros ao formulário

Aqui, podemos dar uma explorada nos métodos que esse formulário disponibiliza pra gente

métodos do formulário

Um método que chama bastante atenção é o addError.

Porém, nesse caso estaríamos adicionando o erro diretamente no formulário ao invés de adicionar no campo CPF especificamente. Isso faria com que o erro aparecesse no começo do formulário ao invés de ser exibido no campo correto.

Antes de chamar o addError, precisamos pegar o campo CPF do formulário.

Especificando o campo

Pra isso, podemos utilizar o método get que retorna um campo específico do form recebendo o nome do campo

buscando o campo CPF

Agora, com o campo CPF em mãos, podemos chamar o addError nesse campo específico.

O método addError, recebe uma instância de FormError, com a mensagem que a gente quiser

adicionando um erro no campo CPF

Assim, quando o usuário digitar um CPF inválido, ele receberá uma mensagem mais confortável e no campo correto =)

Além disso, quando chamarmos o método isValid lá no controller, ele devolverá falso pois o CPF não está válido!

Portanto, podemos remover aquela verificação de CPF do nosso controller e já adicionar o dump de novo para verificar se tudo realmente funcional.

dump dos dados e da validação no controller

Agora, enviando o formulário com os mesmos dados que enviamos lá no começo do post, temos

resultado do dump inválido no navegador

E, com um CPF válido gerado no 4devs.com, podemos ver que o retorno é o oposto

resultado do dump válido no navegador

Agora, para conseguir ver nossa mensagem de erro na tela, precisamos retornar a view ao invés de retornar um redirect lá no controller

chamando o render ao invés do redirect caso o formulário esteja inválido

E pronto!

Assim, caso o formulário não esteja válido, devolvemos ele processado com erros e tudo mais para o twig. Que por sua vez vai chamar toda a camada de renderização pra gente.

mensagem de erro utilizando o tema em bootstrap4

Devolvendo esse form bonitão com a mensagem de erro já no label.

Próximos passos

E… aqui temos o maior post do blog até o momento rsrs

Agora já podemos interceptar qualquer ponto do ciclo de vida dos nossos formulários utilizando EventListeners e FormBuilders. Tudo isso aplicando um Theme que deixa tudo muito mais bonito pra gente em duas linhas de configuração.

Daria pra melhorar? Daria.

Além de isolar a chamada dos listeners em classes separadadas, poderiamos ter deixado a lógica de validação de CPF em outra classe também.

Afinal, seguindo o principio da responsabilidade única em SOLID toda classe deve ter apenas uma responsabilidade e validar o CPF não deveria ser responsabilidade da classe InvestidorType.

Mas, e ai, o que você achou dos eventos? E do Tema? Tem alguma ideia de post? Comenta com a gente aqui em baixo!

Ah, o código desse formulário você encontra lá no meu git =)

--

--

André Chaves
Code Maestro

Empreendedor, CTO, desenvolvedor e apaixonado por automação.