Hoje você vai conhecer sobre um componente muito poderoso do Zend Framework, o Zend Form. Disponível nas versões 2 e 3 do Zend, possibilita que você crie sólidos formulários com validação no backend e ter total confiança nos dados recebidos. Se você quiser conhecer mais a fundo o conceito por trás do Zend Form, acesse esse link. Neste post mostrarei somente a parte prática. Criaremos nosso form com alguns elementos, algumas das validações mais comuns, o renderizaremos na view, trataremos a injeção de dependências e por fim faremos testes automatizados. Bastante coisa não é? A intenção é que você conheça e saiba como trabalhar de forma profissional com o Zend Form.
<?php namespace Application\Form; use Zend\Form\Form; /** * Class ExampleForm * @package Application\Form */ class ExampleForm extends Form { public function __construct($name = null, array $options = []) { parent::__construct($name, $options); } }Como esta classe serve para definir a estrutura de nosso formulário, podemos criar diversos elementos como text, textarea, select, checkbox, radio, file e submit. Todas as definições a seguir serão realizadas dentro do construtor, logo após do parent::__construct($name, $options);
# Nos imports da classe use Zend\Form\Element\Text; $this->add([ 'name' => 'text', 'type' => Text::class, 'options' => [ 'label' => 'Um campo de texto' ], 'attributes' => [ 'id' => 'text', 'class' => 'form-control' ] ]);
# Nos imports da classe use Zend\Form\Element\Textarea; $this->add([ 'name' => 'textarea', 'type' => Textarea::class, 'options' => [ 'label' => 'Uma área de texto' ], 'attributes' => [ 'id' => 'textarea', 'class' => 'form-control' ] ]);
# Nos imports da classe use Zend\Form\Element\Number; $this->add([ 'name' => 'number', 'type' => Number::class, 'options' => [ 'label' => 'Um campo numeral' ], 'attributes' => [ 'id' => 'number', 'class' => 'form-control' ] ]);
# Nos imports da classe use Zend\Form\Element\Select; $this->add([ 'name' => 'select', 'type' => Select::class, 'options' => [ 'label' => 'Uma select', 'value_options' => [ 0 => 'Primeira opção', 1 => 'Segunda opção' ], 'empty_option' => 'Selecione uma opção' ], 'attributes' => [ 'id' => 'select', 'class' => 'form-control' ] ]);Neste elemento temos algumas novidades. O value_options é um array com todas as opções disponíveis para escolha. Já a opção empty_option serve para indicar o placeholder do select.
# Nos imports da classe use Zend\Form\Element\Checkbox; $this->add([ 'name' => 'checkbox', 'type' => Checkbox::class, 'options' => [ 'label' => 'Check' ], 'attributes' => [ 'id' => 'checkbox', 'class' => 'form-control' ] ]);
# Nos imports da classe use Zend\Form\Element\Radio; $this->add([ 'name' => 'radio', 'type' => Radio::class, 'options' => [ 'label' => 'Radio', 'value_options' => [ 0 => 'Opção 1', 1 => 'Opção 2' ] ], 'attributes' => [ 'id' => 'radio', 'class' => 'form-control' ] ]);Assim como o Select, temos que definir as value_options.
# Nos imports da classe use Zend\Form\Element\Submit; $this->add([ 'name' => 'submit', 'type' => Submit::class, 'attributes' => [ 'id' => 'submit', 'class' => 'btn btn-success', 'value' => 'Submit' ] ]);Aqui removemos a configuração options, pois não precisamos de um label. Também adicionamos um item chamado value dentro dos atributos.
<?php // recuperando o form enviado pelo controller $form = $this->form; // preparando o form $form->prepare(); // abrindo a tag form <form> echo $this->form()->openTag($form); ?> <div class="form-group"> <label><?php echo $form->get('text')->getLabel(); ?></label> <?php echo $this->formElement($form->get('text')); ?> </div> <div class="form-group"> <label><?php echo $form->get('textarea')->getLabel(); ?></label> <?php echo $this->formElement($form->get('textarea')); ?> </div> <div class="form-group"> <label><?php echo $form->get('number')->getLabel(); ?></label> <?php echo $this->formElement($form->get('number')); ?> </div> <div class="form-group"> <label><?php echo $form->get('number')->getLabel(); ?></label> <?php echo $this->formElement($form->get('number')); ?> </div> <div class="form-group"> <label><?php echo $form->get('select')->getLabel(); ?></label> <?php echo $this->formElement($form->get('select')); ?> </div> <div class="form-group"> <label><?php echo $form->get('checkbox')->getLabel(); ?></label> <?php echo $this->formElement($form->get('checkbox')); ?> </div> <div class="form-group"> <label><?php echo $form->get('radio')->getLabel(); ?></label> <?php echo $this->formElement($form->get('radio')); ?> </div> <div class="form-group"> <!-- Submit não possui label--> <?php echo $this->formElement($form->get('submit')); ?> </div> <!--Por fim fechando a tag form </form>--> <?php echo $this->form()->closeTag();
# Nos imports da classe use Application\Form\ExampleForm; public function addAction() { $form = new ExampleForm(); return new ViewModel([ 'form' => $form ]); }
<?php namespace Application\Form; use Zend\InputFilter\InputFilter; use Zend\InputFilter\InputFilterAwareInterface; use Zend\InputFilter\InputFilterInterface; /** * Class ExampleFormFilter * @package Application\Form */ class ExampleFormFilter implements InputFilterAwareInterface { /** * @inheritdoc */ public function setInputFilter(InputFilterInterface $inputFilter) { throw new \Exception('Não utilizaremos este método'); } /** * @inheritdoc */ public function getInputFilter() { $inputFilter = new InputFilter(); // Aqui definiremos nossos filtros e validações return $inputFilter; } }Como existem muitas opções para as validações, não serão abordadas todas elas, mostrarei somente algumas para que você aprenda um pouco do que é possível. Como dica, novamente deixo a dica para que você conheça mais a fundo o componente Zend Form. Um possível exemplo que utilizamos no ExempleFormFilter é o seguinte.
# Nos imports da classe Application\Form\ExampleFormFilter use Zend\Filter\Striptags; use Zend\Filter\StringTrim; use Zend\Validator\StringLength; $inputFilter->add([ 'name' => 'text', 'required' => true, 'filters' => [ ['name' => StripTags::class], ['name' => StringTrim::class] ], 'validators' => [ [ 'name' => StringLength::class, 'options' => [ 'encoding' => 'UTF-8', 'min' => 1, 'max' => 255 ] ] ] ]);O nome deve ser exatamente o mesmo que fora definido na classe do Form. O required é required aceita valor true e false, e serve para indicar, claramente, se o campo é obrigatório ou não. Perceba também que existem mais duas configurações, filters e validators. Estes serão explicados na sequência.
# Nos imports de Application\Form\ExampleForm use Zend\Form\Element\Password; $this->add([ 'name' => 'password', 'type' => Password::class, 'options' => [ 'label' => 'Senha' ], 'attributes' => [ 'id' => 'password', 'class' => 'form-control' ] ]); $this->add([ 'name' => 'password_confirmation', 'type' => Password::class, 'options' => [ 'label' => 'Confirme a senha' ], 'attributes' => [ 'id' => 'password_confirmation', 'class' => 'form-control' ] ]);Para a classe ExampleFormFilter vamos adicionar os campos para realizar a validação. O campo password recebe os mesmos filtros que um campo text, StripTags e StringTrim. Para os validadores, podemos utilizar o mesmo StringLength já apresentado, desta vez definimos como mínimo 6 e máximo 14 caracteres.
# Application\Form\ExampleFormFilter $inputFilter->add([ 'name' => 'password', 'required' => true, 'filters' => [ ['name' => StripTags::class], ['name' => StringTrim::class] ], 'validators' => [ [ 'name' => StringLength::class, 'options' => [ 'encoding' => 'UTF-8', 'min' => 6, 'max' => 14 ] ] ] ]);A grande sacada está no password_confirmation. Ele recebe tudo que o campo password recebe e mais, a validação de que a senha foi digitada corretamente nos dois campos necessários. Fazemos isso utilizando o validador Identical. Nele informamos o token para confronto, ou seja, qual é o item que queremos comparar.
# Nos imports de Application\Form\ExampleFormFilter use Zend\Validator\Identical; $inputFilter->add([ 'name' => 'password_confirmation', 'required' => true, 'filters' => [ ['name' => StripTags::class], ['name' => StringTrim::class] ], 'validators' => [ [ 'name' => StringLength::class, 'options' => [ 'encoding' => 'UTF-8', 'min' => 6, 'max' => 14 ] ], [ 'name' => Identical::class, 'options' => [ 'token' => 'password' ] ] ] ]);Apenas fazendo isso a validação de senha e confirmação de senha já está funcionando. Perceba também que definimos uma mensagem de erro, para caso as senhas não sejam iguais.
$inputFilter->add([ 'name' => 'password_confirmation', 'required' => true, 'filters' => [ ['name' => StripTags::class], ['name' => StringTrim::class] ], 'validators' => [ [ 'name' => StringLength::class, 'options' => [ 'encoding' => 'UTF-8', 'min' => 6, 'max' => 14 ] ], [ 'name' => Identical::class, 'options' => [ 'token' => 'password', 'messages' => [ Identical::NOT_SAME => 'Senhas não conferem' ] ] ] ] ]);
$exampleFormFilter = new ExampleFormFilter(); $this->setInputFilter($exampleFormFilter->getInputFilter()); // E o restante dor form segue como já definido anteriormente //$this->add([...
public function addAction() { $form = new ExampleForm(); // inicializando nossa variável de erros $errorMessages = []; /** @var Request $request */ $request = $this->getRequest(); // Caso o request seja um POST if ($request->isPost()) { // Obtemos os dados digitados pelo usuário $data = $request->getPost()->toArray(); // e os passamos para o form $form->setData($data); // validamos se os dados estão ok if (! $form->isValid()) { // caso não estejam, recuperamos os erros $errorMessages = $form->getMessages(); } } return new ViewModel([ 'form' => $form, // Por fim passamos a variável de erros para a view 'errorMessages' => $errorMessages ]); }Ao preencher os dados, dar um submit no formulário e o mesmo possuir erros, podemos simplesmente dar um var_dump() para ver os erros apresentados.
var_dump($errorMessages);O resultado pode ser como este:
<div class="form-group"> <label><?php echo $form->get('number')->getLabel(); ?></label> <?php echo $this->formElement($form->get('number')); ?> <?php if (isset($this->errorMessages['number'])) : ?> <div class="error-messages" style="color: #f00;"> <?php foreach ($this->errorMessages['number'] as $message) : ?> <?php echo $message; ?><br /> <?php endforeach; ?> </div> <?php endif; ?> </div>Pronto, agora só repetir para todos os elementos na view. E o resultado deve ser assim. Agora você já tem todo seu form construído com as melhores práticas do Zend Framework e com uma camada a mais de segurança. Para aprimorar ainda mais o seu conhecimento, vamos agora criar nossos testes automatizados.
composer testVeja o exemplo: Caso você esteja rodando através de algum outro projeto que já possuía e o comando acima resultar em erro, sugiro copiar o phpunit.xml.dist do Skeleton Application para um arquivo phpunit.xml em seu projeto. Este arquivo já prepara sua suíte de testes, caso possua mais módulos, basta os adicionar seguindo o exemplo do módulo Application, veja um exemplo.
<?xml version="1.0" encoding="UTF-8"?> <phpunit colors="true"> <testsuites> <testsuite name="ZendSkeletonApplication Test Suite"> <directory>./module/Application/test</directory> </testsuite> <testsuite name="Test suite do módulo User"> <directory>./module/User/test</directory> </testsuite> </testsuites> </phpunit>
<?php namespace ApplicationTest\Form; use Application\Form\ExampleForm; use PHPUnit\Framework\TestCase; use Zend\Form\Form; /** * Class ExampleFormTest * @package ApplicationTest\Form */ class ExampleFormTest extends TestCase { /** * @var ExampleForm */ protected $form; protected function setUp() { // Inicializando o form $this->form = new ExampleForm(); parent::setUp(); } /** * Definindo os campos existentes em nosso form * @return array */ public function formFields() { return [ ['text'], ['textarea'], ['number'], ['select'], ['checkbox'], ['radio'], ['password'], ['password_confirmation'], ['submit'], ]; } /** * Definindo todos os dados simulando um form preenchido integralmente * @return array */ public function getData() { return [ 'text' => 'Um valor qualquer', 'textarea' => 'Um textarea qualquer', 'number' => 12345, 'select' => 1, 'checkbox' => 'on', 'radio' => 1, 'password' => 'test-123', 'password_confirmation' => 'test-123', ]; } /** * Obtendo todos os atributos do form real * @return array */ public function getFormAttributes() { $dataProviderTest = $this->formFields(); $definedAttributes = array(); foreach ($dataProviderTest as $item) { $definedAttributes[] = $item[0]; } return $definedAttributes; } /** * Garantindo que o form extende do Zend form */ public function testIfClassIsASubClassOfZendForm() { $class = class_parents($this->form); $formExtendsOf = current($class); $this->assertEquals(Form::class, $formExtendsOf); } /** * Garantindo que o form real está conforme o esperado no teste * @dataProvider formFields() */ public function testFormFields($fieldName) { $this->assertTrue($this->form->has($fieldName), 'Field "' . $fieldName . '" not found.'); } /** * Verifica se os atributos estão espelhados, suas existências e respectivas ordens */ public function testIfIsAttributesMirrored() { $definedAttributes = $this->getFormAttributes(); $attributesFormClass = $this->form->getElements(); $attributesForm = array(); foreach ($attributesFormClass as $key => $value) { $attributesForm[] = $key; $messageAssert = 'Attribute "' . $key . '" not found in class test. Value - ' . $value->getName(); $this->assertContains($key, $definedAttributes, $messageAssert); } $this->assertTrue(($definedAttributes === $attributesForm), 'Attributes not equals.'); } /** * E por fim testando se os dados estão sendo validados corretamente */ public function testIfCompleteDataAreValid() { $this->form->setData($this->getData()); $this->assertTrue($this->form->isValid()); } }
$this->add([ 'name' => 'checkbox', 'type' => Checkbox::class, 'options' => [ 'label' => 'Check', // Adicionar estas duas configurações em options 'checked_value' => 'on', 'unchecked_value' => 'off' ], 'attributes' => [ 'id' => 'checkbox', 'class' => 'form-control' ] ]);E agora os testes passam!
<?php namespace ApplicationTest\Form; use Application\Form\ExampleFormFilter; use PHPUnit\Framework\TestCase; use Zend\InputFilter\BaseInputFilter; /** * Class ExampleFormFilterTest * @package ApplicationTest\Form */ class ExampleFormFilterTest extends TestCase { /** * @expectedException \Exception */ public function testSetInputFilter() { $formFilter = new ExampleFormFilter(); $filterInterface = new BaseInputFilter(); $formFilter->setInputFilter($filterInterface); } public function testGetInputFilter() { $formFilter = new ExampleFormFilter(); $result = $formFilter->getInputFilter(); $this->assertNotNull($result); $this->assertArrayHasKey('text', $result->getInputs()); $this->assertArrayHasKey('password', $result->getInputs()); $this->assertArrayHasKey('password_confirmation', $result->getInputs()); } }Pronto, rodamos mais uma vez nossos testes e tudo passa.
<?xml version="1.0" encoding="UTF-8"?> <phpunit colors="true"> <testsuites> <testsuite name="ZendSkeletonApplication Test Suite"> <directory>./module/Application/test</directory> </testsuite> </testsuites> <!--Temos que adicionar um whitelist somente com o que queremos de report--> <filter> <whitelist> <directory suffix=".php">./module/Application/src</directory> </whitelist> </filter> <!--E definir como será a saída--> <logging> <log type="coverage-html" target="./build/coverage-html" lowUpperBound="35" highLowerBound="75"/> </logging> </phpunit>Basta rodar o comando composer test e abrir o arquivo build/coverage-html/index.html para ver o resultado.