Validação: Form Theme e Event Listeners
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
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:
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:
Implementando o Front-end
Tendo o formulário criado e o controller, basta criar a view cadastro/index.html.twig:
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:
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:
E, só de aplicar esse theme, já temos outra cara no formulário:
É 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
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
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
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
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
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!
Agora, não precisamos mais dar new no Controller, basta chamar o getData do nosso formulário e pronto!
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?
Para saber o que acontece, podemos adicionar um dump no nosso código logo antes de verificar se ele está válido
E, ao clicar em enviar, sabaremos se ele enviaría para o banco ou não
Ou seja, nosso usuário estaria prestes a enviar o CPF “xablau” para o banco de dados!
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:
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
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
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
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.
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
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
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
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
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
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
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
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.
Agora, enviando o formulário com os mesmos dados que enviamos lá no começo do post, temos
E, com um CPF válido gerado no 4devs.com, podemos ver que o retorno é o oposto
Agora, para conseguir ver nossa mensagem de erro na tela, precisamos retornar a view ao invés de retornar um redirect lá no controller
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.
Devolvendo esse form bonitão com a mensagem de erro já no label.
Próximos passos
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 =)